diff options
author | 2022-07-01 15:10:30 +0200 | |
---|---|---|
committer | 2022-07-01 15:10:30 +0200 | |
commit | 4f75591060d95208a301bc6bf460d875631b29cc (patch) | |
tree | 4e37d86840e8d990a563ba75d3de6f84a53cc2de | |
parent | 66568e3a39c61546c09a47a5688914a0bdf3c60c (diff) | |
download | rss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.tar.gz rss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.tar.zst rss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.zip |
Reformat codebase v4 (#2872)
Reformat code base to PSR12
Co-authored-by: rssbridge <noreply@github.com>
398 files changed, 62578 insertions, 60413 deletions
diff --git a/actions/ConnectivityAction.php b/actions/ConnectivityAction.php index 1018c4a2..c657f21b 100644 --- a/actions/ConnectivityAction.php +++ b/actions/ConnectivityAction.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -23,85 +24,84 @@ */ class ConnectivityAction implements ActionInterface { - public $userData = []; - - public function execute() { - - if(!Debug::isEnabled()) { - returnError('This action is only available in debug mode!', 400); - } - - if(!isset($this->userData['bridge'])) { - $this->returnEntryPage(); - return; - } - - $bridgeName = $this->userData['bridge']; - - $this->reportBridgeConnectivity($bridgeName); - - } - - /** - * Generates a report about the bridge connectivity status and sends it back - * to the user. - * - * The report is generated as Json-formatted string in the format - * { - * "bridge": "<bridge-name>", - * "successful": true/false - * } - * - * @param string $bridgeName Name of the bridge to generate the report for - * @return void - */ - private function reportBridgeConnectivity($bridgeName) { - - $bridgeFac = new \BridgeFactory(); - - if(!$bridgeFac->isWhitelisted($bridgeName)) { - header('Content-Type: text/html'); - returnServerError('Bridge is not whitelisted!'); - } - - header('Content-Type: text/json'); - - $retVal = array( - 'bridge' => $bridgeName, - 'successful' => false, - 'http_code' => 200, - ); - - $bridge = $bridgeFac->create($bridgeName); - - if($bridge === false) { - echo json_encode($retVal); - return; - } - - $curl_opts = array( - CURLOPT_CONNECTTIMEOUT => 5 - ); - - try { - $reply = getContents($bridge::URI, array(), $curl_opts, true); - - if($reply['code'] === 200) { - $retVal['successful'] = true; - if (strpos(implode('', $reply['status_lines']), '301 Moved Permanently')) { - $retVal['http_code'] = 301; - } - } - } catch(Exception $e) { - $retVal['successful'] = false; - } - - echo json_encode($retVal); - - } - - private function returnEntryPage() { - echo <<<EOD + public $userData = []; + + public function execute() + { + if (!Debug::isEnabled()) { + returnError('This action is only available in debug mode!', 400); + } + + if (!isset($this->userData['bridge'])) { + $this->returnEntryPage(); + return; + } + + $bridgeName = $this->userData['bridge']; + + $this->reportBridgeConnectivity($bridgeName); + } + + /** + * Generates a report about the bridge connectivity status and sends it back + * to the user. + * + * The report is generated as Json-formatted string in the format + * { + * "bridge": "<bridge-name>", + * "successful": true/false + * } + * + * @param string $bridgeName Name of the bridge to generate the report for + * @return void + */ + private function reportBridgeConnectivity($bridgeName) + { + $bridgeFac = new \BridgeFactory(); + + if (!$bridgeFac->isWhitelisted($bridgeName)) { + header('Content-Type: text/html'); + returnServerError('Bridge is not whitelisted!'); + } + + header('Content-Type: text/json'); + + $retVal = [ + 'bridge' => $bridgeName, + 'successful' => false, + 'http_code' => 200, + ]; + + $bridge = $bridgeFac->create($bridgeName); + + if ($bridge === false) { + echo json_encode($retVal); + return; + } + + $curl_opts = [ + CURLOPT_CONNECTTIMEOUT => 5 + ]; + + try { + $reply = getContents($bridge::URI, [], $curl_opts, true); + + if ($reply['code'] === 200) { + $retVal['successful'] = true; + if (strpos(implode('', $reply['status_lines']), '301 Moved Permanently')) { + $retVal['http_code'] = 301; + } + } + } catch (Exception $e) { + $retVal['successful'] = false; + } + + echo json_encode($retVal); + } + + private function returnEntryPage() + { + echo <<<EOD <!DOCTYPE html> <html> @@ -132,5 +132,5 @@ class ConnectivityAction implements ActionInterface </body> </html> EOD; - } + } } diff --git a/actions/DetectAction.php b/actions/DetectAction.php index d662d7aa..149b239d 100644 --- a/actions/DetectAction.php +++ b/actions/DetectAction.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,50 +7,49 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ class DetectAction implements ActionInterface { - public $userData = []; - - public function execute() { - $targetURL = $this->userData['url'] - or returnClientError('You must specify a url!'); - - $format = $this->userData['format'] - or returnClientError('You must specify a format!'); + public $userData = []; - $bridgeFac = new \BridgeFactory(); + public function execute() + { + $targetURL = $this->userData['url'] + or returnClientError('You must specify a url!'); - foreach($bridgeFac->getBridgeNames() as $bridgeName) { + $format = $this->userData['format'] + or returnClientError('You must specify a format!'); - if(!$bridgeFac->isWhitelisted($bridgeName)) { - continue; - } + $bridgeFac = new \BridgeFactory(); - $bridge = $bridgeFac->create($bridgeName); + foreach ($bridgeFac->getBridgeNames() as $bridgeName) { + if (!$bridgeFac->isWhitelisted($bridgeName)) { + continue; + } - if($bridge === false) { - continue; - } + $bridge = $bridgeFac->create($bridgeName); - $bridgeParams = $bridge->detectParameters($targetURL); + if ($bridge === false) { + continue; + } - if(is_null($bridgeParams)) { - continue; - } + $bridgeParams = $bridge->detectParameters($targetURL); - $bridgeParams['bridge'] = $bridgeName; - $bridgeParams['format'] = $format; + if (is_null($bridgeParams)) { + continue; + } - header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301); - die(); + $bridgeParams['bridge'] = $bridgeName; + $bridgeParams['format'] = $format; - } + header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301); + die(); + } - returnClientError('No bridge found for given URL: ' . $targetURL); - } + returnClientError('No bridge found for given URL: ' . $targetURL); + } } diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php index e7031dab..721e9446 100644 --- a/actions/DisplayAction.php +++ b/actions/DisplayAction.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,216 +7,220 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ class DisplayAction implements ActionInterface { - public $userData = []; - - private function getReturnCode($error) { - $returnCode = $error->getCode(); - if ($returnCode === 301 || $returnCode === 302) { - # Don't pass redirect codes to the exterior - $returnCode = 508; - } - return $returnCode; - } - - public function execute() { - $bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null; - - $format = $this->userData['format'] - or returnClientError('You must specify a format!'); - - $bridgeFac = new \BridgeFactory(); - - // whitelist control - if(!$bridgeFac->isWhitelisted($bridge)) { - throw new \Exception('This bridge is not whitelisted', 401); - die; - } - - // Data retrieval - $bridge = $bridgeFac->create($bridge); - $bridge->loadConfiguration(); - - $noproxy = array_key_exists('_noproxy', $this->userData) - && filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN); - - if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) { - define('NOPROXY', true); - } - - // Cache timeout - $cache_timeout = -1; - if(array_key_exists('_cache_timeout', $this->userData)) { - - if(!CUSTOM_CACHE_TIMEOUT) { - unset($this->userData['_cache_timeout']); - $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($this->userData); - header('Location: ' . $uri, true, 301); - die(); - } - - $cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT); - - } else { - $cache_timeout = $bridge->getCacheTimeout(); - } - - // Remove parameters that don't concern bridges - $bridge_params = array_diff_key( - $this->userData, - array_fill_keys( - array( - 'action', - 'bridge', - 'format', - '_noproxy', - '_cache_timeout', - '_error_time' - ), '') - ); - - // Remove parameters that don't concern caches - $cache_params = array_diff_key( - $this->userData, - array_fill_keys( - array( - 'action', - 'format', - '_noproxy', - '_cache_timeout', - '_error_time' - ), '') - ); - - // Initialize cache - $cacheFac = new CacheFactory(); - - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope(''); - $cache->purgeCache(86400); // 24 hours - $cache->setKey($cache_params); - - $items = array(); - $infos = array(); - $mtime = $cache->getTime(); - - if($mtime !== false - && (time() - $cache_timeout < $mtime) - && !Debug::isEnabled()) { // Load cached data - - // Send "Not Modified" response if client supports it - // Implementation based on https://stackoverflow.com/a/10847262 - if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { - $stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); - - if($mtime <= $stime) { // Cached data is older or same - header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304); - die(); - } - } - - $cached = $cache->loadData(); - - if(isset($cached['items']) && isset($cached['extraInfos'])) { - foreach($cached['items'] as $item) { - $items[] = new \FeedItem($item); - } - - $infos = $cached['extraInfos']; - } - - } else { // Collect new data - - try { - $bridge->setDatas($bridge_params); - $bridge->collectData(); - - $items = $bridge->getItems(); - - // Transform "legacy" items to FeedItems if necessary. - // Remove this code when support for "legacy" items ends! - if(isset($items[0]) && is_array($items[0])) { - $feedItems = array(); - - foreach($items as $item) { - $feedItems[] = new \FeedItem($item); - } - - $items = $feedItems; - } - - $infos = array( - 'name' => $bridge->getName(), - 'uri' => $bridge->getURI(), - 'donationUri' => $bridge->getDonationURI(), - 'icon' => $bridge->getIcon() - ); - } catch(\Throwable $e) { - error_log($e); - - if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) { - if(Configuration::getConfig('error', 'output') === 'feed') { - $item = new \FeedItem(); - - // Create "new" error message every 24 hours - $this->userData['_error_time'] = urlencode((int)(time() / 86400)); - - $message = sprintf( - 'Bridge returned error %s! (%s)', - $e->getCode(), - $this->userData['_error_time'] - ); - $item->setTitle($message); - - $item->setURI( - (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '') - . '?' - . http_build_query($this->userData) - ); - - $item->setTimestamp(time()); - $item->setContent(buildBridgeException($e, $bridge)); - - $items[] = $item; - } elseif(Configuration::getConfig('error', 'output') === 'http') { - header('Content-Type: text/html', true, $this->getReturnCode($e)); - die(buildTransformException($e, $bridge)); - } - } - } - - // Store data in cache - $cache->saveData(array( - 'items' => array_map(function($i){ return $i->toArray(); }, $items), - 'extraInfos' => $infos - )); - - } - - // Data transformation - try { - $formatFac = new FormatFactory(); - $format = $formatFac->create($format); - $format->setItems($items); - $format->setExtraInfos($infos); - $lastModified = $cache->getTime(); - $format->setLastModified($lastModified); - if ($lastModified) { - header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT'); - } - header('Content-Type: ' . $format->getMimeType() . '; charset=' . $format->getCharset()); - - echo $format->stringify(); - } catch(\Throwable $e) { - error_log($e); - header('Content-Type: text/html', true, $e->getCode()); - die(buildTransformException($e, $bridge)); - } - } + public $userData = []; + + private function getReturnCode($error) + { + $returnCode = $error->getCode(); + if ($returnCode === 301 || $returnCode === 302) { + # Don't pass redirect codes to the exterior + $returnCode = 508; + } + return $returnCode; + } + + public function execute() + { + $bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null; + + $format = $this->userData['format'] + or returnClientError('You must specify a format!'); + + $bridgeFac = new \BridgeFactory(); + + // whitelist control + if (!$bridgeFac->isWhitelisted($bridge)) { + throw new \Exception('This bridge is not whitelisted', 401); + die; + } + + // Data retrieval + $bridge = $bridgeFac->create($bridge); + $bridge->loadConfiguration(); + + $noproxy = array_key_exists('_noproxy', $this->userData) + && filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN); + + if (defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) { + define('NOPROXY', true); + } + + // Cache timeout + $cache_timeout = -1; + if (array_key_exists('_cache_timeout', $this->userData)) { + if (!CUSTOM_CACHE_TIMEOUT) { + unset($this->userData['_cache_timeout']); + $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($this->userData); + header('Location: ' . $uri, true, 301); + die(); + } + + $cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT); + } else { + $cache_timeout = $bridge->getCacheTimeout(); + } + + // Remove parameters that don't concern bridges + $bridge_params = array_diff_key( + $this->userData, + array_fill_keys( + [ + 'action', + 'bridge', + 'format', + '_noproxy', + '_cache_timeout', + '_error_time' + ], + '' + ) + ); + + // Remove parameters that don't concern caches + $cache_params = array_diff_key( + $this->userData, + array_fill_keys( + [ + 'action', + 'format', + '_noproxy', + '_cache_timeout', + '_error_time' + ], + '' + ) + ); + + // Initialize cache + $cacheFac = new CacheFactory(); + + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope(''); + $cache->purgeCache(86400); // 24 hours + $cache->setKey($cache_params); + + $items = []; + $infos = []; + $mtime = $cache->getTime(); + + if ( + $mtime !== false + && (time() - $cache_timeout < $mtime) + && !Debug::isEnabled() + ) { // Load cached data + // Send "Not Modified" response if client supports it + // Implementation based on https://stackoverflow.com/a/10847262 + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + $stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); + + if ($mtime <= $stime) { // Cached data is older or same + header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304); + die(); + } + } + + $cached = $cache->loadData(); + + if (isset($cached['items']) && isset($cached['extraInfos'])) { + foreach ($cached['items'] as $item) { + $items[] = new \FeedItem($item); + } + + $infos = $cached['extraInfos']; + } + } else { // Collect new data + try { + $bridge->setDatas($bridge_params); + $bridge->collectData(); + + $items = $bridge->getItems(); + + // Transform "legacy" items to FeedItems if necessary. + // Remove this code when support for "legacy" items ends! + if (isset($items[0]) && is_array($items[0])) { + $feedItems = []; + + foreach ($items as $item) { + $feedItems[] = new \FeedItem($item); + } + + $items = $feedItems; + } + + $infos = [ + 'name' => $bridge->getName(), + 'uri' => $bridge->getURI(), + 'donationUri' => $bridge->getDonationURI(), + 'icon' => $bridge->getIcon() + ]; + } catch (\Throwable $e) { + error_log($e); + + if (logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) { + if (Configuration::getConfig('error', 'output') === 'feed') { + $item = new \FeedItem(); + + // Create "new" error message every 24 hours + $this->userData['_error_time'] = urlencode((int)(time() / 86400)); + + $message = sprintf( + 'Bridge returned error %s! (%s)', + $e->getCode(), + $this->userData['_error_time'] + ); + $item->setTitle($message); + + $item->setURI( + (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '') + . '?' + . http_build_query($this->userData) + ); + + $item->setTimestamp(time()); + $item->setContent(buildBridgeException($e, $bridge)); + + $items[] = $item; + } elseif (Configuration::getConfig('error', 'output') === 'http') { + header('Content-Type: text/html', true, $this->getReturnCode($e)); + die(buildTransformException($e, $bridge)); + } + } + } + + // Store data in cache + $cache->saveData([ + 'items' => array_map(function ($i) { + return $i->toArray(); + }, $items), + 'extraInfos' => $infos + ]); + } + + // Data transformation + try { + $formatFac = new FormatFactory(); + $format = $formatFac->create($format); + $format->setItems($items); + $format->setExtraInfos($infos); + $lastModified = $cache->getTime(); + $format->setLastModified($lastModified); + if ($lastModified) { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT'); + } + header('Content-Type: ' . $format->getMimeType() . '; charset=' . $format->getCharset()); + + echo $format->stringify(); + } catch (\Throwable $e) { + error_log($e); + header('Content-Type: text/html', true, $e->getCode()); + die(buildTransformException($e, $bridge)); + } + } } diff --git a/actions/ListAction.php b/actions/ListAction.php index a778d846..7ddc42cb 100644 --- a/actions/ListAction.php +++ b/actions/ListAction.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,52 +7,49 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ class ListAction implements ActionInterface { - public function execute() { - $list = new StdClass(); - $list->bridges = array(); - $list->total = 0; - - $bridgeFac = new \BridgeFactory(); - - foreach($bridgeFac->getBridgeNames() as $bridgeName) { - - $bridge = $bridgeFac->create($bridgeName); - - if($bridge === false) { // Broken bridge, show as inactive - - $list->bridges[$bridgeName] = array( - 'status' => 'inactive' - ); - - continue; - - } - - $status = $bridgeFac->isWhitelisted($bridgeName) ? 'active' : 'inactive'; - - $list->bridges[$bridgeName] = array( - 'status' => $status, - 'uri' => $bridge->getURI(), - 'donationUri' => $bridge->getDonationURI(), - 'name' => $bridge->getName(), - 'icon' => $bridge->getIcon(), - 'parameters' => $bridge->getParameters(), - 'maintainer' => $bridge->getMaintainer(), - 'description' => $bridge->getDescription() - ); - - } - - $list->total = count($list->bridges); - - header('Content-Type: application/json'); - echo json_encode($list, JSON_PRETTY_PRINT); - } + public function execute() + { + $list = new StdClass(); + $list->bridges = []; + $list->total = 0; + + $bridgeFac = new \BridgeFactory(); + + foreach ($bridgeFac->getBridgeNames() as $bridgeName) { + $bridge = $bridgeFac->create($bridgeName); + + if ($bridge === false) { // Broken bridge, show as inactive + $list->bridges[$bridgeName] = [ + 'status' => 'inactive' + ]; + + continue; + } + + $status = $bridgeFac->isWhitelisted($bridgeName) ? 'active' : 'inactive'; + + $list->bridges[$bridgeName] = [ + 'status' => $status, + 'uri' => $bridge->getURI(), + 'donationUri' => $bridge->getDonationURI(), + 'name' => $bridge->getName(), + 'icon' => $bridge->getIcon(), + 'parameters' => $bridge->getParameters(), + 'maintainer' => $bridge->getMaintainer(), + 'description' => $bridge->getDescription() + ]; + } + + $list->total = count($list->bridges); + + header('Content-Type: application/json'); + echo json_encode($list, JSON_PRETTY_PRINT); + } } diff --git a/bridges/ABCNewsBridge.php b/bridges/ABCNewsBridge.php index 44208de1..94cd1fb3 100644 --- a/bridges/ABCNewsBridge.php +++ b/bridges/ABCNewsBridge.php @@ -1,45 +1,48 @@ <?php -class ABCNewsBridge extends BridgeAbstract { - const NAME = 'ABC News Bridge'; - const URI = 'https://www.abc.net.au'; - const DESCRIPTION = 'Topics of the Australian Broadcasting Corporation'; - const MAINTAINER = 'yue-dongchen'; - const PARAMETERS = array( - array( - 'topic' => array( - 'type' => 'list', - 'name' => 'Region', - 'title' => 'Choose state', - 'values' => array( - 'ACT' => 'act', - 'NSW' => 'nsw', - 'NT' => 'nt', - 'QLD' => 'qld', - 'SA' => 'sa', - 'TAS' => 'tas', - 'VIC' => 'vic', - 'WA' => 'wa' - ), - ) - ) - ); +class ABCNewsBridge extends BridgeAbstract +{ + const NAME = 'ABC News Bridge'; + const URI = 'https://www.abc.net.au'; + const DESCRIPTION = 'Topics of the Australian Broadcasting Corporation'; + const MAINTAINER = 'yue-dongchen'; - public function collectData() { - $url = 'https://www.abc.net.au/news/' . $this->getInput('topic'); - $html = getSimpleHTMLDOM($url)->find('.YAJzu._2FvRw.ZWhbj._3BZxh', 0); - $html = defaultLinkTo($html, $this->getURI()); + const PARAMETERS = [ + [ + 'topic' => [ + 'type' => 'list', + 'name' => 'Region', + 'title' => 'Choose state', + 'values' => [ + 'ACT' => 'act', + 'NSW' => 'nsw', + 'NT' => 'nt', + 'QLD' => 'qld', + 'SA' => 'sa', + 'TAS' => 'tas', + 'VIC' => 'vic', + 'WA' => 'wa' + ], + ] + ] + ]; - foreach($html->find('._2H7Su') as $article) { - $item = array(); + public function collectData() + { + $url = 'https://www.abc.net.au/news/' . $this->getInput('topic'); + $html = getSimpleHTMLDOM($url)->find('.YAJzu._2FvRw.ZWhbj._3BZxh', 0); + $html = defaultLinkTo($html, $this->getURI()); - $title = $article->find('._3T9Id.fmhNa.nsZdE._2c2Zy._1tOey._3EOTW', 0); - $item['title'] = $title->plaintext; - $item['uri'] = $title->href; - $item['content'] = $article->find('.rMkro._1cBaI._3PhF6._10YQT._1yL-m', 0)->plaintext; - $item['timestamp'] = strtotime($article->find('time', 0)->datetime); + foreach ($html->find('._2H7Su') as $article) { + $item = []; - $this->items[] = $item; - } - } + $title = $article->find('._3T9Id.fmhNa.nsZdE._2c2Zy._1tOey._3EOTW', 0); + $item['title'] = $title->plaintext; + $item['uri'] = $title->href; + $item['content'] = $article->find('.rMkro._1cBaI._3PhF6._10YQT._1yL-m', 0)->plaintext; + $item['timestamp'] = strtotime($article->find('time', 0)->datetime); + + $this->items[] = $item; + } + } } diff --git a/bridges/AO3Bridge.php b/bridges/AO3Bridge.php index 4a6f2ccf..f55c0d45 100644 --- a/bridges/AO3Bridge.php +++ b/bridges/AO3Bridge.php @@ -1,118 +1,130 @@ <?php -class AO3Bridge extends BridgeAbstract { - const NAME = 'AO3'; - const URI = 'https://archiveofourown.org/'; - const CACHE_TIMEOUT = 1800; - const DESCRIPTION = 'Returns works or chapters from Archive of Our Own'; - const MAINTAINER = 'Obsidienne'; - const PARAMETERS = array( - 'List' => array( - 'url' => array( - 'name' => 'url', - 'required' => true, - // Example: F/F tag, complete works only - 'exampleValue' => 'https://archiveofourown.org/works?work_search[complete]=T&tag_id=F*s*F', - ), - ), - 'Bookmarks' => array( - 'user' => array( - 'name' => 'user', - 'required' => true, - // Example: Nyaaru's bookmarks - 'exampleValue' => 'Nyaaru', - ), - ), - 'Work' => array( - 'id' => array( - 'name' => 'id', - 'required' => true, - // Example: latest chapters from A Better Past by LysSerris - 'exampleValue' => '18181853', - ), - ) - ); - - // Feed for lists of works (e.g. recent works, search results, filtered tags, - // bookmarks, series, collections). - private function collectList($url) { - $html = getSimpleHTMLDOM($url); - $html = defaultLinkTo($html, self::URI); - - foreach($html->find('.index.group > li') as $element) { - $item = array(); - - $title = $element->find('div h4 a', 0); - if (!isset($title)) continue; // discard deleted works - $item['title'] = $title->plaintext; - $item['content'] = $element; - $item['uri'] = $title->href; - - $strdate = $element->find('div p.datetime', 0)->plaintext; - $item['timestamp'] = strtotime($strdate); - - $chapters = $element->find('dl dd.chapters', 0); - // bookmarked series and external works do not have a chapters count - $chapters = (isset($chapters) ? $chapters->plaintext : 0); - $item['uid'] = $item['uri'] . "/$strdate/$chapters"; - - $this->items[] = $item; - } - } - - // Feed for recent chapters of a specific work. - private function collectWork($id) { - $url = self::URI . "/works/$id/navigate"; - $html = getSimpleHTMLDOM($url); - $html = defaultLinkTo($html, self::URI); - - $this->title = $html->find('h2 a', 0)->plaintext; - - foreach($html->find('ol.index.group > li') as $element) { - $item = array(); - - $item['title'] = $element->find('a', 0)->plaintext; - $item['content'] = $element; - $item['uri'] = $element->find('a', 0)->href; - - $strdate = $element->find('span.datetime', 0)->plaintext; - $strdate = str_replace('(', '', $strdate); - $strdate = str_replace(')', '', $strdate); - $item['timestamp'] = strtotime($strdate); - - $item['uid'] = $item['uri'] . "/$strdate"; - - $this->items[] = $item; - } - - $this->items = array_reverse($this->items); - } - - public function collectData() { - switch($this->queriedContext) { - case 'Bookmarks': - $user = $this->getInput('user'); - $this->title = $user; - $url = self::URI - . '/users/' . $user - . '/bookmarks?bookmark_search[sort_column]=bookmarkable_date'; - return $this->collectList($url); - case 'List': return $this->collectList( - $this->getInput('url') - ); - case 'Work': return $this->collectWork( - $this->getInput('id') - ); - } - } - - public function getName() { - $name = parent::getName() . " $this->queriedContext"; - if (isset($this->title)) $name .= " - $this->title"; - return $name; - } - - public function getIcon() { - return self::URI . '/favicon.ico'; - } +class AO3Bridge extends BridgeAbstract +{ + const NAME = 'AO3'; + const URI = 'https://archiveofourown.org/'; + const CACHE_TIMEOUT = 1800; + const DESCRIPTION = 'Returns works or chapters from Archive of Our Own'; + const MAINTAINER = 'Obsidienne'; + const PARAMETERS = [ + 'List' => [ + 'url' => [ + 'name' => 'url', + 'required' => true, + // Example: F/F tag, complete works only + 'exampleValue' => 'https://archiveofourown.org/works?work_search[complete]=T&tag_id=F*s*F', + ], + ], + 'Bookmarks' => [ + 'user' => [ + 'name' => 'user', + 'required' => true, + // Example: Nyaaru's bookmarks + 'exampleValue' => 'Nyaaru', + ], + ], + 'Work' => [ + 'id' => [ + 'name' => 'id', + 'required' => true, + // Example: latest chapters from A Better Past by LysSerris + 'exampleValue' => '18181853', + ], + ] + ]; + + // Feed for lists of works (e.g. recent works, search results, filtered tags, + // bookmarks, series, collections). + private function collectList($url) + { + $html = getSimpleHTMLDOM($url); + $html = defaultLinkTo($html, self::URI); + + foreach ($html->find('.index.group > li') as $element) { + $item = []; + + $title = $element->find('div h4 a', 0); + if (!isset($title)) { + continue; // discard deleted works + } + $item['title'] = $title->plaintext; + $item['content'] = $element; + $item['uri'] = $title->href; + + $strdate = $element->find('div p.datetime', 0)->plaintext; + $item['timestamp'] = strtotime($strdate); + + $chapters = $element->find('dl dd.chapters', 0); + // bookmarked series and external works do not have a chapters count + $chapters = (isset($chapters) ? $chapters->plaintext : 0); + $item['uid'] = $item['uri'] . "/$strdate/$chapters"; + + $this->items[] = $item; + } + } + + // Feed for recent chapters of a specific work. + private function collectWork($id) + { + $url = self::URI . "/works/$id/navigate"; + $html = getSimpleHTMLDOM($url); + $html = defaultLinkTo($html, self::URI); + + $this->title = $html->find('h2 a', 0)->plaintext; + + foreach ($html->find('ol.index.group > li') as $element) { + $item = []; + + $item['title'] = $element->find('a', 0)->plaintext; + $item['content'] = $element; + $item['uri'] = $element->find('a', 0)->href; + + $strdate = $element->find('span.datetime', 0)->plaintext; + $strdate = str_replace('(', '', $strdate); + $strdate = str_replace(')', '', $strdate); + $item['timestamp'] = strtotime($strdate); + + $item['uid'] = $item['uri'] . "/$strdate"; + + $this->items[] = $item; + } + + $this->items = array_reverse($this->items); + } + + public function collectData() + { + switch ($this->queriedContext) { + case 'Bookmarks': + $user = $this->getInput('user'); + $this->title = $user; + $url = self::URI + . '/users/' . $user + . '/bookmarks?bookmark_search[sort_column]=bookmarkable_date'; + return $this->collectList($url); + case 'List': + return $this->collectList( + $this->getInput('url') + ); + case 'Work': + return $this->collectWork( + $this->getInput('id') + ); + } + } + + public function getName() + { + $name = parent::getName() . " $this->queriedContext"; + if (isset($this->title)) { + $name .= " - $this->title"; + } + return $name; + } + + public function getIcon() + { + return self::URI . '/favicon.ico'; + } } diff --git a/bridges/ARDMediathekBridge.php b/bridges/ARDMediathekBridge.php index 97250272..6de8dad7 100644 --- a/bridges/ARDMediathekBridge.php +++ b/bridges/ARDMediathekBridge.php @@ -1,95 +1,98 @@ <?php -class ARDMediathekBridge extends BridgeAbstract { - const NAME = 'ARD-Mediathek Bridge'; - const URI = 'https://www.ardmediathek.de'; - const DESCRIPTION = 'Feed of any series in the ARD-Mediathek, specified by its path'; - const MAINTAINER = 'yue-dongchen'; - /* - * Number of Items to be requested from ARDmediathek API - * 12 has been observed on the wild - * 29 is the highest successfully tested value - * More Items could be fetched via pagination - * The JSON-field pagination holds more information on that - * @const PAGESIZE number of requested items - */ - const PAGESIZE = 29; - /* - * The URL Prefix of the (Webapp-)API - * @const APIENDPOINT https-URL of the used endpoint - */ - const APIENDPOINT = 'https://api.ardmediathek.de/page-gateway/widgets/ard/asset/'; - /* - * The URL prefix of the video link - * URLs from the webapp include a slug containing titles of show, episode, and tv station. - * It seems to work without that. - * @const VIDEOLINKPREFIX https-URL prefix of video links - */ - const VIDEOLINKPREFIX = 'https://www.ardmediathek.de/video/'; - /* - * The requested width of the preview image - * 432 has been observed on the wild - * The webapp seems to also compute and add the height value - * It seems to works without that. - * @const IMAGEWIDTH width in px of the preview image - */ - const IMAGEWIDTH = 432; - /* - * Placeholder that will be replace by IMAGEWIDTH in the preview image URL - * @const IMAGEWIDTHPLACEHOLDER - */ - const IMAGEWIDTHPLACEHOLDER = '{width}'; - const PARAMETERS = array( - array( - 'path' => array( - 'name' => 'Show Link or ID', - 'required' => true, - 'title' => 'Link to the show page or just its alphanumeric suffix', - 'defaultValue' => 'https://www.ardmediathek.de/sendung/45-min/Y3JpZDovL25kci5kZS8xMzkx/' - ) - ) - ); +class ARDMediathekBridge extends BridgeAbstract +{ + const NAME = 'ARD-Mediathek Bridge'; + const URI = 'https://www.ardmediathek.de'; + const DESCRIPTION = 'Feed of any series in the ARD-Mediathek, specified by its path'; + const MAINTAINER = 'yue-dongchen'; + /* + * Number of Items to be requested from ARDmediathek API + * 12 has been observed on the wild + * 29 is the highest successfully tested value + * More Items could be fetched via pagination + * The JSON-field pagination holds more information on that + * @const PAGESIZE number of requested items + */ + const PAGESIZE = 29; + /* + * The URL Prefix of the (Webapp-)API + * @const APIENDPOINT https-URL of the used endpoint + */ + const APIENDPOINT = 'https://api.ardmediathek.de/page-gateway/widgets/ard/asset/'; + /* + * The URL prefix of the video link + * URLs from the webapp include a slug containing titles of show, episode, and tv station. + * It seems to work without that. + * @const VIDEOLINKPREFIX https-URL prefix of video links + */ + const VIDEOLINKPREFIX = 'https://www.ardmediathek.de/video/'; + /* + * The requested width of the preview image + * 432 has been observed on the wild + * The webapp seems to also compute and add the height value + * It seems to works without that. + * @const IMAGEWIDTH width in px of the preview image + */ + const IMAGEWIDTH = 432; + /* + * Placeholder that will be replace by IMAGEWIDTH in the preview image URL + * @const IMAGEWIDTHPLACEHOLDER + */ + const IMAGEWIDTHPLACEHOLDER = '{width}'; - public function collectData() { - $oldTz = date_default_timezone_get(); + const PARAMETERS = [ + [ + 'path' => [ + 'name' => 'Show Link or ID', + 'required' => true, + 'title' => 'Link to the show page or just its alphanumeric suffix', + 'defaultValue' => 'https://www.ardmediathek.de/sendung/45-min/Y3JpZDovL25kci5kZS8xMzkx/' + ] + ] + ]; - date_default_timezone_set('Europe/Berlin'); + public function collectData() + { + $oldTz = date_default_timezone_get(); - $pathComponents = explode('/', $this->getInput('path')); - if (empty($pathComponents)) { - returnClientError('Path may not be empty'); - } - if (count($pathComponents) < 2) { - $showID = $pathComponents[0]; - } else { - $lastKey = count($pathComponents) - 1; - $showID = $pathComponents[$lastKey]; - if (strlen($showID) === 0) { - $showID = $pathComponents[$lastKey - 1]; - } - } + date_default_timezone_set('Europe/Berlin'); - $url = SELF::APIENDPOINT . $showID . '/?pageSize=' . SELF::PAGESIZE; - $rawJSON = getContents($url); - $processedJSON = json_decode($rawJSON); + $pathComponents = explode('/', $this->getInput('path')); + if (empty($pathComponents)) { + returnClientError('Path may not be empty'); + } + if (count($pathComponents) < 2) { + $showID = $pathComponents[0]; + } else { + $lastKey = count($pathComponents) - 1; + $showID = $pathComponents[$lastKey]; + if (strlen($showID) === 0) { + $showID = $pathComponents[$lastKey - 1]; + } + } - foreach($processedJSON->teasers as $video) { - $item = array(); - // there is also ->links->self->id, ->links->self->urlId, ->links->target->id, ->links->target->urlId - $item['uri'] = SELF::VIDEOLINKPREFIX . $video->id . '/'; - // there is also ->mediumTitle and ->shortTitle - $item['title'] = $video->longTitle; - // in the test, aspect16x9 was the only child of images, not sure whether that is always true - $item['enclosures'] = array( - str_replace(SELF::IMAGEWIDTHPLACEHOLDER, SELF::IMAGEWIDTH, $video->images->aspect16x9->src) - ); - $item['content'] = '<img src="' . $item['enclosures'][0] . '" /><p>'; - $item['timestamp'] = $video->broadcastedOn; - $item['uid'] = $video->id; - $item['author'] = $video->publicationService->name; - $this->items[] = $item; - } + $url = self::APIENDPOINT . $showID . '/?pageSize=' . self::PAGESIZE; + $rawJSON = getContents($url); + $processedJSON = json_decode($rawJSON); - date_default_timezone_set($oldTz); - } + foreach ($processedJSON->teasers as $video) { + $item = []; + // there is also ->links->self->id, ->links->self->urlId, ->links->target->id, ->links->target->urlId + $item['uri'] = self::VIDEOLINKPREFIX . $video->id . '/'; + // there is also ->mediumTitle and ->shortTitle + $item['title'] = $video->longTitle; + // in the test, aspect16x9 was the only child of images, not sure whether that is always true + $item['enclosures'] = [ + str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $video->images->aspect16x9->src) + ]; + $item['content'] = '<img src="' . $item['enclosures'][0] . '" /><p>'; + $item['timestamp'] = $video->broadcastedOn; + $item['uid'] = $video->id; + $item['author'] = $video->publicationService->name; + $this->items[] = $item; + } + + date_default_timezone_set($oldTz); + } } diff --git a/bridges/ASRockNewsBridge.php b/bridges/ASRockNewsBridge.php index 6c93798f..1b516377 100644 --- a/bridges/ASRockNewsBridge.php +++ b/bridges/ASRockNewsBridge.php @@ -1,55 +1,58 @@ <?php -class ASRockNewsBridge extends BridgeAbstract { - const NAME = 'ASRock News Bridge'; - const URI = 'https://www.asrock.com'; - const DESCRIPTION = 'Returns latest news articles'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(); - const CACHE_TIMEOUT = 3600; // 1 hour +class ASRockNewsBridge extends BridgeAbstract +{ + const NAME = 'ASRock News Bridge'; + const URI = 'https://www.asrock.com'; + const DESCRIPTION = 'Returns latest news articles'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = []; - public function collectData() { + const CACHE_TIMEOUT = 3600; // 1 hour - $html = getSimpleHTMLDOM(self::URI . '/news/index.asp'); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI . '/news/index.asp'); - $html = defaultLinkTo($html, self::URI . '/news/'); + $html = defaultLinkTo($html, self::URI . '/news/'); - foreach($html->find('div.inner > a') as $index => $a) { - $item = array(); + foreach ($html->find('div.inner > a') as $index => $a) { + $item = []; - $articlePath = $a->href; + $articlePath = $a->href; - $articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT); + $articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT); - $articlePageHtml = defaultLinkTo($articlePageHtml, self::URI); + $articlePageHtml = defaultLinkTo($articlePageHtml, self::URI); - $contents = $articlePageHtml->find('div.Contents', 0); + $contents = $articlePageHtml->find('div.Contents', 0); - $item['uri'] = $articlePath; - $item['title'] = $contents->find('h3', 0)->innertext; + $item['uri'] = $articlePath; + $item['title'] = $contents->find('h3', 0)->innertext; - $contents->find('h3', 0)->outertext = ''; + $contents->find('h3', 0)->outertext = ''; - $item['content'] = $contents->innertext; - $item['timestamp'] = $this->extractDate($a->plaintext); - $item['enclosures'][] = $a->find('img', 0)->src; - $this->items[] = $item; + $item['content'] = $contents->innertext; + $item['timestamp'] = $this->extractDate($a->plaintext); + $item['enclosures'][] = $a->find('img', 0)->src; + $this->items[] = $item; - if (count($this->items) >= 10) { - break; - } - } - } + if (count($this->items) >= 10) { + break; + } + } + } - private function extractDate($text) { - $dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/'; + private function extractDate($text) + { + $dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/'; - $text = trim($text); + $text = trim($text); - if (preg_match($dateRegex, $text, $matches)) { - return $matches[1]; - } + if (preg_match($dateRegex, $text, $matches)) { + return $matches[1]; + } - return ''; - } + return ''; + } } diff --git a/bridges/AcrimedBridge.php b/bridges/AcrimedBridge.php index 7bc73176..d37f3ce4 100644 --- a/bridges/AcrimedBridge.php +++ b/bridges/AcrimedBridge.php @@ -1,37 +1,40 @@ <?php -class AcrimedBridge extends FeedExpander { - const MAINTAINER = 'qwertygc'; - const NAME = 'Acrimed Bridge'; - const URI = 'https://www.acrimed.org/'; - const CACHE_TIMEOUT = 4800; //2hours - const DESCRIPTION = 'Returns the newest articles'; +class AcrimedBridge extends FeedExpander +{ + const MAINTAINER = 'qwertygc'; + const NAME = 'Acrimed Bridge'; + const URI = 'https://www.acrimed.org/'; + const CACHE_TIMEOUT = 4800; //2hours + const DESCRIPTION = 'Returns the newest articles'; - const PARAMETERS = [ - [ - 'limit' => [ - 'name' => 'limit', - 'type' => 'number', - 'defaultValue' => -1, - ] - ] - ]; + const PARAMETERS = [ + [ + 'limit' => [ + 'name' => 'limit', + 'type' => 'number', + 'defaultValue' => -1, + ] + ] + ]; - public function collectData(){ - $this->collectExpandableDatas( - static::URI . 'spip.php?page=backend', - $this->getInput('limit') - ); - } + public function collectData() + { + $this->collectExpandableDatas( + static::URI . 'spip.php?page=backend', + $this->getInput('limit') + ); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); - $articlePage = getSimpleHTMLDOM($newsItem->link); - $article = sanitize($articlePage->find('article.article1', 0)->innertext); - $article = defaultLinkTo($article, static::URI); - $item['content'] = $article; + $articlePage = getSimpleHTMLDOM($newsItem->link); + $article = sanitize($articlePage->find('article.article1', 0)->innertext); + $article = defaultLinkTo($article, static::URI); + $item['content'] = $article; - return $item; - } + return $item; + } } diff --git a/bridges/AirBreizhBridge.php b/bridges/AirBreizhBridge.php index 2d852da5..a822625f 100644 --- a/bridges/AirBreizhBridge.php +++ b/bridges/AirBreizhBridge.php @@ -1,54 +1,57 @@ <?php -class AirBreizhBridge extends BridgeAbstract { - const MAINTAINER = 'fanch317'; - const NAME = 'Air Breizh'; - const URI = 'https://www.airbreizh.asso.fr/'; - const DESCRIPTION = 'Returns newests publications on Air Breizh'; - const PARAMETERS = array( - 'Publications' => array( - 'theme' => array( - 'name' => 'Thematique', - 'type' => 'list', - 'values' => array( - 'Tout' => '', - 'Rapport d\'activite' => 'rapport-dactivite', - 'Etude' => 'etudes', - 'Information' => 'information', - 'Autres documents' => 'autres-documents', - 'Plan Régional de Surveillance de la qualité de l’air' => 'prsqa', - 'Transport' => 'transport' - ) - ) - ) - ); +class AirBreizhBridge extends BridgeAbstract +{ + const MAINTAINER = 'fanch317'; + const NAME = 'Air Breizh'; + const URI = 'https://www.airbreizh.asso.fr/'; + const DESCRIPTION = 'Returns newests publications on Air Breizh'; + const PARAMETERS = [ + 'Publications' => [ + 'theme' => [ + 'name' => 'Thematique', + 'type' => 'list', + 'values' => [ + 'Tout' => '', + 'Rapport d\'activite' => 'rapport-dactivite', + 'Etude' => 'etudes', + 'Information' => 'information', + 'Autres documents' => 'autres-documents', + 'Plan Régional de Surveillance de la qualité de l’air' => 'prsqa', + 'Transport' => 'transport' + ] + ] + ] + ]; - public function getIcon() { - return 'https://www.airbreizh.asso.fr/voy_content/uploads/2017/11/favicon.png'; - } + public function getIcon() + { + return 'https://www.airbreizh.asso.fr/voy_content/uploads/2017/11/favicon.png'; + } - public function collectData(){ - $html = ''; - $html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme')) - or returnClientError('No results for this query.'); + public function collectData() + { + $html = ''; + $html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme')) + or returnClientError('No results for this query.'); - foreach ($html->find('article') as $article) { - $item = array(); - // Title - $item['title'] = $article->find('h2', 0)->plaintext; - // Author - $item['author'] = 'Air Breizh'; - // Image - $imagelink = $article->find('.card__image', 0)->find('img', 0)->getAttribute('src'); - // Content preview - $item['content'] = '<img src="' . $imagelink . '" /> + foreach ($html->find('article') as $article) { + $item = []; + // Title + $item['title'] = $article->find('h2', 0)->plaintext; + // Author + $item['author'] = 'Air Breizh'; + // Image + $imagelink = $article->find('.card__image', 0)->find('img', 0)->getAttribute('src'); + // Content preview + $item['content'] = '<img src="' . $imagelink . '" /> <br/>' - . $article->find('.card__text', 0)->plaintext; - // URL - $item['uri'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href'); - // ID - $item['id'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href'); - $this->items[] = $item; - } - } + . $article->find('.card__text', 0)->plaintext; + // URL + $item['uri'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href'); + // ID + $item['id'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href'); + $this->items[] = $item; + } + } } diff --git a/bridges/AlbionOnlineBridge.php b/bridges/AlbionOnlineBridge.php index f51b815b..4b191b18 100644 --- a/bridges/AlbionOnlineBridge.php +++ b/bridges/AlbionOnlineBridge.php @@ -1,73 +1,76 @@ <?php -class AlbionOnlineBridge extends BridgeAbstract { - const NAME = 'Albion Online Changelog'; - const MAINTAINER = 'otakuf'; - const URI = 'https://albiononline.com'; - const DESCRIPTION = 'Returns the changes made to the Albion Online'; - const CACHE_TIMEOUT = 3600; // 60min +class AlbionOnlineBridge extends BridgeAbstract +{ + const NAME = 'Albion Online Changelog'; + const MAINTAINER = 'otakuf'; + const URI = 'https://albiononline.com'; + const DESCRIPTION = 'Returns the changes made to the Albion Online'; + const CACHE_TIMEOUT = 3600; // 60min - const PARAMETERS = array( array( - 'postcount' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'title' => 'Maximum number of items to return', - 'defaultValue' => 5, - ), - 'language' => array( - 'name' => 'Language', - 'type' => 'list', - 'values' => array( - 'English' => 'en', - 'Deutsch' => 'de', - 'Polski' => 'pl', - 'Français' => 'fr', - 'Русский' => 'ru', - 'Português' => 'pt', - 'Español' => 'es', - ), - 'title' => 'Language of changelog posts', - 'defaultValue' => 'en', - ), - 'full' => array( - 'name' => 'Full changelog', - 'type' => 'checkbox', - 'required' => false, - 'title' => 'Enable to receive the full changelog post for each item' - ), - )); + const PARAMETERS = [ [ + 'postcount' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'title' => 'Maximum number of items to return', + 'defaultValue' => 5, + ], + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'values' => [ + 'English' => 'en', + 'Deutsch' => 'de', + 'Polski' => 'pl', + 'Français' => 'fr', + 'Русский' => 'ru', + 'Português' => 'pt', + 'Español' => 'es', + ], + 'title' => 'Language of changelog posts', + 'defaultValue' => 'en', + ], + 'full' => [ + 'name' => 'Full changelog', + 'type' => 'checkbox', + 'required' => false, + 'title' => 'Enable to receive the full changelog post for each item' + ], + ]]; - public function collectData() { - $api = 'https://albiononline.com/'; - // Example: https://albiononline.com/en/changelog/1/5 - $url = $api . $this->getInput('language') . '/changelog/1/' . $this->getInput('postcount'); + public function collectData() + { + $api = 'https://albiononline.com/'; + // Example: https://albiononline.com/en/changelog/1/5 + $url = $api . $this->getInput('language') . '/changelog/1/' . $this->getInput('postcount'); - $html = getSimpleHTMLDOM($url); + $html = getSimpleHTMLDOM($url); - foreach ($html->find('li') as $data) { - $item = array(); - $item['uri'] = self::URI . $data->find('a', 0)->getAttribute('href'); - $item['title'] = trim(explode('|', $data->find('span', 0)->plaintext)[0]); - // Time below work only with en lang. Need to think about solution. May be separate request like getFullChangelog, but to english list for all language - //print_r( date_parse_from_format( 'M j, Y' , 'Sep 9, 2020') ); - //$item['timestamp'] = $this->extractDate($a->plaintext); - $item['author'] = 'albiononline.com'; - if($this->getInput('full')) { - $item['content'] = $this->getFullChangelog($item['uri']); - } else { - //$item['content'] = trim(preg_replace('/\s+/', ' ', $data->find('span', 0)->plaintext)); - // Just use title, no info at all or use title and date, see above - $item['content'] = $item['title']; - } - $item['uid'] = hash('sha256', $item['title']); - $this->items[] = $item; - } - } + foreach ($html->find('li') as $data) { + $item = []; + $item['uri'] = self::URI . $data->find('a', 0)->getAttribute('href'); + $item['title'] = trim(explode('|', $data->find('span', 0)->plaintext)[0]); + // Time below work only with en lang. Need to think about solution. May be separate request like getFullChangelog, but to english list for all language + //print_r( date_parse_from_format( 'M j, Y' , 'Sep 9, 2020') ); + //$item['timestamp'] = $this->extractDate($a->plaintext); + $item['author'] = 'albiononline.com'; + if ($this->getInput('full')) { + $item['content'] = $this->getFullChangelog($item['uri']); + } else { + //$item['content'] = trim(preg_replace('/\s+/', ' ', $data->find('span', 0)->plaintext)); + // Just use title, no info at all or use title and date, see above + $item['content'] = $item['title']; + } + $item['uid'] = hash('sha256', $item['title']); + $this->items[] = $item; + } + } - private function getFullChangelog($url) { - $html = getSimpleHTMLDOMCached($url); - $html = defaultLinkTo($html, self::URI); - return $html->find('div.small-12.columns', 1)->innertext; - } + private function getFullChangelog($url) + { + $html = getSimpleHTMLDOMCached($url); + $html = defaultLinkTo($html, self::URI); + return $html->find('div.small-12.columns', 1)->innertext; + } } diff --git a/bridges/AlfaBankByBridge.php b/bridges/AlfaBankByBridge.php index 4b1ed48e..7c13c14d 100644 --- a/bridges/AlfaBankByBridge.php +++ b/bridges/AlfaBankByBridge.php @@ -1,83 +1,87 @@ <?php -class AlfaBankByBridge extends BridgeAbstract { - const MAINTAINER = 'lassana'; - const NAME = 'AlfaBank.by Новости'; - const URI = 'https://www.alfabank.by'; - const DESCRIPTION = 'Уведомления Alfa-Now — новости от Альфа-Банка'; - const CACHE_TIMEOUT = 3600; // 1 hour - const PARAMETERS = array( - 'News' => array( - 'business' => array( - 'name' => 'Альфа Бизнес', - 'type' => 'list', - 'title' => 'В зависимости от выбора, возращает уведомления для" . +class AlfaBankByBridge extends BridgeAbstract +{ + const MAINTAINER = 'lassana'; + const NAME = 'AlfaBank.by Новости'; + const URI = 'https://www.alfabank.by'; + const DESCRIPTION = 'Уведомления Alfa-Now — новости от Альфа-Банка'; + const CACHE_TIMEOUT = 3600; // 1 hour + const PARAMETERS = [ + 'News' => [ + 'business' => [ + 'name' => 'Альфа Бизнес', + 'type' => 'list', + 'title' => 'В зависимости от выбора, возращает уведомления для" . " клиентов физ. лиц либо для клиентов-юридических лиц и ИП', - 'values' => array( - 'Новости' => 'news', - 'Новости бизнеса' => 'newsBusiness' - ), - 'defaultValue' => 'news' - ), - 'fullContent' => array( - 'name' => 'Включать содержимое', - 'type' => 'checkbox', - 'title' => 'Если выбрано, содержимое уведомлений вставляется в поток (работает медленно)' - ) - ) - ); + 'values' => [ + 'Новости' => 'news', + 'Новости бизнеса' => 'newsBusiness' + ], + 'defaultValue' => 'news' + ], + 'fullContent' => [ + 'name' => 'Включать содержимое', + 'type' => 'checkbox', + 'title' => 'Если выбрано, содержимое уведомлений вставляется в поток (работает медленно)' + ] + ] + ]; - public function collectData() { - $business = $this->getInput('business') == 'newsBusiness'; - $fullContent = $this->getInput('fullContent') == 'on'; + public function collectData() + { + $business = $this->getInput('business') == 'newsBusiness'; + $fullContent = $this->getInput('fullContent') == 'on'; - $mainPageUrl = self::URI . '/about/articles/uvedomleniya/'; - if($business) { - $mainPageUrl .= '?business=true'; - } - $html = getSimpleHTMLDOM($mainPageUrl); - $limit = 0; + $mainPageUrl = self::URI . '/about/articles/uvedomleniya/'; + if ($business) { + $mainPageUrl .= '?business=true'; + } + $html = getSimpleHTMLDOM($mainPageUrl); + $limit = 0; - foreach($html->find('a.notifications__item') as $element) { - if($limit < 10) { - $item = array(); - $item['uid'] = 'urn:sha1:' . hash('sha1', $element->getAttribute('data-notification-id')); - $item['title'] = $element->find('div.item-title', 0)->innertext; - $item['timestamp'] = DateTime::createFromFormat( - 'd M Y', - $this->ruMonthsToEn($element->find('div.item-date', 0)->innertext) - )->getTimestamp(); + foreach ($html->find('a.notifications__item') as $element) { + if ($limit < 10) { + $item = []; + $item['uid'] = 'urn:sha1:' . hash('sha1', $element->getAttribute('data-notification-id')); + $item['title'] = $element->find('div.item-title', 0)->innertext; + $item['timestamp'] = DateTime::createFromFormat( + 'd M Y', + $this->ruMonthsToEn($element->find('div.item-date', 0)->innertext) + )->getTimestamp(); - $itemUrl = self::URI . $element->href; - if($business) { - $itemUrl = str_replace('?business=true', '', $itemUrl); - } - $item['uri'] = $itemUrl; + $itemUrl = self::URI . $element->href; + if ($business) { + $itemUrl = str_replace('?business=true', '', $itemUrl); + } + $item['uri'] = $itemUrl; - if($fullContent) { - $itemHtml = getSimpleHTMLDOM($itemUrl); - if($itemHtml) { - $item['content'] = $itemHtml->find('div.now-p__content-text', 0)->innertext; - } - } + if ($fullContent) { + $itemHtml = getSimpleHTMLDOM($itemUrl); + if ($itemHtml) { + $item['content'] = $itemHtml->find('div.now-p__content-text', 0)->innertext; + } + } - $this->items[] = $item; - $limit++; - } - } - } + $this->items[] = $item; + $limit++; + } + } + } - public function getIcon() { - return static::URI . '/local/images/favicon.ico'; - } + public function getIcon() + { + return static::URI . '/local/images/favicon.ico'; + } - private function ruMonthsToEn($date) { - $ruMonths = array( - 'Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня', - 'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря' ); - $enMonths = array( - 'January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December' ); - return str_replace($ruMonths, $enMonths, $date); - } + private function ruMonthsToEn($date) + { + $ruMonths = [ + 'Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня', + 'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря' ]; + $enMonths = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' ]; + return str_replace($ruMonths, $enMonths, $date); + } } diff --git a/bridges/AllocineFRBridge.php b/bridges/AllocineFRBridge.php index 07d031d8..b93bccd2 100644 --- a/bridges/AllocineFRBridge.php +++ b/bridges/AllocineFRBridge.php @@ -1,113 +1,115 @@ <?php -class AllocineFRBridge extends BridgeAbstract { - const MAINTAINER = 'superbaillot.net'; - const NAME = 'Allo Cine Bridge'; - const CACHE_TIMEOUT = 25200; // 7h - const URI = 'https://www.allocine.fr'; - const DESCRIPTION = 'Bridge for allocine.fr'; - const PARAMETERS = array( array( - 'category' => array( - 'name' => 'Emission', - 'type' => 'list', - 'title' => 'Sélectionner l\'emission', - 'values' => array( - 'Faux Raccord' => 'faux-raccord', - 'Fanzone' => 'fanzone', - 'Game In Ciné' => 'game-in-cine', - 'Pour la faire courte' => 'pour-la-faire-courte', - 'Home Cinéma' => 'home-cinema', - 'PILS - Par Ici Les Sorties' => 'pils-par-ici-les-sorties', - 'AlloCiné : l\'émission, sur LeStream' => 'allocine-lemission-sur-lestream', - 'Give Me Five' => 'give-me-five', - 'Aviez-vous remarqué ?' => 'aviez-vous-remarque', - 'Et paf, il est mort' => 'et-paf-il-est-mort', - 'The Big Fan Theory' => 'the-big-fan-theory', - 'Clichés' => 'cliches', - 'Complètement...' => 'completement', - '#Fun Facts' => 'fun-facts', - 'Origin Story' => 'origin-story', - ) - ) - )); +class AllocineFRBridge extends BridgeAbstract +{ + const MAINTAINER = 'superbaillot.net'; + const NAME = 'Allo Cine Bridge'; + const CACHE_TIMEOUT = 25200; // 7h + const URI = 'https://www.allocine.fr'; + const DESCRIPTION = 'Bridge for allocine.fr'; + const PARAMETERS = [ [ + 'category' => [ + 'name' => 'Emission', + 'type' => 'list', + 'title' => 'Sélectionner l\'emission', + 'values' => [ + 'Faux Raccord' => 'faux-raccord', + 'Fanzone' => 'fanzone', + 'Game In Ciné' => 'game-in-cine', + 'Pour la faire courte' => 'pour-la-faire-courte', + 'Home Cinéma' => 'home-cinema', + 'PILS - Par Ici Les Sorties' => 'pils-par-ici-les-sorties', + 'AlloCiné : l\'émission, sur LeStream' => 'allocine-lemission-sur-lestream', + 'Give Me Five' => 'give-me-five', + 'Aviez-vous remarqué ?' => 'aviez-vous-remarque', + 'Et paf, il est mort' => 'et-paf-il-est-mort', + 'The Big Fan Theory' => 'the-big-fan-theory', + 'Clichés' => 'cliches', + 'Complètement...' => 'completement', + '#Fun Facts' => 'fun-facts', + 'Origin Story' => 'origin-story', + ] + ] + ]]; - public function getURI(){ - if(!is_null($this->getInput('category'))) { + public function getURI() + { + if (!is_null($this->getInput('category'))) { + $categories = [ + 'faux-raccord' => '/video/programme-12284/', + 'fanzone' => '/video/programme-12298/', + 'game-in-cine' => '/video/programme-12288/', + 'pour-la-faire-courte' => '/video/programme-20960/', + 'home-cinema' => '/video/programme-12287/', + 'pils-par-ici-les-sorties' => '/video/programme-25789/', + 'allocine-lemission-sur-lestream' => '/video/programme-25123/', + 'give-me-five' => '/video/programme-21919/saison-34518/', + 'aviez-vous-remarque' => '/video/programme-19518/', + 'et-paf-il-est-mort' => '/video/programme-25113/', + 'the-big-fan-theory' => '/video/programme-20403/', + 'cliches' => '/video/programme-24834/', + 'completement' => '/video/programme-23859/', + 'fun-facts' => '/video/programme-23040/', + 'origin-story' => '/video/programme-25667/' + ]; - $categories = array( - 'faux-raccord' => '/video/programme-12284/', - 'fanzone' => '/video/programme-12298/', - 'game-in-cine' => '/video/programme-12288/', - 'pour-la-faire-courte' => '/video/programme-20960/', - 'home-cinema' => '/video/programme-12287/', - 'pils-par-ici-les-sorties' => '/video/programme-25789/', - 'allocine-lemission-sur-lestream' => '/video/programme-25123/', - 'give-me-five' => '/video/programme-21919/saison-34518/', - 'aviez-vous-remarque' => '/video/programme-19518/', - 'et-paf-il-est-mort' => '/video/programme-25113/', - 'the-big-fan-theory' => '/video/programme-20403/', - 'cliches' => '/video/programme-24834/', - 'completement' => '/video/programme-23859/', - 'fun-facts' => '/video/programme-23040/', - 'origin-story' => '/video/programme-25667/' - ); + $category = $this->getInput('category'); + if (array_key_exists($category, $categories)) { + return static::URI . $this->getLastSeasonURI($categories[$category]); + } else { + returnClientError('Emission inconnue'); + } + } - $category = $this->getInput('category'); - if(array_key_exists($category, $categories)) { - return static::URI . $this->getLastSeasonURI($categories[$category]); - } else { - returnClientError('Emission inconnue'); - } - } + return parent::getURI(); + } - return parent::getURI(); - } + private function getLastSeasonURI($category) + { + $html = getSimpleHTMLDOMCached(static::URI . $category, 86400); + $seasonLink = $html->find('section[class=section-wrap section]', 0)->find('div[class=cf]', 0)->find('a', 0); + $URI = $seasonLink->href; + return $URI; + } - private function getLastSeasonURI($category) - { - $html = getSimpleHTMLDOMCached(static::URI . $category, 86400); - $seasonLink = $html->find('section[class=section-wrap section]', 0)->find('div[class=cf]', 0)->find('a', 0); - $URI = $seasonLink->href; - return $URI; - } + public function getName() + { + if (!is_null($this->getInput('category'))) { + return self::NAME . ' : ' + . array_search( + $this->getInput('category'), + self::PARAMETERS[$this->queriedContext]['category']['values'] + ); + } - public function getName(){ - if(!is_null($this->getInput('category'))) { - return self::NAME . ' : ' - . array_search( - $this->getInput('category'), - self::PARAMETERS[$this->queriedContext]['category']['values'] - ); - } + return parent::getName(); + } - return parent::getName(); - } + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); - public function collectData(){ + $category = array_search( + $this->getInput('category'), + self::PARAMETERS[$this->queriedContext]['category']['values'] + ); + foreach ($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) { + $item = []; - $html = getSimpleHTMLDOM($this->getURI()); + $title = $element->find('a[class*=meta-title-link]', 0); + $content = trim(defaultLinkTo($element->outertext, static::URI)); - $category = array_search( - $this->getInput('category'), - self::PARAMETERS[$this->queriedContext]['category']['values'] - ); - foreach($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) { - $item = array(); + // Replace image 'src' with the one in 'data-src' + $content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content); + $content = preg_replace('@data-src=@', 'src=', $content); - $title = $element->find('a[class*=meta-title-link]', 0); - $content = trim(defaultLinkTo($element->outertext, static::URI)); + // Remove date in the content to prevent content update while the video is getting older + $content = preg_replace('@<div class="meta-sub light">.*<span>[^<]*</span>[^<]*</div>@', '', $content); - // Replace image 'src' with the one in 'data-src' - $content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content); - $content = preg_replace('@data-src=@', 'src=', $content); - - // Remove date in the content to prevent content update while the video is getting older - $content = preg_replace('@<div class="meta-sub light">.*<span>[^<]*</span>[^<]*</div>@', '', $content); - - $item['content'] = $content; - $item['title'] = trim($title->innertext); - $item['uri'] = static::URI . '/' . substr($title->href, 1); - $this->items[] = $item; - } - } + $item['content'] = $content; + $item['title'] = trim($title->innertext); + $item['uri'] = static::URI . '/' . substr($title->href, 1); + $this->items[] = $item; + } + } } diff --git a/bridges/AmazonBridge.php b/bridges/AmazonBridge.php index ba440a59..40855a15 100644 --- a/bridges/AmazonBridge.php +++ b/bridges/AmazonBridge.php @@ -1,103 +1,104 @@ <?php -class AmazonBridge extends BridgeAbstract { - - const MAINTAINER = 'Alexis CHEMEL'; - const NAME = 'Amazon'; - const URI = 'https://www.amazon.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns products from Amazon search'; - - const PARAMETERS = array(array( - 'q' => array( - 'name' => 'Keyword', - 'required' => true, - 'exampleValue' => 'watch', - ), - 'sort' => array( - 'name' => 'Sort by', - 'type' => 'list', - 'values' => array( - 'Relevance' => 'relevanceblender', - 'Price: Low to High' => 'price-asc-rank', - 'Price: High to Low' => 'price-desc-rank', - 'Average Customer Review' => 'review-rank', - 'Newest Arrivals' => 'date-desc-rank', - ), - 'defaultValue' => 'relevanceblender', - ), - 'tld' => array( - 'name' => 'Country', - 'type' => 'list', - 'values' => array( - 'Australia' => 'com.au', - 'Brazil' => 'com.br', - 'Canada' => 'ca', - 'China' => 'cn', - 'France' => 'fr', - 'Germany' => 'de', - 'India' => 'in', - 'Italy' => 'it', - 'Japan' => 'co.jp', - 'Mexico' => 'com.mx', - 'Netherlands' => 'nl', - 'Spain' => 'es', - 'Sweden' => 'se', - 'Turkey' => 'com.tr', - 'United Kingdom' => 'co.uk', - 'United States' => 'com', - ), - 'defaultValue' => 'com', - ), - )); - - public function collectData() { - - $baseUrl = sprintf('https://www.amazon.%s', $this->getInput('tld')); - - $url = sprintf( - '%s/s/?field-keywords=%s&sort=%s', - $baseUrl, - urlencode($this->getInput('q')), - $this->getInput('sort') - ); - - $dom = getSimpleHTMLDOM($url); - - $elements = $dom->find('div.s-result-item'); - - foreach($elements as $element) { - $item = []; - - $title = $element->find('h2', 0); - if (!$title) { - continue; - } - - $item['title'] = $title->innertext; - - $itemUrl = $element->find('a', 0)->href; - $item['uri'] = urljoin($baseUrl, $itemUrl); - - $image = $element->find('img', 0); - if ($image) { - $item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />'; - } - - $price = $element->find('span.a-price > .a-offscreen', 0); - if ($price) { - $item['content'] .= $price->innertext; - } - - $this->items[] = $item; - } - } - - public function getName(){ - if(!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) { - return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q'); - } - - return parent::getName(); - } +class AmazonBridge extends BridgeAbstract +{ + const MAINTAINER = 'Alexis CHEMEL'; + const NAME = 'Amazon'; + const URI = 'https://www.amazon.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns products from Amazon search'; + + const PARAMETERS = [[ + 'q' => [ + 'name' => 'Keyword', + 'required' => true, + 'exampleValue' => 'watch', + ], + 'sort' => [ + 'name' => 'Sort by', + 'type' => 'list', + 'values' => [ + 'Relevance' => 'relevanceblender', + 'Price: Low to High' => 'price-asc-rank', + 'Price: High to Low' => 'price-desc-rank', + 'Average Customer Review' => 'review-rank', + 'Newest Arrivals' => 'date-desc-rank', + ], + 'defaultValue' => 'relevanceblender', + ], + 'tld' => [ + 'name' => 'Country', + 'type' => 'list', + 'values' => [ + 'Australia' => 'com.au', + 'Brazil' => 'com.br', + 'Canada' => 'ca', + 'China' => 'cn', + 'France' => 'fr', + 'Germany' => 'de', + 'India' => 'in', + 'Italy' => 'it', + 'Japan' => 'co.jp', + 'Mexico' => 'com.mx', + 'Netherlands' => 'nl', + 'Spain' => 'es', + 'Sweden' => 'se', + 'Turkey' => 'com.tr', + 'United Kingdom' => 'co.uk', + 'United States' => 'com', + ], + 'defaultValue' => 'com', + ], + ]]; + + public function collectData() + { + $baseUrl = sprintf('https://www.amazon.%s', $this->getInput('tld')); + + $url = sprintf( + '%s/s/?field-keywords=%s&sort=%s', + $baseUrl, + urlencode($this->getInput('q')), + $this->getInput('sort') + ); + + $dom = getSimpleHTMLDOM($url); + + $elements = $dom->find('div.s-result-item'); + + foreach ($elements as $element) { + $item = []; + + $title = $element->find('h2', 0); + if (!$title) { + continue; + } + + $item['title'] = $title->innertext; + + $itemUrl = $element->find('a', 0)->href; + $item['uri'] = urljoin($baseUrl, $itemUrl); + + $image = $element->find('img', 0); + if ($image) { + $item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />'; + } + + $price = $element->find('span.a-price > .a-offscreen', 0); + if ($price) { + $item['content'] .= $price->innertext; + } + + $this->items[] = $item; + } + } + + public function getName() + { + if (!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) { + return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q'); + } + + return parent::getName(); + } } diff --git a/bridges/AmazonPriceTrackerBridge.php b/bridges/AmazonPriceTrackerBridge.php index 3824c939..af8f4459 100644 --- a/bridges/AmazonPriceTrackerBridge.php +++ b/bridges/AmazonPriceTrackerBridge.php @@ -1,243 +1,257 @@ <?php -class AmazonPriceTrackerBridge extends BridgeAbstract { - const MAINTAINER = 'captn3m0, sal0max'; - const NAME = 'Amazon Price Tracker'; - const URI = 'https://www.amazon.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Tracks price for a single product on Amazon'; - - const PARAMETERS = array( - array( - 'asin' => array( - 'name' => 'ASIN', - 'required' => true, - 'exampleValue' => 'B071GB1VMQ', - // https://stackoverflow.com/a/12827734 - 'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)', - ), - 'tld' => array( - 'name' => 'Country', - 'type' => 'list', - 'values' => array( - 'Australia' => 'com.au', - 'Brazil' => 'com.br', - 'Canada' => 'ca', - 'China' => 'cn', - 'France' => 'fr', - 'Germany' => 'de', - 'India' => 'in', - 'Italy' => 'it', - 'Japan' => 'co.jp', - 'Mexico' => 'com.mx', - 'Netherlands' => 'nl', - 'Spain' => 'es', - 'Sweden' => 'se', - 'Turkey' => 'com.tr', - 'United Kingdom' => 'co.uk', - 'United States' => 'com', - ), - 'defaultValue' => 'com', - ), - )); - - const PRICE_SELECTORS = array( - '#priceblock_ourprice', - '.priceBlockBuyingPriceString', - '#newBuyBoxPrice', - '#tp_price_block_total_price_ww', - 'span.offer-price', - '.a-color-price', - ); - - const WHITESPACE = " \t\n\r\0\x0B\xC2\xA0"; - - protected $title; - - /** - * Generates domain name given a amazon TLD - */ - private function getDomainName() { - return 'https://www.amazon.' . $this->getInput('tld'); - } - - /** - * Generates URI for a Amazon product page - */ - public function getURI() { - if (!is_null($this->getInput('asin'))) { - return $this->getDomainName() . '/dp/' . $this->getInput('asin'); - } - return parent::getURI(); - } - - /** - * Scrapes the product title from the html page - * returns the default title if scraping fails - */ - private function getTitle($html) { - $titleTag = $html->find('#productTitle', 0); - - if (!$titleTag) { - return $this->getDefaultTitle(); - } else { - return trim(html_entity_decode($titleTag->innertext, ENT_QUOTES)); - } - } - - /** - * Title used by the feed if none could be found - */ - private function getDefaultTitle() { - return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin'); - } - - /** - * Returns name for the feed - * Uses title (already scraped) if it has one - */ - public function getName() { - if (isset($this->title)) { - return $this->title; - } else { - return parent::getName(); - } - } - - private function parseDynamicImage($attribute) { - $json = json_decode(html_entity_decode($attribute), true); - - if ($json and count($json) > 0) { - return array_keys($json)[0]; - } - } - - /** - * Returns a generated image tag for the product - */ - private function getImage($html) { - $imageSrc = $html->find('#main-image-container img', 0); - - if ($imageSrc) { - $hiresImage = $imageSrc->getAttribute('data-old-hires'); - $dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image'); - $image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute); - } - $image = $image ?: 'https://placekitten.com/200/300'; - - return <<<EOT +class AmazonPriceTrackerBridge extends BridgeAbstract +{ + const MAINTAINER = 'captn3m0, sal0max'; + const NAME = 'Amazon Price Tracker'; + const URI = 'https://www.amazon.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Tracks price for a single product on Amazon'; + + const PARAMETERS = [ + [ + 'asin' => [ + 'name' => 'ASIN', + 'required' => true, + 'exampleValue' => 'B071GB1VMQ', + // https://stackoverflow.com/a/12827734 + 'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)', + ], + 'tld' => [ + 'name' => 'Country', + 'type' => 'list', + 'values' => [ + 'Australia' => 'com.au', + 'Brazil' => 'com.br', + 'Canada' => 'ca', + 'China' => 'cn', + 'France' => 'fr', + 'Germany' => 'de', + 'India' => 'in', + 'Italy' => 'it', + 'Japan' => 'co.jp', + 'Mexico' => 'com.mx', + 'Netherlands' => 'nl', + 'Spain' => 'es', + 'Sweden' => 'se', + 'Turkey' => 'com.tr', + 'United Kingdom' => 'co.uk', + 'United States' => 'com', + ], + 'defaultValue' => 'com', + ], + ]]; + + const PRICE_SELECTORS = [ + '#priceblock_ourprice', + '.priceBlockBuyingPriceString', + '#newBuyBoxPrice', + '#tp_price_block_total_price_ww', + 'span.offer-price', + '.a-color-price', + ]; + + const WHITESPACE = " \t\n\r\0\x0B\xC2\xA0"; + + protected $title; + + /** + * Generates domain name given a amazon TLD + */ + private function getDomainName() + { + return 'https://www.amazon.' . $this->getInput('tld'); + } + + /** + * Generates URI for a Amazon product page + */ + public function getURI() + { + if (!is_null($this->getInput('asin'))) { + return $this->getDomainName() . '/dp/' . $this->getInput('asin'); + } + return parent::getURI(); + } + + /** + * Scrapes the product title from the html page + * returns the default title if scraping fails + */ + private function getTitle($html) + { + $titleTag = $html->find('#productTitle', 0); + + if (!$titleTag) { + return $this->getDefaultTitle(); + } else { + return trim(html_entity_decode($titleTag->innertext, ENT_QUOTES)); + } + } + + /** + * Title used by the feed if none could be found + */ + private function getDefaultTitle() + { + return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin'); + } + + /** + * Returns name for the feed + * Uses title (already scraped) if it has one + */ + public function getName() + { + if (isset($this->title)) { + return $this->title; + } else { + return parent::getName(); + } + } + + private function parseDynamicImage($attribute) + { + $json = json_decode(html_entity_decode($attribute), true); + + if ($json and count($json) > 0) { + return array_keys($json)[0]; + } + } + + /** + * Returns a generated image tag for the product + */ + private function getImage($html) + { + $imageSrc = $html->find('#main-image-container img', 0); + + if ($imageSrc) { + $hiresImage = $imageSrc->getAttribute('data-old-hires'); + $dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image'); + $image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute); + } + $image = $image ?: 'https://placekitten.com/200/300'; + + return <<<EOT <img width="300" style="max-width:300;max-height:300" src="$image" alt="{$this->title}" /> EOT; - } - - /** - * Return \simple_html_dom object - * for the entire html of the product page - */ - private function getHtml() { - $uri = $this->getURI(); - - return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.'); - } - - private function scrapePriceFromMetrics($html) { - $asinData = $html->find('#cerberus-data-metrics', 0); - - // <div id="cerberus-data-metrics" style="display: none;" - // data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0" - // data-asin-currency-code="USD" data-substitute-count="-1" ... /> - if ($asinData) { - return array( - 'price' => $asinData->getAttribute('data-asin-price'), - 'currency' => $asinData->getAttribute('data-asin-currency-code'), - 'shipping' => $asinData->getAttribute('data-asin-shipping') - ); - } - - return false; - } - - private function scrapePriceTwister($html) { - $str = $html->find('.twister-plus-buying-options-price-data', 0); - - $data = json_decode($str->innertext, true); - if(count($data) === 1) { - $data = $data[0]; - return array( - 'displayPrice' => $data['displayPrice'], - 'currency' => $data['currency'], - 'shipping' => '0', - ); - } - - return false; - } - - private function scrapePriceGeneric($html) { - $priceDiv = null; - - foreach(self::PRICE_SELECTORS as $sel) { - $priceDiv = $html->find($sel, 0); - if ($priceDiv) { - break; - } - } - - if (!$priceDiv) { - return false; - } - - $priceString = str_replace(str_split(self::WHITESPACE), '', $priceDiv->plaintext); - preg_match('/(\d+\.\d{0,2})/', $priceString, $matches); - - $price = $matches[0]; - $currency = str_replace($price, '', $priceString); - - if ($price != null && $currency != null) { - return array( - 'price' => $price, - 'currency' => $currency, - 'shipping' => '0' - ); - } - - return false; - } - - private function renderContent($image, $data) { - $price = $data['displayPrice']; - if (!$price) { - $price = "{$data['price']} {$data['currency']}"; - } - - $html = "$image<br>Price: $price"; - - if ($data['shipping'] !== '0') { - $html .= "<br>Shipping: {$data['shipping']} {$data['currency']}</br>"; - } - - return $html; - } - - /** - * Scrape method for Amazon product page - * @return [type] [description] - */ - public function collectData() { - $html = $this->getHtml(); - $this->title = $this->getTitle($html); - $imageTag = $this->getImage($html); - - $data = $this->scrapePriceGeneric($html); - - $item = array( - 'title' => $this->title, - 'uri' => $this->getURI(), - 'content' => $this->renderContent($imageTag, $data), - // This is to ensure that feed readers notice the price change - 'uid' => md5($data['price']) - ); - - $this->items[] = $item; - } + } + + /** + * Return \simple_html_dom object + * for the entire html of the product page + */ + private function getHtml() + { + $uri = $this->getURI(); + + return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.'); + } + + private function scrapePriceFromMetrics($html) + { + $asinData = $html->find('#cerberus-data-metrics', 0); + + // <div id="cerberus-data-metrics" style="display: none;" + // data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0" + // data-asin-currency-code="USD" data-substitute-count="-1" ... /> + if ($asinData) { + return [ + 'price' => $asinData->getAttribute('data-asin-price'), + 'currency' => $asinData->getAttribute('data-asin-currency-code'), + 'shipping' => $asinData->getAttribute('data-asin-shipping') + ]; + } + + return false; + } + + private function scrapePriceTwister($html) + { + $str = $html->find('.twister-plus-buying-options-price-data', 0); + + $data = json_decode($str->innertext, true); + if (count($data) === 1) { + $data = $data[0]; + return [ + 'displayPrice' => $data['displayPrice'], + 'currency' => $data['currency'], + 'shipping' => '0', + ]; + } + + return false; + } + + private function scrapePriceGeneric($html) + { + $priceDiv = null; + + foreach (self::PRICE_SELECTORS as $sel) { + $priceDiv = $html->find($sel, 0); + if ($priceDiv) { + break; + } + } + + if (!$priceDiv) { + return false; + } + + $priceString = str_replace(str_split(self::WHITESPACE), '', $priceDiv->plaintext); + preg_match('/(\d+\.\d{0,2})/', $priceString, $matches); + + $price = $matches[0]; + $currency = str_replace($price, '', $priceString); + + if ($price != null && $currency != null) { + return [ + 'price' => $price, + 'currency' => $currency, + 'shipping' => '0' + ]; + } + + return false; + } + + private function renderContent($image, $data) + { + $price = $data['displayPrice']; + if (!$price) { + $price = "{$data['price']} {$data['currency']}"; + } + + $html = "$image<br>Price: $price"; + + if ($data['shipping'] !== '0') { + $html .= "<br>Shipping: {$data['shipping']} {$data['currency']}</br>"; + } + + return $html; + } + + /** + * Scrape method for Amazon product page + * @return [type] [description] + */ + public function collectData() + { + $html = $this->getHtml(); + $this->title = $this->getTitle($html); + $imageTag = $this->getImage($html); + + $data = $this->scrapePriceGeneric($html); + + $item = [ + 'title' => $this->title, + 'uri' => $this->getURI(), + 'content' => $this->renderContent($imageTag, $data), + // This is to ensure that feed readers notice the price change + 'uid' => md5($data['price']) + ]; + + $this->items[] = $item; + } } diff --git a/bridges/AnidexBridge.php b/bridges/AnidexBridge.php index a97e434c..6d41365b 100644 --- a/bridges/AnidexBridge.php +++ b/bridges/AnidexBridge.php @@ -1,217 +1,218 @@ <?php -class AnidexBridge extends BridgeAbstract { - const MAINTAINER = 'ORelio'; - const NAME = 'Anidex'; - const URI = 'http://anidex.info/'; // anidex.info has ddos-guard so we need to use anidex.moe - const ALTERNATE_URI = 'https://anidex.moe/'; // anidex.moe returns 301 unless Host is set to anidex.info - const ALTERNATE_HOST = 'anidex.info'; // Correct host for requesting anidex.moe without 301 redirect - const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.'; - const PARAMETERS = array( - array( - 'id' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'All categories' => '0', - 'Anime' => '1,2,3', - 'Anime - Sub' => '1', - 'Anime - Raw' => '2', - 'Anime - Dub' => '3', - 'Live Action' => '4,5', - 'Live Action - Sub' => '4', - 'Live Action - Raw' => '5', - 'Light Novel' => '6', - 'Manga' => '7,8', - 'Manga - Translated' => '7', - 'Manga - Raw' => '8', - 'Music' => '9,10,11', - 'Music - Lossy' => '9', - 'Music - Lossless' => '10', - 'Music - Video' => '11', - 'Games' => '12', - 'Applications' => '13', - 'Pictures' => '14', - 'Adult Video' => '15', - 'Other' => '16' - ) - ), - 'lang_id' => array( - 'name' => 'Language', - 'type' => 'list', - 'values' => array( - 'All languages' => '0', - 'English' => '1', - 'Japanese' => '2', - 'Polish' => '3', - 'Serbo-Croatian' => '4', - 'Dutch' => '5', - 'Italian' => '6', - 'Russian' => '7', - 'German' => '8', - 'Hungarian' => '9', - 'French' => '10', - 'Finnish' => '11', - 'Vietnamese' => '12', - 'Greek' => '13', - 'Bulgarian' => '14', - 'Spanish (Spain)' => '15', - 'Portuguese (Brazil)' => '16', - 'Portuguese (Portugal)' => '17', - 'Swedish' => '18', - 'Arabic' => '19', - 'Danish' => '20', - 'Chinese (Simplified)' => '21', - 'Bengali' => '22', - 'Romanian' => '23', - 'Czech' => '24', - 'Mongolian' => '25', - 'Turkish' => '26', - 'Indonesian' => '27', - 'Korean' => '28', - 'Spanish (LATAM)' => '29', - 'Persian' => '30', - 'Malaysian' => '31' - ) - ), - 'group_id' => array( - 'name' => 'Group ID', - 'type' => 'number' - ), - 'r' => array( - 'name' => 'Hide Remakes', - 'type' => 'checkbox' - ), - 'b' => array( - 'name' => 'Only Batches', - 'type' => 'checkbox' - ), - 'a' => array( - 'name' => 'Only Authorized', - 'type' => 'checkbox' - ), - 'q' => array( - 'name' => 'Keyword', - 'description' => 'Keyword(s)', - 'type' => 'text' - ), - 'h' => array( - 'name' => 'Adult content', - 'type' => 'list', - 'values' => array( - 'No filter' => '0', - 'Hide +18' => '1', - 'Only +18' => '2' - ) - ) - ) - ); +class AnidexBridge extends BridgeAbstract +{ + const MAINTAINER = 'ORelio'; + const NAME = 'Anidex'; + const URI = 'http://anidex.info/'; // anidex.info has ddos-guard so we need to use anidex.moe + const ALTERNATE_URI = 'https://anidex.moe/'; // anidex.moe returns 301 unless Host is set to anidex.info + const ALTERNATE_HOST = 'anidex.info'; // Correct host for requesting anidex.moe without 301 redirect + const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.'; + const PARAMETERS = [ + [ + 'id' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'All categories' => '0', + 'Anime' => '1,2,3', + 'Anime - Sub' => '1', + 'Anime - Raw' => '2', + 'Anime - Dub' => '3', + 'Live Action' => '4,5', + 'Live Action - Sub' => '4', + 'Live Action - Raw' => '5', + 'Light Novel' => '6', + 'Manga' => '7,8', + 'Manga - Translated' => '7', + 'Manga - Raw' => '8', + 'Music' => '9,10,11', + 'Music - Lossy' => '9', + 'Music - Lossless' => '10', + 'Music - Video' => '11', + 'Games' => '12', + 'Applications' => '13', + 'Pictures' => '14', + 'Adult Video' => '15', + 'Other' => '16' + ] + ], + 'lang_id' => [ + 'name' => 'Language', + 'type' => 'list', + 'values' => [ + 'All languages' => '0', + 'English' => '1', + 'Japanese' => '2', + 'Polish' => '3', + 'Serbo-Croatian' => '4', + 'Dutch' => '5', + 'Italian' => '6', + 'Russian' => '7', + 'German' => '8', + 'Hungarian' => '9', + 'French' => '10', + 'Finnish' => '11', + 'Vietnamese' => '12', + 'Greek' => '13', + 'Bulgarian' => '14', + 'Spanish (Spain)' => '15', + 'Portuguese (Brazil)' => '16', + 'Portuguese (Portugal)' => '17', + 'Swedish' => '18', + 'Arabic' => '19', + 'Danish' => '20', + 'Chinese (Simplified)' => '21', + 'Bengali' => '22', + 'Romanian' => '23', + 'Czech' => '24', + 'Mongolian' => '25', + 'Turkish' => '26', + 'Indonesian' => '27', + 'Korean' => '28', + 'Spanish (LATAM)' => '29', + 'Persian' => '30', + 'Malaysian' => '31' + ] + ], + 'group_id' => [ + 'name' => 'Group ID', + 'type' => 'number' + ], + 'r' => [ + 'name' => 'Hide Remakes', + 'type' => 'checkbox' + ], + 'b' => [ + 'name' => 'Only Batches', + 'type' => 'checkbox' + ], + 'a' => [ + 'name' => 'Only Authorized', + 'type' => 'checkbox' + ], + 'q' => [ + 'name' => 'Keyword', + 'description' => 'Keyword(s)', + 'type' => 'text' + ], + 'h' => [ + 'name' => 'Adult content', + 'type' => 'list', + 'values' => [ + 'No filter' => '0', + 'Hide +18' => '1', + 'Only +18' => '2' + ] + ] + ] + ]; - public function collectData() { + public function collectData() + { + // Build Search URL from user-provided parameters + $search_url = self::ALTERNATE_URI . '?s=upload_timestamp&o=desc'; + foreach (['id', 'lang_id', 'group_id'] as $param_name) { + $param = $this->getInput($param_name); + if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) { + $search_url .= '&' . $param_name . '=' . $param; + } + } + foreach (['r', 'b', 'a'] as $param_name) { + $param = $this->getInput($param_name); + if (!empty($param) && boolval($param)) { + $search_url .= '&' . $param_name . '=1'; + } + } + $query = $this->getInput('q'); + if (!empty($query)) { + $search_url .= '&q=' . urlencode($query); + } + $opt = []; + $h = $this->getInput('h'); + if (!empty($h) && intval($h) != 0 && ctype_digit($h)) { + $opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h; + } - // Build Search URL from user-provided parameters - $search_url = self::ALTERNATE_URI . '?s=upload_timestamp&o=desc'; - foreach (array('id', 'lang_id', 'group_id') as $param_name) { - $param = $this->getInput($param_name); - if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) { - $search_url .= '&' . $param_name . '=' . $param; - } - } - foreach (array('r', 'b', 'a') as $param_name) { - $param = $this->getInput($param_name); - if (!empty($param) && boolval($param)) { - $search_url .= '&' . $param_name . '=1'; - } - } - $query = $this->getInput('q'); - if (!empty($query)) { - $search_url .= '&q=' . urlencode($query); - } - $opt = array(); - $h = $this->getInput('h'); - if (!empty($h) && intval($h) != 0 && ctype_digit($h)) { - $opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h; - } + // We need to use a different Host HTTP header to reach the correct page on ALTERNATE_URI + $headers = ['Host: ' . self::ALTERNATE_HOST]; - // We need to use a different Host HTTP header to reach the correct page on ALTERNATE_URI - $headers = array('Host: ' . self::ALTERNATE_HOST); + // The HTTPS certificate presented by anidex.moe is for anidex.info. We need to ignore this. + // As a consequence, the bridge is intentionally marked as insecure by setting self::URI to http:// + $opt[CURLOPT_SSL_VERIFYHOST] = 0; + $opt[CURLOPT_SSL_VERIFYPEER] = 0; - // The HTTPS certificate presented by anidex.moe is for anidex.info. We need to ignore this. - // As a consequence, the bridge is intentionally marked as insecure by setting self::URI to http:// - $opt[CURLOPT_SSL_VERIFYHOST] = 0; - $opt[CURLOPT_SSL_VERIFYPEER] = 0; + // Retrieve torrent listing from search results, which does not contain torrent description + $html = getSimpleHTMLDOM($search_url, $headers, $opt); + $links = $html->find('a'); + $results = []; + foreach ($links as $link) { + if (strpos($link->href, '/torrent/') === 0 && !in_array($link->href, $results)) { + $results[] = $link->href; + } + } + if (empty($results) && empty($this->getInput('q'))) { + returnServerError('No results from Anidex: ' . $search_url); + } - // Retrieve torrent listing from search results, which does not contain torrent description - $html = getSimpleHTMLDOM($search_url, $headers, $opt); - $links = $html->find('a'); - $results = array(); - foreach ($links as $link) - if (strpos($link->href, '/torrent/') === 0 && !in_array($link->href, $results)) - $results[] = $link->href; - if (empty($results) && empty($this->getInput('q'))) - returnServerError('No results from Anidex: ' . $search_url); + //Process each item individually + foreach ($results as $element) { + //Limit total amount of requests + if (count($this->items) >= 20) { + break; + } - //Process each item individually - foreach ($results as $element) { + $torrent_id = str_replace('/torrent/', '', $element); - //Limit total amount of requests - if(count($this->items) >= 20) { - break; - } + //Ignore entries without valid torrent ID + if ($torrent_id != 0 && ctype_digit($torrent_id)) { + //Retrieve data for this torrent ID + $item_browse_uri = self::URI . 'torrent/' . $torrent_id; + $item_fetch_uri = self::ALTERNATE_URI . 'torrent/' . $torrent_id; - $torrent_id = str_replace('/torrent/', '', $element); + //Retrieve full description from torrent page (cached for 24 hours: 86400 seconds) + if ($item_html = getSimpleHTMLDOMCached($item_fetch_uri, 86400, $headers, $opt)) { + //Retrieve data from page contents + $item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext); + $item_desc = $item_html->find('div.panel-body', 0); + $item_author = trim($item_html->find('span.fa-user', 0)->parent()->plaintext); + $item_date = strtotime(trim($item_html->find('span.fa-clock', 0)->parent()->plaintext)); + $item_image = $this->getURI() . 'images/user_logos/default.png'; - //Ignore entries without valid torrent ID - if ($torrent_id != 0 && ctype_digit($torrent_id)) { + //Check for description-less torrent andn optionally extract image + $desc_title_found = false; + foreach ($item_html->find('h3.panel-title') as $h3) { + if (strpos($h3, 'Description') !== false) { + $desc_title_found = true; + break; + } + } + if ($desc_title_found) { + //Retrieve image for thumbnail or generic logo fallback + foreach ($item_desc->find('img') as $img) { + if (strpos($img->src, 'prez') === false) { + $item_image = $img->src; + break; + } + } + $item_desc = trim($item_desc->innertext); + } else { + $item_desc = '<em>No description.</em>'; + } - //Retrieve data for this torrent ID - $item_browse_uri = self::URI . 'torrent/' . $torrent_id; - $item_fetch_uri = self::ALTERNATE_URI . 'torrent/' . $torrent_id; - - //Retrieve full description from torrent page (cached for 24 hours: 86400 seconds) - if ($item_html = getSimpleHTMLDOMCached($item_fetch_uri, 86400, $headers, $opt)) { - - //Retrieve data from page contents - $item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext); - $item_desc = $item_html->find('div.panel-body', 0); - $item_author = trim($item_html->find('span.fa-user', 0)->parent()->plaintext); - $item_date = strtotime(trim($item_html->find('span.fa-clock', 0)->parent()->plaintext)); - $item_image = $this->getURI() . 'images/user_logos/default.png'; - - //Check for description-less torrent andn optionally extract image - $desc_title_found = false; - foreach ($item_html->find('h3.panel-title') as $h3) { - if (strpos($h3, 'Description') !== false) { - $desc_title_found = true; - break; - } - } - if ($desc_title_found) { - //Retrieve image for thumbnail or generic logo fallback - foreach ($item_desc->find('img') as $img) { - if (strpos($img->src, 'prez') === false) { - $item_image = $img->src; - break; - } - } - $item_desc = trim($item_desc->innertext); - } else { - $item_desc = '<em>No description.</em>'; - } - - //Build and add final item - $item = array(); - $item['uri'] = $item_browse_uri; - $item['title'] = $item_title; - $item['author'] = $item_author; - $item['timestamp'] = $item_date; - $item['enclosures'] = array($item_image); - $item['content'] = $item_desc; - $this->items[] = $item; - } - } - $element = null; - } - $results = null; - } + //Build and add final item + $item = []; + $item['uri'] = $item_browse_uri; + $item['title'] = $item_title; + $item['author'] = $item_author; + $item['timestamp'] = $item_date; + $item['enclosures'] = [$item_image]; + $item['content'] = $item_desc; + $this->items[] = $item; + } + } + $element = null; + } + $results = null; + } } diff --git a/bridges/AnimeUltimeBridge.php b/bridges/AnimeUltimeBridge.php index c18821aa..23d093a6 100644 --- a/bridges/AnimeUltimeBridge.php +++ b/bridges/AnimeUltimeBridge.php @@ -1,141 +1,141 @@ <?php -class AnimeUltimeBridge extends BridgeAbstract { - - const MAINTAINER = 'ORelio'; - const NAME = 'Anime-Ultime'; - const URI = 'http://www.anime-ultime.net/'; - const CACHE_TIMEOUT = 10800; // 3h - const DESCRIPTION = 'Returns the newest releases posted on Anime-Ultime.'; - const PARAMETERS = array( array( - 'type' => array( - 'name' => 'Type', - 'type' => 'list', - 'values' => array( - 'Everything' => '', - 'Anime' => 'A', - 'Drama' => 'D', - 'Tokusatsu' => 'T' - ) - ) - )); - - private $filter = 'Releases'; - - public function collectData(){ - - //Add type filter if provided - $typeFilter = array_search( - $this->getInput('type'), - self::PARAMETERS[$this->queriedContext]['type']['values'] - ); - - //Build date and filters for making requests - $thismonth = date('mY') . $typeFilter; - $lastmonth = date('mY', mktime(0, 0, 0, date('n') - 1, 1, date('Y'))) . $typeFilter; - - //Process each HTML page until having 10 releases - $processedOK = 0; - foreach (array($thismonth, $lastmonth) as $requestFilter) { - - $url = self::URI . 'history-0-1/' . $requestFilter; - $html = getContents($url); - // Convert html from iso-8859-1 => utf8 - $html = utf8_encode($html); - $html = str_get_html($html); - - //Relases are sorted by day : process each day individually - foreach($html->find('div.history', 0)->find('h3') as $daySection) { - - //Retrieve day and build date information - $dateString = $daySection->plaintext; - $day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2)); - $item_date = strtotime(str_pad($day, 2, '0', STR_PAD_LEFT) - . '-' - . substr($requestFilter, 0, 2) - . '-' - . substr($requestFilter, 2, 4)); - - //<h3>day</h3><br /><table><tr> <-- useful data in table rows - $release = $daySection->next_sibling()->next_sibling()->first_child(); - - //Process each release of that day, ignoring first table row: contains table headers - while(!is_null($release = $release->next_sibling())) { - if(count($release->find('td')) > 0) { - - //Retrieve metadata from table columns - $item_link_element = $release->find('td', 0)->find('a', 0); - $item_uri = self::URI . $item_link_element->href; - $item_name = html_entity_decode($item_link_element->plaintext); - - $item_image = self::URI . substr( - $item_link_element->onmouseover, - 37, - strpos($item_link_element->onmouseover, ' ', 37) - 37 - ); - - $item_episode = html_entity_decode( - str_pad( - $release->find('td', 1)->plaintext, - 2, - '0', - STR_PAD_LEFT - ) - ); - - $item_fansub = $release->find('td', 2)->plaintext; - $item_type = $release->find('td', 4)->plaintext; - - if(!empty($item_uri)) { - - // Retrieve description from description page - $html_item = getContents($item_uri); - // Convert html from iso-8859-1 => utf8 - $html_item = utf8_encode($html_item); - $item_description = substr( - $html_item, - strpos($html_item, 'class="principal_contain" align="center">') + 41 - ); - $item_description = substr($item_description, - 0, - strpos($item_description, '<div id="table">') - ); - - // Convert relative image src into absolute image src, remove line breaks - $item_description = defaultLinkTo($item_description, self::URI); - $item_description = str_replace("\r", '', $item_description); - $item_description = str_replace("\n", '', $item_description); - - //Build and add final item - $item = array(); - $item['uri'] = $item_uri; - $item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode; - $item['author'] = $item_fansub; - $item['timestamp'] = $item_date; - $item['enclosures'] = array($item_image); - $item['content'] = $item_description; - $this->items[] = $item; - $processedOK++; - - //Stop processing once limit is reached - if ($processedOK >= 10) - return; - } - } - } - } - } - } - - public function getName() { - if(!is_null($this->getInput('type'))) { - $typeFilter = array_search( - $this->getInput('type'), - self::PARAMETERS[$this->queriedContext]['type']['values'] - ); - - return 'Latest ' . $typeFilter . ' - Anime-Ultime Bridge'; - } - - return parent::getName(); - } + +class AnimeUltimeBridge extends BridgeAbstract +{ + const MAINTAINER = 'ORelio'; + const NAME = 'Anime-Ultime'; + const URI = 'http://www.anime-ultime.net/'; + const CACHE_TIMEOUT = 10800; // 3h + const DESCRIPTION = 'Returns the newest releases posted on Anime-Ultime.'; + const PARAMETERS = [ [ + 'type' => [ + 'name' => 'Type', + 'type' => 'list', + 'values' => [ + 'Everything' => '', + 'Anime' => 'A', + 'Drama' => 'D', + 'Tokusatsu' => 'T' + ] + ] + ]]; + + private $filter = 'Releases'; + + public function collectData() + { + //Add type filter if provided + $typeFilter = array_search( + $this->getInput('type'), + self::PARAMETERS[$this->queriedContext]['type']['values'] + ); + + //Build date and filters for making requests + $thismonth = date('mY') . $typeFilter; + $lastmonth = date('mY', mktime(0, 0, 0, date('n') - 1, 1, date('Y'))) . $typeFilter; + + //Process each HTML page until having 10 releases + $processedOK = 0; + foreach ([$thismonth, $lastmonth] as $requestFilter) { + $url = self::URI . 'history-0-1/' . $requestFilter; + $html = getContents($url); + // Convert html from iso-8859-1 => utf8 + $html = utf8_encode($html); + $html = str_get_html($html); + + //Relases are sorted by day : process each day individually + foreach ($html->find('div.history', 0)->find('h3') as $daySection) { + //Retrieve day and build date information + $dateString = $daySection->plaintext; + $day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2)); + $item_date = strtotime(str_pad($day, 2, '0', STR_PAD_LEFT) + . '-' + . substr($requestFilter, 0, 2) + . '-' + . substr($requestFilter, 2, 4)); + + //<h3>day</h3><br /><table><tr> <-- useful data in table rows + $release = $daySection->next_sibling()->next_sibling()->first_child(); + + //Process each release of that day, ignoring first table row: contains table headers + while (!is_null($release = $release->next_sibling())) { + if (count($release->find('td')) > 0) { + //Retrieve metadata from table columns + $item_link_element = $release->find('td', 0)->find('a', 0); + $item_uri = self::URI . $item_link_element->href; + $item_name = html_entity_decode($item_link_element->plaintext); + + $item_image = self::URI . substr( + $item_link_element->onmouseover, + 37, + strpos($item_link_element->onmouseover, ' ', 37) - 37 + ); + + $item_episode = html_entity_decode( + str_pad( + $release->find('td', 1)->plaintext, + 2, + '0', + STR_PAD_LEFT + ) + ); + + $item_fansub = $release->find('td', 2)->plaintext; + $item_type = $release->find('td', 4)->plaintext; + + if (!empty($item_uri)) { + // Retrieve description from description page + $html_item = getContents($item_uri); + // Convert html from iso-8859-1 => utf8 + $html_item = utf8_encode($html_item); + $item_description = substr( + $html_item, + strpos($html_item, 'class="principal_contain" align="center">') + 41 + ); + $item_description = substr( + $item_description, + 0, + strpos($item_description, '<div id="table">') + ); + + // Convert relative image src into absolute image src, remove line breaks + $item_description = defaultLinkTo($item_description, self::URI); + $item_description = str_replace("\r", '', $item_description); + $item_description = str_replace("\n", '', $item_description); + + //Build and add final item + $item = []; + $item['uri'] = $item_uri; + $item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode; + $item['author'] = $item_fansub; + $item['timestamp'] = $item_date; + $item['enclosures'] = [$item_image]; + $item['content'] = $item_description; + $this->items[] = $item; + $processedOK++; + + //Stop processing once limit is reached + if ($processedOK >= 10) { + return; + } + } + } + } + } + } + } + + public function getName() + { + if (!is_null($this->getInput('type'))) { + $typeFilter = array_search( + $this->getInput('type'), + self::PARAMETERS[$this->queriedContext]['type']['values'] + ); + + return 'Latest ' . $typeFilter . ' - Anime-Ultime Bridge'; + } + + return parent::getName(); + } } diff --git a/bridges/AppleAppStoreBridge.php b/bridges/AppleAppStoreBridge.php index 8655a891..607581e8 100644 --- a/bridges/AppleAppStoreBridge.php +++ b/bridges/AppleAppStoreBridge.php @@ -1,151 +1,159 @@ <?php -class AppleAppStoreBridge extends BridgeAbstract { - - const MAINTAINER = 'captn3m0'; - const NAME = 'Apple App Store'; - const URI = 'https://apps.apple.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns version updates for a specific application'; - - const PARAMETERS = array(array( - 'id' => array( - 'name' => 'Application ID', - 'required' => true, - 'exampleValue' => '310633997' - ), - 'p' => array( - 'name' => 'Platform', - 'type' => 'list', - 'values' => array( - 'iPad' => 'ipad', - 'iPhone' => 'iphone', - 'Mac' => 'mac', - - // The following 2 are present in responses - // but not yet tested - 'Web' => 'web', - 'Apple TV' => 'appletv', - ), - 'defaultValue' => 'iphone', - ), - 'country' => array( - 'name' => 'Store Country', - 'type' => 'list', - 'values' => array( - 'US' => 'US', - 'India' => 'IN', - 'Canada' => 'CA', - 'Germany' => 'DE', - ), - 'defaultValue' => 'US', - ), - )); - - const PLATFORM_MAPPING = array( - 'iphone' => 'ios', - 'ipad' => 'ios', - ); - - private function makeHtmlUrl($id, $country){ - return 'https://apps.apple.com/' . $country . '/app/id' . $id; - } - - private function makeJsonUrl($id, $platform, $country){ - return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory"; - } - - public function getName(){ - if (isset($this->name)) { - return $this->name . ' - AppStore Updates'; - } - - return parent::getName(); - } - - /** - * In case of some platforms, the data is present in the initial response - */ - private function getDataFromShoebox($id, $platform, $country){ - $uri = $this->makeHtmlUrl($id, $country); - $html = getSimpleHTMLDOMCached($uri, 3600); - $script = $html->find('script[id="shoebox-ember-data-store"]', 0); - - $json = json_decode($script->innertext, true); - return $json['data']; - } - - private function getJWTToken($id, $platform, $country){ - $uri = $this->makeHtmlUrl($id, $country); - - $html = getSimpleHTMLDOMCached($uri, 3600); - - $meta = $html->find('meta[name="web-experience-app/config/environment"]', 0); - - $json = urldecode($meta->content); - - $json = json_decode($json); - - return $json->MEDIA_API->token; - } - - private function getAppData($id, $platform, $country, $token){ - $uri = $this->makeJsonUrl($id, $platform, $country); - - $headers = array( - "Authorization: Bearer $token", - 'Origin: https://apps.apple.com', - ); - - $json = json_decode(getContents($uri, $headers), true); - - return $json['data'][0]; - } - - /** - * Parses the version history from the data received - * @return array list of versions with details on each element - */ - private function getVersionHistory($data, $platform){ - switch($platform) { - case 'mac': - return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory']; - default: - $os = self::PLATFORM_MAPPING[$platform]; - return $data['attributes']['platformAttributes'][$os]['versionHistory']; - } - } - - public function collectData() { - $id = $this->getInput('id'); - $country = $this->getInput('country'); - $platform = $this->getInput('p'); - - switch ($platform) { - case 'mac': - $data = $this->getDataFromShoebox($id, $platform, $country); - break; - - default: - $token = $this->getJWTToken($id, $platform, $country); - $data = $this->getAppData($id, $platform, $country, $token); - } - - $versionHistory = $this->getVersionHistory($data, $platform); - $name = $this->name = $data['attributes']['name']; - $author = $data['attributes']['artistName']; - - foreach ($versionHistory as $row) { - $item = array(); - - $item['content'] = nl2br($row['releaseNotes']); - $item['title'] = $name . ' - ' . $row['versionDisplay']; - $item['timestamp'] = $row['releaseDate']; - $item['author'] = $author; - - $item['uri'] = $this->makeHtmlUrl($id, $country); - - $this->items[] = $item; - } - } +class AppleAppStoreBridge extends BridgeAbstract +{ + const MAINTAINER = 'captn3m0'; + const NAME = 'Apple App Store'; + const URI = 'https://apps.apple.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns version updates for a specific application'; + + const PARAMETERS = [[ + 'id' => [ + 'name' => 'Application ID', + 'required' => true, + 'exampleValue' => '310633997' + ], + 'p' => [ + 'name' => 'Platform', + 'type' => 'list', + 'values' => [ + 'iPad' => 'ipad', + 'iPhone' => 'iphone', + 'Mac' => 'mac', + + // The following 2 are present in responses + // but not yet tested + 'Web' => 'web', + 'Apple TV' => 'appletv', + ], + 'defaultValue' => 'iphone', + ], + 'country' => [ + 'name' => 'Store Country', + 'type' => 'list', + 'values' => [ + 'US' => 'US', + 'India' => 'IN', + 'Canada' => 'CA', + 'Germany' => 'DE', + ], + 'defaultValue' => 'US', + ], + ]]; + + const PLATFORM_MAPPING = [ + 'iphone' => 'ios', + 'ipad' => 'ios', + ]; + + private function makeHtmlUrl($id, $country) + { + return 'https://apps.apple.com/' . $country . '/app/id' . $id; + } + + private function makeJsonUrl($id, $platform, $country) + { + return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory"; + } + + public function getName() + { + if (isset($this->name)) { + return $this->name . ' - AppStore Updates'; + } + + return parent::getName(); + } + + /** + * In case of some platforms, the data is present in the initial response + */ + private function getDataFromShoebox($id, $platform, $country) + { + $uri = $this->makeHtmlUrl($id, $country); + $html = getSimpleHTMLDOMCached($uri, 3600); + $script = $html->find('script[id="shoebox-ember-data-store"]', 0); + + $json = json_decode($script->innertext, true); + return $json['data']; + } + + private function getJWTToken($id, $platform, $country) + { + $uri = $this->makeHtmlUrl($id, $country); + + $html = getSimpleHTMLDOMCached($uri, 3600); + + $meta = $html->find('meta[name="web-experience-app/config/environment"]', 0); + + $json = urldecode($meta->content); + + $json = json_decode($json); + + return $json->MEDIA_API->token; + } + + private function getAppData($id, $platform, $country, $token) + { + $uri = $this->makeJsonUrl($id, $platform, $country); + + $headers = [ + "Authorization: Bearer $token", + 'Origin: https://apps.apple.com', + ]; + + $json = json_decode(getContents($uri, $headers), true); + + return $json['data'][0]; + } + + /** + * Parses the version history from the data received + * @return array list of versions with details on each element + */ + private function getVersionHistory($data, $platform) + { + switch ($platform) { + case 'mac': + return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory']; + default: + $os = self::PLATFORM_MAPPING[$platform]; + return $data['attributes']['platformAttributes'][$os]['versionHistory']; + } + } + + public function collectData() + { + $id = $this->getInput('id'); + $country = $this->getInput('country'); + $platform = $this->getInput('p'); + + switch ($platform) { + case 'mac': + $data = $this->getDataFromShoebox($id, $platform, $country); + break; + + default: + $token = $this->getJWTToken($id, $platform, $country); + $data = $this->getAppData($id, $platform, $country, $token); + } + + $versionHistory = $this->getVersionHistory($data, $platform); + $name = $this->name = $data['attributes']['name']; + $author = $data['attributes']['artistName']; + + foreach ($versionHistory as $row) { + $item = []; + + $item['content'] = nl2br($row['releaseNotes']); + $item['title'] = $name . ' - ' . $row['versionDisplay']; + $item['timestamp'] = $row['releaseDate']; + $item['author'] = $author; + + $item['uri'] = $this->makeHtmlUrl($id, $country); + + $this->items[] = $item; + } + } } diff --git a/bridges/AppleMusicBridge.php b/bridges/AppleMusicBridge.php index 26efe204..4c3e0e2f 100644 --- a/bridges/AppleMusicBridge.php +++ b/bridges/AppleMusicBridge.php @@ -1,55 +1,57 @@ <?php -class AppleMusicBridge extends BridgeAbstract { - const NAME = 'Apple Music'; - const URI = 'https://www.apple.com'; - const DESCRIPTION = 'Fetches the latest releases from an artist'; - const MAINTAINER = 'bockiii'; - const PARAMETERS = array(array( - 'artist' => array( - 'name' => 'Artist ID', - 'exampleValue' => '909253', - 'required' => true, - ), - 'limit' => array( - 'name' => 'Latest X Releases (max 50)', - 'defaultValue' => '10', - 'required' => true, - ), - )); - const CACHE_TIMEOUT = 21600; // 6 hours +class AppleMusicBridge extends BridgeAbstract +{ + const NAME = 'Apple Music'; + const URI = 'https://www.apple.com'; + const DESCRIPTION = 'Fetches the latest releases from an artist'; + const MAINTAINER = 'bockiii'; + const PARAMETERS = [[ + 'artist' => [ + 'name' => 'Artist ID', + 'exampleValue' => '909253', + 'required' => true, + ], + 'limit' => [ + 'name' => 'Latest X Releases (max 50)', + 'defaultValue' => '10', + 'required' => true, + ], + ]]; + const CACHE_TIMEOUT = 21600; // 6 hours - public function collectData() { - # Limit the amount of releases to 50 - if ($this->getInput('limit') > 50) { - $limit = 50; - } else { - $limit = $this->getInput('limit'); - } + public function collectData() + { + # Limit the amount of releases to 50 + if ($this->getInput('limit') > 50) { + $limit = 50; + } else { + $limit = $this->getInput('limit'); + } - $url = 'https://itunes.apple.com/lookup?id=' - . $this->getInput('artist') - . '&entity=album&limit=' - . $limit . - '&sort=recent'; - $html = getSimpleHTMLDOM($url); + $url = 'https://itunes.apple.com/lookup?id=' + . $this->getInput('artist') + . '&entity=album&limit=' + . $limit . + '&sort=recent'; + $html = getSimpleHTMLDOM($url); - $json = json_decode($html); + $json = json_decode($html); - foreach ($json->results as $obj) { - if ($obj->wrapperType === 'collection') { - $this->items[] = array( - 'title' => $obj->artistName . ' - ' . $obj->collectionName, - 'uri' => $obj->collectionViewUrl, - 'timestamp' => $obj->releaseDate, - 'enclosures' => $obj->artworkUrl100, - 'content' => '<a href=' . $obj->collectionViewUrl - . '><img src="' . $obj->artworkUrl100 . '" /></a><br><br>' - . $obj->artistName . ' - ' . $obj->collectionName - . '<br>' - . $obj->copyright, - ); - } - } - } + foreach ($json->results as $obj) { + if ($obj->wrapperType === 'collection') { + $this->items[] = [ + 'title' => $obj->artistName . ' - ' . $obj->collectionName, + 'uri' => $obj->collectionViewUrl, + 'timestamp' => $obj->releaseDate, + 'enclosures' => $obj->artworkUrl100, + 'content' => '<a href=' . $obj->collectionViewUrl + . '><img src="' . $obj->artworkUrl100 . '" /></a><br><br>' + . $obj->artistName . ' - ' . $obj->collectionName + . '<br>' + . $obj->copyright, + ]; + } + } + } } diff --git a/bridges/ArtStationBridge.php b/bridges/ArtStationBridge.php index 55bf87a8..5a2be59d 100644 --- a/bridges/ArtStationBridge.php +++ b/bridges/ArtStationBridge.php @@ -1,92 +1,101 @@ <?php -class ArtStationBridge extends BridgeAbstract { - const NAME = 'ArtStation'; - const URI = 'https://www.artstation.com'; - const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.'; - const MAINTAINER = 'thefranke'; - const CACHE_TIMEOUT = 3600; // 1h - - const PARAMETERS = array( - 'Search Query' => array( - 'q' => array( - 'name' => 'Search term', - 'required' => true, - 'exampleValue' => 'bird' - ) - ) - ); - - public function getIcon() { - return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico'; - } - - public function getName() { - return self::NAME . ': ' . $this->getInput('q'); - } - - private function fetchSearch($searchQuery) { - $data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",'; - $data .= '"pro_first":"1","filters":[],"additional_fields":[]}'; - - $header = array( - 'Content-Type: application/json', - 'Accept: application/json' - ); - - $opts = array( - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $data, - CURLOPT_RETURNTRANSFER => true - ); - - $jsonSearchURL = self::URI . '/api/v2/search/projects.json'; - $jsonSearchStr = getContents($jsonSearchURL, $header, $opts); - return json_decode($jsonSearchStr); - } - - private function fetchProject($hashID) { - $jsonProjectURL = self::URI . '/projects/' . $hashID . '.json'; - $jsonProjectStr = getContents($jsonProjectURL); - return json_decode($jsonProjectStr); - } - - public function collectData() { - $searchTerm = $this->getInput('q'); - $jsonQuery = $this->fetchSearch($searchTerm); - - foreach($jsonQuery->data as $media) { - // get detailed info about media item - $jsonProject = $this->fetchProject($media->hash_id); - - // create item - $item = array(); - $item['title'] = $media->title; - $item['uri'] = $media->url; - $item['timestamp'] = strtotime($jsonProject->published_at); - $item['author'] = $media->user->full_name; - $item['categories'] = implode(',', $jsonProject->tags); - - $item['content'] = '<a href="' - . $media->url - . '"><img style="max-width: 100%" src="' - . $jsonProject->cover_url - . '"></a><p>' - . $jsonProject->description - . '</p>'; - - $numAssets = count($jsonProject->assets); - - if ($numAssets > 1) - $item['content'] .= '<p><a href="' - . $media->url - . '">Project contains ' - . ($numAssets - 1) - . ' more item(s).</a></p>'; - - $this->items[] = $item; - - if (count($this->items) >= 10) - break; - } - } + +class ArtStationBridge extends BridgeAbstract +{ + const NAME = 'ArtStation'; + const URI = 'https://www.artstation.com'; + const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.'; + const MAINTAINER = 'thefranke'; + const CACHE_TIMEOUT = 3600; // 1h + + const PARAMETERS = [ + 'Search Query' => [ + 'q' => [ + 'name' => 'Search term', + 'required' => true, + 'exampleValue' => 'bird' + ] + ] + ]; + + public function getIcon() + { + return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico'; + } + + public function getName() + { + return self::NAME . ': ' . $this->getInput('q'); + } + + private function fetchSearch($searchQuery) + { + $data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",'; + $data .= '"pro_first":"1","filters":[],"additional_fields":[]}'; + + $header = [ + 'Content-Type: application/json', + 'Accept: application/json' + ]; + + $opts = [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $data, + CURLOPT_RETURNTRANSFER => true + ]; + + $jsonSearchURL = self::URI . '/api/v2/search/projects.json'; + $jsonSearchStr = getContents($jsonSearchURL, $header, $opts); + return json_decode($jsonSearchStr); + } + + private function fetchProject($hashID) + { + $jsonProjectURL = self::URI . '/projects/' . $hashID . '.json'; + $jsonProjectStr = getContents($jsonProjectURL); + return json_decode($jsonProjectStr); + } + + public function collectData() + { + $searchTerm = $this->getInput('q'); + $jsonQuery = $this->fetchSearch($searchTerm); + + foreach ($jsonQuery->data as $media) { + // get detailed info about media item + $jsonProject = $this->fetchProject($media->hash_id); + + // create item + $item = []; + $item['title'] = $media->title; + $item['uri'] = $media->url; + $item['timestamp'] = strtotime($jsonProject->published_at); + $item['author'] = $media->user->full_name; + $item['categories'] = implode(',', $jsonProject->tags); + + $item['content'] = '<a href="' + . $media->url + . '"><img style="max-width: 100%" src="' + . $jsonProject->cover_url + . '"></a><p>' + . $jsonProject->description + . '</p>'; + + $numAssets = count($jsonProject->assets); + + if ($numAssets > 1) { + $item['content'] .= '<p><a href="' + . $media->url + . '">Project contains ' + . ($numAssets - 1) + . ' more item(s).</a></p>'; + } + + $this->items[] = $item; + + if (count($this->items) >= 10) { + break; + } + } + } } diff --git a/bridges/Arte7Bridge.php b/bridges/Arte7Bridge.php index 26296104..ae092c0e 100644 --- a/bridges/Arte7Bridge.php +++ b/bridges/Arte7Bridge.php @@ -1,160 +1,163 @@ <?php -class Arte7Bridge extends BridgeAbstract { - const NAME = 'Arte +7'; - const URI = 'https://www.arte.tv/'; - const MAINTAINER = 'imagoiq'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns newest videos from ARTE +7'; +class Arte7Bridge extends BridgeAbstract +{ + const NAME = 'Arte +7'; + const URI = 'https://www.arte.tv/'; + const MAINTAINER = 'imagoiq'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns newest videos from ARTE +7'; - const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA'; + const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA'; - const PARAMETERS = array( - 'global' => [ - 'sort_by' => array( - 'type' => 'list', - 'name' => 'Sort by', - 'required' => false, - 'defaultValue' => null, - 'values' => array( - 'Default' => null, - 'Video rights start date' => 'videoRightsBegin', - 'Video rights end date' => 'videoRightsEnd', - 'Brodcast date' => 'broadcastBegin', - 'Creation date' => 'creationDate', - 'Last modified' => 'lastModified', - 'Number of views' => 'views', - 'Number of views per period' => 'viewsPeriod', - 'Available screens' => 'availableScreens', - 'Episode' => 'episode' - ), - ), - 'sort_direction' => array( - 'type' => 'list', - 'name' => 'Sort direction', - 'required' => false, - 'defaultValue' => 'DESC', - 'values' => array( - 'Ascending' => 'ASC', - 'Descending' => 'DESC' - ), - ), - 'exclude_trailers' => [ - 'name' => 'Exclude trailers', - 'type' => 'checkbox', - 'required' => false, - 'defaultValue' => false - ], - ], - 'Category' => array( - 'lang' => array( - 'type' => 'list', - 'name' => 'Language', - 'values' => array( - 'Français' => 'fr', - 'Deutsch' => 'de', - 'English' => 'en', - 'Español' => 'es', - 'Polski' => 'pl', - 'Italiano' => 'it' - ), - ), - 'cat' => array( - 'type' => 'list', - 'name' => 'Category', - 'values' => array( - 'All videos' => null, - 'News & society' => 'ACT', - 'Series & fiction' => 'SER', - 'Cinema' => 'CIN', - 'Culture' => 'ARS', - 'Culture pop' => 'CPO', - 'Discovery' => 'DEC', - 'History' => 'HIST', - 'Science' => 'SCI', - 'Other' => 'AUT' - ) - ), - ), - 'Collection' => array( - 'lang' => array( - 'type' => 'list', - 'name' => 'Language', - 'values' => array( - 'Français' => 'fr', - 'Deutsch' => 'de', - 'English' => 'en', - 'Español' => 'es', - 'Polski' => 'pl', - 'Italiano' => 'it' - ) - ), - 'col' => array( - 'name' => 'Collection id', - 'required' => true, - 'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/', - 'exampleValue' => 'RC-014095' - ) - ) - ); + const PARAMETERS = [ + 'global' => [ + 'sort_by' => [ + 'type' => 'list', + 'name' => 'Sort by', + 'required' => false, + 'defaultValue' => null, + 'values' => [ + 'Default' => null, + 'Video rights start date' => 'videoRightsBegin', + 'Video rights end date' => 'videoRightsEnd', + 'Brodcast date' => 'broadcastBegin', + 'Creation date' => 'creationDate', + 'Last modified' => 'lastModified', + 'Number of views' => 'views', + 'Number of views per period' => 'viewsPeriod', + 'Available screens' => 'availableScreens', + 'Episode' => 'episode' + ], + ], + 'sort_direction' => [ + 'type' => 'list', + 'name' => 'Sort direction', + 'required' => false, + 'defaultValue' => 'DESC', + 'values' => [ + 'Ascending' => 'ASC', + 'Descending' => 'DESC' + ], + ], + 'exclude_trailers' => [ + 'name' => 'Exclude trailers', + 'type' => 'checkbox', + 'required' => false, + 'defaultValue' => false + ], + ], + 'Category' => [ + 'lang' => [ + 'type' => 'list', + 'name' => 'Language', + 'values' => [ + 'Français' => 'fr', + 'Deutsch' => 'de', + 'English' => 'en', + 'Español' => 'es', + 'Polski' => 'pl', + 'Italiano' => 'it' + ], + ], + 'cat' => [ + 'type' => 'list', + 'name' => 'Category', + 'values' => [ + 'All videos' => null, + 'News & society' => 'ACT', + 'Series & fiction' => 'SER', + 'Cinema' => 'CIN', + 'Culture' => 'ARS', + 'Culture pop' => 'CPO', + 'Discovery' => 'DEC', + 'History' => 'HIST', + 'Science' => 'SCI', + 'Other' => 'AUT' + ] + ], + ], + 'Collection' => [ + 'lang' => [ + 'type' => 'list', + 'name' => 'Language', + 'values' => [ + 'Français' => 'fr', + 'Deutsch' => 'de', + 'English' => 'en', + 'Español' => 'es', + 'Polski' => 'pl', + 'Italiano' => 'it' + ] + ], + 'col' => [ + 'name' => 'Collection id', + 'required' => true, + 'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/', + 'exampleValue' => 'RC-014095' + ] + ] + ]; - public function collectData(){ - switch($this->queriedContext) { - case 'Category': - $category = $this->getInput('cat'); - $collectionId = null; - break; - case 'Collection': - $collectionId = $this->getInput('col'); - $category = null; - break; - } + public function collectData() + { + switch ($this->queriedContext) { + case 'Category': + $category = $this->getInput('cat'); + $collectionId = null; + break; + case 'Collection': + $collectionId = $this->getInput('col'); + $category = null; + break; + } - $lang = $this->getInput('lang'); - $sort_by = $this->getInput('sort_by'); - $sort_direction = $this->getInput('sort_direction') == 'ASC' ? '' : '-'; + $lang = $this->getInput('lang'); + $sort_by = $this->getInput('sort_by'); + $sort_direction = $this->getInput('sort_direction') == 'ASC' ? '' : '-'; - $url = 'https://api.arte.tv/api/opa/v3/videos?limit=15&language=' - . $lang - . ($sort_by != null ? '&sort=' . $sort_direction . $sort_by : '') - . ($category != null ? '&category.code=' . $category : '') - . ($collectionId != null ? '&collections.collectionId=' . $collectionId : ''); + $url = 'https://api.arte.tv/api/opa/v3/videos?limit=15&language=' + . $lang + . ($sort_by != null ? '&sort=' . $sort_direction . $sort_by : '') + . ($category != null ? '&category.code=' . $category : '') + . ($collectionId != null ? '&collections.collectionId=' . $collectionId : ''); - $header = array( - 'Authorization: Bearer ' . self::API_TOKEN - ); + $header = [ + 'Authorization: Bearer ' . self::API_TOKEN + ]; - $input = getContents($url, $header); - $input_json = json_decode($input, true); + $input = getContents($url, $header); + $input_json = json_decode($input, true); - foreach($input_json['videos'] as $element) { - if($this->getInput('exclude_trailers') && $element['platform'] == 'EXTRAIT') { - continue; - } + foreach ($input_json['videos'] as $element) { + if ($this->getInput('exclude_trailers') && $element['platform'] == 'EXTRAIT') { + continue; + } - $durationSeconds = $element['durationSeconds']; + $durationSeconds = $element['durationSeconds']; - $item = array(); - $item['uri'] = $element['url']; - $item['id'] = $element['id']; + $item = []; + $item['uri'] = $element['url']; + $item['id'] = $element['id']; - $item['timestamp'] = strtotime($element['videoRightsBegin']); - $item['title'] = $element['title']; + $item['timestamp'] = strtotime($element['videoRightsBegin']); + $item['title'] = $element['title']; - if(!empty($element['subtitle'])) - $item['title'] = $element['title'] . ' | ' . $element['subtitle']; + if (!empty($element['subtitle'])) { + $item['title'] = $element['title'] . ' | ' . $element['subtitle']; + } - $durationMinutes = round((int)$durationSeconds / 60); - $item['content'] = $element['teaserText'] - . '<br><br>' - . $durationMinutes - . 'min<br><a href="' - . $item['uri'] - . '"><img src="' - . $element['mainImage']['url'] - . '" /></a>'; + $durationMinutes = round((int)$durationSeconds / 60); + $item['content'] = $element['teaserText'] + . '<br><br>' + . $durationMinutes + . 'min<br><a href="' + . $item['uri'] + . '"><img src="' + . $element['mainImage']['url'] + . '" /></a>'; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/AsahiShimbunAJWBridge.php b/bridges/AsahiShimbunAJWBridge.php index 2e7c5dc2..226196d8 100644 --- a/bridges/AsahiShimbunAJWBridge.php +++ b/bridges/AsahiShimbunAJWBridge.php @@ -1,72 +1,76 @@ <?php -class AsahiShimbunAJWBridge extends BridgeAbstract { - const NAME = 'Asahi Shimbun AJW'; - const BASE_URI = 'http://www.asahi.com'; - const URI = self::BASE_URI . '/ajw/'; - const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - array( - 'section' => array( - 'type' => 'list', - 'name' => 'Section', - 'values' => array( - 'Japan » Social Affairs' => 'japan/social', - 'Japan » People' => 'japan/people', - 'Japan » 3/11 Disaster' => 'japan/0311disaster', - 'Japan » Sci & Tech' => 'japan/sci_tech', - 'Politics' => 'politics', - 'Business' => 'business', - 'Culture » Style' => 'culture/style', - 'Culture » Movies' => 'culture/movies', - 'Culture » Manga & Anime' => 'culture/manga_anime', - 'Asia » China' => 'asia_world/china', - 'Asia » Korean Peninsula' => 'asia_world/korean_peninsula', - 'Asia » Around Asia' => 'asia_world/around_asia', - 'Asia » World' => 'asia_world/world', - 'Opinion » Editorial' => 'opinion/editorial', - 'Opinion » Vox Populi' => 'opinion/vox', - ), - 'defaultValue' => 'politics', - ) - ) - ); - private function getSectionURI($section) { - return self::getURI() . $section . '/'; - } +class AsahiShimbunAJWBridge extends BridgeAbstract +{ + const NAME = 'Asahi Shimbun AJW'; + const BASE_URI = 'http://www.asahi.com'; + const URI = self::BASE_URI . '/ajw/'; + const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + [ + 'section' => [ + 'type' => 'list', + 'name' => 'Section', + 'values' => [ + 'Japan » Social Affairs' => 'japan/social', + 'Japan » People' => 'japan/people', + 'Japan » 3/11 Disaster' => 'japan/0311disaster', + 'Japan » Sci & Tech' => 'japan/sci_tech', + 'Politics' => 'politics', + 'Business' => 'business', + 'Culture » Style' => 'culture/style', + 'Culture » Movies' => 'culture/movies', + 'Culture » Manga & Anime' => 'culture/manga_anime', + 'Asia » China' => 'asia_world/china', + 'Asia » Korean Peninsula' => 'asia_world/korean_peninsula', + 'Asia » Around Asia' => 'asia_world/around_asia', + 'Asia » World' => 'asia_world/world', + 'Opinion » Editorial' => 'opinion/editorial', + 'Opinion » Vox Populi' => 'opinion/vox', + ], + 'defaultValue' => 'politics', + ] + ] + ]; - public function collectData() { - $html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section'))); + private function getSectionURI($section) + { + return self::getURI() . $section . '/'; + } - foreach($html->find('#MainInner li a') as $element) { - if ($element->parent()->class == 'HeadlineTopImage-S') { - Debug::log('Skip Headline, it is repeated below'); - continue; - } - $item = array(); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section'))); - $item['uri'] = self::BASE_URI . $element->href; - $e_lead = $element->find('span.Lead', 0); - if ($e_lead) { - $item['content'] = $e_lead->innertext; - $e_lead->outertext = ''; - } else { - $item['content'] = $element->innertext; - } - $e_date = $element->find('span.EnDate', 0); - if ($e_date) { - $item['timestamp'] = strtotime($e_date->innertext); - $e_date->outertext = ''; - } - $e_video = $element->find('span.EnVideo', 0); - if ($e_video) { - $e_video->outertext = ''; - $element->innertext = "VIDEO: $element->innertext"; - } - $item['title'] = $element->innertext; + foreach ($html->find('#MainInner li a') as $element) { + if ($element->parent()->class == 'HeadlineTopImage-S') { + Debug::log('Skip Headline, it is repeated below'); + continue; + } + $item = []; - $this->items[] = $item; - } - } + $item['uri'] = self::BASE_URI . $element->href; + $e_lead = $element->find('span.Lead', 0); + if ($e_lead) { + $item['content'] = $e_lead->innertext; + $e_lead->outertext = ''; + } else { + $item['content'] = $element->innertext; + } + $e_date = $element->find('span.EnDate', 0); + if ($e_date) { + $item['timestamp'] = strtotime($e_date->innertext); + $e_date->outertext = ''; + } + $e_video = $element->find('span.EnVideo', 0); + if ($e_video) { + $e_video->outertext = ''; + $element->innertext = "VIDEO: $element->innertext"; + } + $item['title'] = $element->innertext; + + $this->items[] = $item; + } + } } diff --git a/bridges/AskfmBridge.php b/bridges/AskfmBridge.php index cf92ed6a..0a326417 100644 --- a/bridges/AskfmBridge.php +++ b/bridges/AskfmBridge.php @@ -1,74 +1,79 @@ <?php -class AskfmBridge extends BridgeAbstract { - const MAINTAINER = 'az5he6ch, logmanoriginal'; - const NAME = 'Ask.fm Answers'; - const URI = 'https://ask.fm/'; - const CACHE_TIMEOUT = 300; //5 min - const DESCRIPTION = 'Returns answers from an Ask.fm user'; - const PARAMETERS = array( - 'Ask.fm username' => array( - 'u' => array( - 'name' => 'Username', - 'required' => true, - 'exampleValue' => 'ApprovedAndReal' - ) - ) - ); +class AskfmBridge extends BridgeAbstract +{ + const MAINTAINER = 'az5he6ch, logmanoriginal'; + const NAME = 'Ask.fm Answers'; + const URI = 'https://ask.fm/'; + const CACHE_TIMEOUT = 300; //5 min + const DESCRIPTION = 'Returns answers from an Ask.fm user'; + const PARAMETERS = [ + 'Ask.fm username' => [ + 'u' => [ + 'name' => 'Username', + 'required' => true, + 'exampleValue' => 'ApprovedAndReal' + ] + ] + ]; - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); - $html = defaultLinkTo($html, self::URI); + $html = defaultLinkTo($html, self::URI); - foreach($html->find('article.streamItem-answer') as $element) { - $item = array(); - $item['uri'] = $element->find('a.streamItem_meta', 0)->href; - $question = trim($element->find('header.streamItem_header', 0)->innertext); + foreach ($html->find('article.streamItem-answer') as $element) { + $item = []; + $item['uri'] = $element->find('a.streamItem_meta', 0)->href; + $question = trim($element->find('header.streamItem_header', 0)->innertext); - $item['title'] = trim( - htmlspecialchars_decode($element->find('header.streamItem_header', 0)->plaintext, - ENT_QUOTES - ) - ); + $item['title'] = trim( + htmlspecialchars_decode( + $element->find('header.streamItem_header', 0)->plaintext, + ENT_QUOTES + ) + ); - $item['timestamp'] = strtotime($element->find('time', 0)->datetime); + $item['timestamp'] = strtotime($element->find('time', 0)->datetime); - $answer = trim($element->find('div.streamItem_content', 0)->innertext); + $answer = trim($element->find('div.streamItem_content', 0)->innertext); - // This probably should be cleaned up, especially for YouTube embeds - if($visual = $element->find('div.streamItem_visual', 0)) { - $visual = $visual->innertext; - } + // This probably should be cleaned up, especially for YouTube embeds + if ($visual = $element->find('div.streamItem_visual', 0)) { + $visual = $visual->innertext; + } - // Fix tracking links, also doesn't work - foreach($element->find('a') as $link) { - if(strpos($link->href, 'l.ask.fm') !== false) { - $link->href = $link->plaintext; - } - } + // Fix tracking links, also doesn't work + foreach ($element->find('a') as $link) { + if (strpos($link->href, 'l.ask.fm') !== false) { + $link->href = $link->plaintext; + } + } - $item['content'] = '<p>' . $question - . '</p><p>' . $answer - . '</p><p>' . $visual . '</p>'; + $item['content'] = '<p>' . $question + . '</p><p>' . $answer + . '</p><p>' . $visual . '</p>'; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getName(){ - if(!is_null($this->getInput('u'))) { - return self::NAME . ' : ' . $this->getInput('u'); - } + public function getName() + { + if (!is_null($this->getInput('u'))) { + return self::NAME . ' : ' . $this->getInput('u'); + } - return parent::getName(); - } + return parent::getName(); + } - public function getURI(){ - if(!is_null($this->getInput('u'))) { - return self::URI . urlencode($this->getInput('u')); - } + public function getURI() + { + if (!is_null($this->getInput('u'))) { + return self::URI . urlencode($this->getInput('u')); + } - return parent::getURI(); - } + return parent::getURI(); + } } diff --git a/bridges/AssociatedPressNewsBridge.php b/bridges/AssociatedPressNewsBridge.php index 80fa98fc..303168a0 100644 --- a/bridges/AssociatedPressNewsBridge.php +++ b/bridges/AssociatedPressNewsBridge.php @@ -1,270 +1,278 @@ <?php -class AssociatedPressNewsBridge extends BridgeAbstract { - const NAME = 'Associated Press News Bridge'; - const URI = 'https://apnews.com/'; - const DESCRIPTION = 'Returns newest articles by topic'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - 'Standard Topics' => array( - 'topic' => array( - 'name' => 'Topic', - 'type' => 'list', - 'values' => array( - 'AP Top News' => 'apf-topnews', - 'Sports' => 'apf-sports', - 'Entertainment' => 'apf-entertainment', - 'Oddities' => 'apf-oddities', - 'Travel' => 'apf-Travel', - 'Technology' => 'apf-technology', - 'Lifestyle' => 'apf-lifestyle', - 'Business' => 'apf-business', - 'U.S. News' => 'apf-usnews', - 'Health' => 'apf-Health', - 'Science' => 'apf-science', - 'World News' => 'apf-WorldNews', - 'Politics' => 'apf-politics', - 'Religion' => 'apf-religion', - 'Photo Galleries' => 'PhotoGalleries', - 'Fact Checks' => 'APFactCheck', - 'Videos' => 'apf-videos', - ), - 'defaultValue' => 'apf-topnews', - ), - ), - 'Custom Topic' => array( - 'topic' => array( - 'name' => 'Topic', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'europe' - ), - ) - ); - - const CACHE_TIMEOUT = 900; // 15 mins - - private $detectParamRegex = '/^https?:\/\/(?:www\.)?apnews\.com\/(?:[tag|hub]+\/)?([\w-]+)$/'; - private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags='; - private $feedName = ''; - - public function detectParameters($url) { - $params = array(); - - if(preg_match($this->detectParamRegex, $url, $matches) > 0) { - $params['topic'] = $matches[1]; - $params['context'] = 'Custom Topic'; - return $params; - } - - return null; - } - - public function collectData() { - switch($this->getInput('topic')) { - case 'Podcasts': - returnClientError('Podcasts topic feed is not supported'); - break; - case 'PressReleases': - returnClientError('PressReleases topic feed is not supported'); - break; - default: - $this->collectCardData(); - } - } - - public function getURI() { - if (!is_null($this->getInput('topic'))) { - return self::URI . $this->getInput('topic'); - } - - return parent::getURI(); - } - - public function getName() { - if (!empty($this->feedName)) { - return $this->feedName . ' - Associated Press'; - } - - return parent::getName(); - } - - private function getTagURI() { - if (!is_null($this->getInput('topic'))) { - return $this->tagEndpoint . $this->getInput('topic'); - } - - return parent::getURI(); - } - - private function collectCardData() { - $json = getContents($this->getTagURI()) - or returnServerError('Could not request: ' . $this->getTagURI()); - - $tagContents = json_decode($json, true); - - if (empty($tagContents['tagObjs'])) { - returnClientError('Topic not found: ' . $this->getInput('topic')); - } - - $this->feedName = $tagContents['tagObjs'][0]['name']; - - foreach ($tagContents['cards'] as $card) { - $item = array(); - - // skip hub peeks & Notifications - if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') { - continue; - } - - $storyContent = $card['contents'][0]; - - switch($storyContent['contentType']) { - case 'web': // Skip link only content - continue 2; - - case 'video': - $html = $this->processVideo($storyContent); - - $item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/' - . $storyContent['media'][0]['id'] . '/800.jpeg'; - break; - default: - if (empty($storyContent['storyHTML'])) { // Skip if no storyHTML - continue 2; - } - - $html = defaultLinkTo($storyContent['storyHTML'], self::URI); - $html = str_get_html($html); - - $this->processMediaPlaceholders($html, $storyContent['id']); - $this->processHubLinks($html, $storyContent); - $this->processIframes($html); - if (!is_null($storyContent['leadPhotoId'])) { - $item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/' - . $storyContent['leadPhotoId'] . '/800.jpeg'; - } - } +class AssociatedPressNewsBridge extends BridgeAbstract +{ + const NAME = 'Associated Press News Bridge'; + const URI = 'https://apnews.com/'; + const DESCRIPTION = 'Returns newest articles by topic'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + 'Standard Topics' => [ + 'topic' => [ + 'name' => 'Topic', + 'type' => 'list', + 'values' => [ + 'AP Top News' => 'apf-topnews', + 'Sports' => 'apf-sports', + 'Entertainment' => 'apf-entertainment', + 'Oddities' => 'apf-oddities', + 'Travel' => 'apf-Travel', + 'Technology' => 'apf-technology', + 'Lifestyle' => 'apf-lifestyle', + 'Business' => 'apf-business', + 'U.S. News' => 'apf-usnews', + 'Health' => 'apf-Health', + 'Science' => 'apf-science', + 'World News' => 'apf-WorldNews', + 'Politics' => 'apf-politics', + 'Religion' => 'apf-religion', + 'Photo Galleries' => 'PhotoGalleries', + 'Fact Checks' => 'APFactCheck', + 'Videos' => 'apf-videos', + ], + 'defaultValue' => 'apf-topnews', + ], + ], + 'Custom Topic' => [ + 'topic' => [ + 'name' => 'Topic', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'europe' + ], + ] + ]; + + const CACHE_TIMEOUT = 900; // 15 mins + + private $detectParamRegex = '/^https?:\/\/(?:www\.)?apnews\.com\/(?:[tag|hub]+\/)?([\w-]+)$/'; + private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags='; + private $feedName = ''; + + public function detectParameters($url) + { + $params = []; + + if (preg_match($this->detectParamRegex, $url, $matches) > 0) { + $params['topic'] = $matches[1]; + $params['context'] = 'Custom Topic'; + return $params; + } + + return null; + } + + public function collectData() + { + switch ($this->getInput('topic')) { + case 'Podcasts': + returnClientError('Podcasts topic feed is not supported'); + break; + case 'PressReleases': + returnClientError('PressReleases topic feed is not supported'); + break; + default: + $this->collectCardData(); + } + } + + public function getURI() + { + if (!is_null($this->getInput('topic'))) { + return self::URI . $this->getInput('topic'); + } + + return parent::getURI(); + } + + public function getName() + { + if (!empty($this->feedName)) { + return $this->feedName . ' - Associated Press'; + } + + return parent::getName(); + } + + private function getTagURI() + { + if (!is_null($this->getInput('topic'))) { + return $this->tagEndpoint . $this->getInput('topic'); + } + + return parent::getURI(); + } + + private function collectCardData() + { + $json = getContents($this->getTagURI()) + or returnServerError('Could not request: ' . $this->getTagURI()); + + $tagContents = json_decode($json, true); + + if (empty($tagContents['tagObjs'])) { + returnClientError('Topic not found: ' . $this->getInput('topic')); + } + + $this->feedName = $tagContents['tagObjs'][0]['name']; + + foreach ($tagContents['cards'] as $card) { + $item = []; + + // skip hub peeks & Notifications + if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') { + continue; + } + + $storyContent = $card['contents'][0]; + + switch ($storyContent['contentType']) { + case 'web': // Skip link only content + continue 2; + + case 'video': + $html = $this->processVideo($storyContent); + + $item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/' + . $storyContent['media'][0]['id'] . '/800.jpeg'; + break; + default: + if (empty($storyContent['storyHTML'])) { // Skip if no storyHTML + continue 2; + } + + $html = defaultLinkTo($storyContent['storyHTML'], self::URI); + $html = str_get_html($html); + + $this->processMediaPlaceholders($html, $storyContent['id']); + $this->processHubLinks($html, $storyContent); + $this->processIframes($html); + + if (!is_null($storyContent['leadPhotoId'])) { + $item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/' + . $storyContent['leadPhotoId'] . '/800.jpeg'; + } + } + + $item['title'] = $card['contents'][0]['headline']; + $item['uri'] = self::URI . $card['shortId']; + + if ($card['contents'][0]['localLinkUrl']) { + $item['uri'] = $card['contents'][0]['localLinkUrl']; + } + + $item['timestamp'] = $storyContent['published']; + + if (is_null($storyContent['bylines']) === false) { + // Remove 'By' from the bylines + if (substr($storyContent['bylines'], 0, 2) == 'By') { + $item['author'] = ltrim($storyContent['bylines'], 'By '); + } else { + $item['author'] = $storyContent['bylines']; + } + } + + $item['content'] = $html; - $item['title'] = $card['contents'][0]['headline']; - $item['uri'] = self::URI . $card['shortId']; + foreach ($storyContent['tagObjs'] as $tag) { + $item['categories'][] = $tag['name']; + } - if ($card['contents'][0]['localLinkUrl']) { - $item['uri'] = $card['contents'][0]['localLinkUrl']; - } + $this->items[] = $item; - $item['timestamp'] = $storyContent['published']; + if (count($this->items) >= 15) { + break; + } + } + } - if (is_null($storyContent['bylines']) === false) { - // Remove 'By' from the bylines - if (substr($storyContent['bylines'], 0, 2) == 'By') { - $item['author'] = ltrim($storyContent['bylines'], 'By '); - } else { - $item['author'] = $storyContent['bylines']; - } - } + private function processMediaPlaceholders($html, $id) + { + if ($html->find('div.media-placeholder', 0)) { + // Fetch page content + $json = getContents('https://afs-prod.appspot.com/api/v2/content/' . $id); + $storyContent = json_decode($json, true); - $item['content'] = $html; + foreach ($html->find('div.media-placeholder') as $div) { + $key = array_search($div->id, $storyContent['mediumIds']); - foreach ($storyContent['tagObjs'] as $tag) { - $item['categories'][] = $tag['name']; - } + if (!isset($storyContent['media'][$key])) { + continue; + } - $this->items[] = $item; + $media = $storyContent['media'][$key]; - if (count($this->items) >= 15) { - break; - } - } - } + if ($media['type'] === 'Photo') { + $mediaUrl = $media['gcsBaseUrl'] . $media['imageRenderedSizes'][0] . $media['imageFileExtension']; + $mediaCaption = $media['caption']; - private function processMediaPlaceholders($html, $id) { - - if ($html->find('div.media-placeholder', 0)) { - // Fetch page content - $json = getContents('https://afs-prod.appspot.com/api/v2/content/' . $id); - $storyContent = json_decode($json, true); - - foreach ($html->find('div.media-placeholder') as $div) { - $key = array_search($div->id, $storyContent['mediumIds']); - - if (!isset($storyContent['media'][$key])) { - continue; - } - - $media = $storyContent['media'][$key]; - - if ($media['type'] === 'Photo') { - $mediaUrl = $media['gcsBaseUrl'] . $media['imageRenderedSizes'][0] . $media['imageFileExtension']; - $mediaCaption = $media['caption']; - - $div->outertext = <<<EOD + $div->outertext = <<<EOD <figure><img loading="lazy" src="{$mediaUrl}"/><figcaption>{$mediaCaption}</figcaption></figure> EOD; - } + } - if ($media['type'] === 'YouTube') { - $div->outertext = <<<EOD + if ($media['type'] === 'YouTube') { + $div->outertext = <<<EOD <iframe src="https://www.youtube.com/embed/{$media['externalId']}" width="560" height="315"> </iframe> EOD; - } - } - } - } - - /* - Create full coverage links (HubLinks) - */ - private function processHubLinks($html, $storyContent) { - - if (!empty($storyContent['richEmbeds'])) { - foreach ($storyContent['richEmbeds'] as $embed) { - - if ($embed['type'] === 'Hub Link') { - $url = self::URI . $embed['tag']['id']; - $div = $html->find('div[id=' . $embed['id'] . ']', 0); - - if ($div) { - $div->outertext = <<<EOD + } + } + } + } + + /* + Create full coverage links (HubLinks) + */ + private function processHubLinks($html, $storyContent) + { + if (!empty($storyContent['richEmbeds'])) { + foreach ($storyContent['richEmbeds'] as $embed) { + if ($embed['type'] === 'Hub Link') { + $url = self::URI . $embed['tag']['id']; + $div = $html->find('div[id=' . $embed['id'] . ']', 0); + + if ($div) { + $div->outertext = <<<EOD <p><a href="{$url}">{$embed['calloutText']} {$embed['displayName']}</a></p> EOD; - } - } - } - } - } - - private function processVideo($storyContent) { - $video = $storyContent['media'][0]; - - if ($video['type'] === 'YouTube') { - $url = 'https://www.youtube.com/embed/' . $video['externalId']; - $html = <<<EOD + } + } + } + } + } + + private function processVideo($storyContent) + { + $video = $storyContent['media'][0]; + + if ($video['type'] === 'YouTube') { + $url = 'https://www.youtube.com/embed/' . $video['externalId']; + $html = <<<EOD <iframe width="560" height="315" src="{$url}" frameborder="0" allowfullscreen></iframe> EOD; - } else { - $html = <<<EOD + } else { + $html = <<<EOD <video controls poster="https://storage.googleapis.com/afs-prod/media/{$video['id']}/800.jpeg" preload="none"> <source src="{$video['gcsBaseUrl']} {$video['videoRenderedSizes'][0]} {$video['videoFileExtension']}" type="video/mp4"> </video> EOD; - } - - return $html; - } - - // Remove datawrapper.dwcdn.net iframes and related javaScript - private function processIframes($html) { - - foreach ($html->find('iframe') as $index => $iframe) { - if (preg_match('/datawrapper\.dwcdn\.net/', $iframe->src)) { - $iframe->outertext = ''; - - if ($html->find('script', $index)) { - $html->find('script', $index)->outertext = ''; - } - } - } - } + } + + return $html; + } + + // Remove datawrapper.dwcdn.net iframes and related javaScript + private function processIframes($html) + { + foreach ($html->find('iframe') as $index => $iframe) { + if (preg_match('/datawrapper\.dwcdn\.net/', $iframe->src)) { + $iframe->outertext = ''; + + if ($html->find('script', $index)) { + $html->find('script', $index)->outertext = ''; + } + } + } + } } diff --git a/bridges/AstrophysicsDataSystemBridge.php b/bridges/AstrophysicsDataSystemBridge.php index b287b52b..e971cad4 100644 --- a/bridges/AstrophysicsDataSystemBridge.php +++ b/bridges/AstrophysicsDataSystemBridge.php @@ -1,48 +1,53 @@ <?php -class AstrophysicsDataSystemBridge extends BridgeAbstract { - const NAME = 'SAO/NASA Astrophysics Data System'; - const DESCRIPTION = 'Returns the latest publications from a query'; - const URI = 'https://ui.adsabs.harvard.edu'; - const PARAMETERS = array( - 'Publications' => array( - 'query' => array( - 'name' => 'query', - 'title' => 'Same format as the search bar on the website', - 'exampleValue' => 'author:"huchra, john"', - 'required' => true - ) - )); - private $feedTitle; +class AstrophysicsDataSystemBridge extends BridgeAbstract +{ + const NAME = 'SAO/NASA Astrophysics Data System'; + const DESCRIPTION = 'Returns the latest publications from a query'; + const URI = 'https://ui.adsabs.harvard.edu'; + const PARAMETERS = [ + 'Publications' => [ + 'query' => [ + 'name' => 'query', + 'title' => 'Same format as the search bar on the website', + 'exampleValue' => 'author:"huchra, john"', + 'required' => true + ] + ]]; - public function getName() { - if ($this->queriedContext === 'Publications') { - return $this->feedTitle; - } - return parent::getName(); - } + private $feedTitle; - public function getURI() { - if ($this->queriedContext === 'Publications') { - return self::URI . '/search/?q=' . urlencode($this->getInput('query')); - } - return parent::getURI(); - } + public function getName() + { + if ($this->queriedContext === 'Publications') { + return $this->feedTitle; + } + return parent::getName(); + } - public function collectData() { - $headers = array ( - 'Cookie: core=always;' - ); - $html = str_get_html(defaultLinkTo(getContents($this->getURI(), $headers), self::URI)); - $this->feedTitle = html_entity_decode($html->find('title', 0)->plaintext); - foreach($html->find('div.row > ul > li') as $pub) { - $item = array(); - $item['title'] = $pub->find('h3.s-results-title', 0)->plaintext; - $item['content'] = $pub->find('div.s-results-links', 0); - $item['uri'] = $pub->find('a.abs-redirect-link', 0)->href; - $item['author'] = rtrim($pub->find('li.article-author', 0)->plaintext, ' ;'); - $item['timestamp'] = $pub->find('div[aria-label="date published"]', 0)->plaintext; - $this->items[] = $item; - } - } + public function getURI() + { + if ($this->queriedContext === 'Publications') { + return self::URI . '/search/?q=' . urlencode($this->getInput('query')); + } + return parent::getURI(); + } + + public function collectData() + { + $headers = [ + 'Cookie: core=always;' + ]; + $html = str_get_html(defaultLinkTo(getContents($this->getURI(), $headers), self::URI)); + $this->feedTitle = html_entity_decode($html->find('title', 0)->plaintext); + foreach ($html->find('div.row > ul > li') as $pub) { + $item = []; + $item['title'] = $pub->find('h3.s-results-title', 0)->plaintext; + $item['content'] = $pub->find('div.s-results-links', 0); + $item['uri'] = $pub->find('a.abs-redirect-link', 0)->href; + $item['author'] = rtrim($pub->find('li.article-author', 0)->plaintext, ' ;'); + $item['timestamp'] = $pub->find('div[aria-label="date published"]', 0)->plaintext; + $this->items[] = $item; + } + } } diff --git a/bridges/AtmoNouvelleAquitaineBridge.php b/bridges/AtmoNouvelleAquitaineBridge.php index f84d1120..d4244fa9 100644 --- a/bridges/AtmoNouvelleAquitaineBridge.php +++ b/bridges/AtmoNouvelleAquitaineBridge.php @@ -1,4637 +1,4650 @@ <?php -class AtmoNouvelleAquitaineBridge extends BridgeAbstract { - const NAME = 'Atmo Nouvelle Aquitaine'; - const URI = 'https://www.atmo-nouvelleaquitaine.org'; - const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo'; - const MAINTAINER = 'floviolleau'; - const PARAMETERS = array(array( - 'cities' => array( - 'name' => 'Choisir une ville', - 'type' => 'list', - 'values' => self::CITIES - ) - )); - const CACHE_TIMEOUT = 7200; +class AtmoNouvelleAquitaineBridge extends BridgeAbstract +{ + const NAME = 'Atmo Nouvelle Aquitaine'; + const URI = 'https://www.atmo-nouvelleaquitaine.org'; + const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo'; + const MAINTAINER = 'floviolleau'; + const PARAMETERS = [[ + 'cities' => [ + 'name' => 'Choisir une ville', + 'type' => 'list', + 'values' => self::CITIES + ] + ]]; + const CACHE_TIMEOUT = 7200; - private $dom; + private $dom; - private function getClosest($search, $arr) { - $closest = null; - foreach ($arr as $key => $value) { - if ($closest === null || abs((int)$search - $closest) > abs((int)$key - (int)$search)) { - $closest = (int)$key; - } - } - return $arr[$closest]; - } + private function getClosest($search, $arr) + { + $closest = null; + foreach ($arr as $key => $value) { + if ($closest === null || abs((int)$search - $closest) > abs((int)$key - (int)$search)) { + $closest = (int)$key; + } + } + return $arr[$closest]; + } - public function collectData() { - $uri = self::URI . '/monair/commune/' . $this->getInput('cities'); + public function collectData() + { + $uri = self::URI . '/monair/commune/' . $this->getInput('cities'); - $html = getSimpleHTMLDOM($uri); + $html = getSimpleHTMLDOM($uri); - $this->dom = $html->find('#block-system-main .city-prevision-map', 0); + $this->dom = $html->find('#block-system-main .city-prevision-map', 0); - $message = $this->getIndexMessage() . ' ' . $this->getQualityMessage(); - $message .= ' ' . $this->getTomorrowTrendIndexMessage() . ' ' . $this->getTomorrowTrendQualityMessage(); + $message = $this->getIndexMessage() . ' ' . $this->getQualityMessage(); + $message .= ' ' . $this->getTomorrowTrendIndexMessage() . ' ' . $this->getTomorrowTrendQualityMessage(); - $item['uri'] = $uri; - $today = date('d/m/Y'); - $item['title'] = "Bulletin de l'air du $today pour la région Nouvelle Aquitaine."; - $item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-nouvelleaquitaine.org #QualiteAir.'; - $item['author'] = 'floviolleau'; - $item['content'] = $message; - $item['uid'] = hash('sha256', $item['title']); + $item['uri'] = $uri; + $today = date('d/m/Y'); + $item['title'] = "Bulletin de l'air du $today pour la région Nouvelle Aquitaine."; + $item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-nouvelleaquitaine.org #QualiteAir.'; + $item['author'] = 'floviolleau'; + $item['content'] = $message; + $item['uid'] = hash('sha256', $item['title']); - $this->items[] = $item; - } + $this->items[] = $item; + } - private function getIndex() { - $index = $this->dom->find('.indice', 0)->innertext; + private function getIndex() + { + $index = $this->dom->find('.indice', 0)->innertext; - if ($index == 'XX') { - return -1; - } + if ($index == 'XX') { + return -1; + } - return $index; - } + return $index; + } - private function getMaxIndexText() { - // will return '/100' - return $this->dom->find('.pourcent', 0)->innertext; - } + private function getMaxIndexText() + { + // will return '/100' + return $this->dom->find('.pourcent', 0)->innertext; + } - private function getQualityText($index, $indexes) { - if ($index == -1) { - if (array_key_exists('no-available', $indexes)) { - return $indexes['no-available']; - } + private function getQualityText($index, $indexes) + { + if ($index == -1) { + if (array_key_exists('no-available', $indexes)) { + return $indexes['no-available']; + } - return 'Aucune donnée'; - } + return 'Aucune donnée'; + } - return $this->getClosest($index, $indexes); - } + return $this->getClosest($index, $indexes); + } - private function getLegendIndexes() { - $rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label'); - $indexes = array(); - for ($i = 0; $i < count($rawIndexes); $i++) { - if ($rawIndexes[$i]->hasAttribute('data-color')) { - $indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext; - } - } + private function getLegendIndexes() + { + $rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label'); + $indexes = []; + for ($i = 0; $i < count($rawIndexes); $i++) { + if ($rawIndexes[$i]->hasAttribute('data-color')) { + $indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext; + } + } - return $indexes; - } + return $indexes; + } - private function getTomorrowTrendIndex() { - $tomorrowTrendDomNode = $this->dom - ->find('.day-controls.raster-controls .list-raster-controls .raster-control', 2); - $tomorrowTrendIndexNode = null; + private function getTomorrowTrendIndex() + { + $tomorrowTrendDomNode = $this->dom + ->find('.day-controls.raster-controls .list-raster-controls .raster-control', 2); + $tomorrowTrendIndexNode = null; - if ($tomorrowTrendDomNode) { - $tomorrowTrendIndexNode = $tomorrowTrendDomNode->find('.raster-control-link', 0); - } + if ($tomorrowTrendDomNode) { + $tomorrowTrendIndexNode = $tomorrowTrendDomNode->find('.raster-control-link', 0); + } - if ($tomorrowTrendIndexNode && $tomorrowTrendIndexNode->hasAttribute('data-index')) { - $tomorrowTrendIndex = $tomorrowTrendIndexNode->getAttribute('data-index'); - } else { - return -1; - } + if ($tomorrowTrendIndexNode && $tomorrowTrendIndexNode->hasAttribute('data-index')) { + $tomorrowTrendIndex = $tomorrowTrendIndexNode->getAttribute('data-index'); + } else { + return -1; + } - return $tomorrowTrendIndex; - } + return $tomorrowTrendIndex; + } - private function getTomorrowTrendQualityText($trendIndex, $indexes) { - if ($trendIndex == -1) { - if (array_key_exists('no-available', $indexes)) { - return $indexes['no-available']; - } + private function getTomorrowTrendQualityText($trendIndex, $indexes) + { + if ($trendIndex == -1) { + if (array_key_exists('no-available', $indexes)) { + return $indexes['no-available']; + } - return 'Aucune donnée'; - } + return 'Aucune donnée'; + } - return $this->getClosest($trendIndex, $indexes); - } + return $this->getClosest($trendIndex, $indexes); + } - private function getIndexMessage() { - $index = $this->getIndex(); - $maxIndexText = $this->getMaxIndexText(); + private function getIndexMessage() + { + $index = $this->getIndex(); + $maxIndexText = $this->getMaxIndexText(); - if ($index == -1) { - return 'Aucune donnée pour l\'indice.'; - } + if ($index == -1) { + return 'Aucune donnée pour l\'indice.'; + } - return "L'indice d'aujourd'hui est $index$maxIndexText."; - } + return "L'indice d'aujourd'hui est $index$maxIndexText."; + } - private function getQualityMessage() { - $index = $index = $this->getIndex(); - $indexes = $this->getLegendIndexes(); - $quality = $this->getQualityText($index, $indexes); + private function getQualityMessage() + { + $index = $index = $this->getIndex(); + $indexes = $this->getLegendIndexes(); + $quality = $this->getQualityText($index, $indexes); - if ($index == -1) { - return 'Aucune donnée pour la qualité de l\'air.'; - } + if ($index == -1) { + return 'Aucune donnée pour la qualité de l\'air.'; + } - return "La qualité de l'air est $quality."; - } + return "La qualité de l'air est $quality."; + } - private function getTomorrowTrendIndexMessage() { - $trendIndex = $this->getTomorrowTrendIndex(); - $maxIndexText = $this->getMaxIndexText(); + private function getTomorrowTrendIndexMessage() + { + $trendIndex = $this->getTomorrowTrendIndex(); + $maxIndexText = $this->getMaxIndexText(); - if ($trendIndex == -1) { - return 'Aucune donnée pour l\'indice prévu demain.'; - } + if ($trendIndex == -1) { + return 'Aucune donnée pour l\'indice prévu demain.'; + } - return "L'indice prévu pour demain est $trendIndex$maxIndexText."; - } + return "L'indice prévu pour demain est $trendIndex$maxIndexText."; + } - private function getTomorrowTrendQualityMessage() { - $trendIndex = $this->getTomorrowTrendIndex(); - $indexes = $this->getLegendIndexes(); - $trendQuality = $this->getTomorrowTrendQualityText($trendIndex, $indexes); + private function getTomorrowTrendQualityMessage() + { + $trendIndex = $this->getTomorrowTrendIndex(); + $indexes = $this->getLegendIndexes(); + $trendQuality = $this->getTomorrowTrendQualityText($trendIndex, $indexes); - if ($trendIndex == -1) { - return 'Aucune donnée pour la qualité de l\'air de demain.'; - } - return "La qualite de l'air pour demain sera $trendQuality."; - } + if ($trendIndex == -1) { + return 'Aucune donnée pour la qualité de l\'air de demain.'; + } + return "La qualite de l'air pour demain sera $trendQuality."; + } - const CITIES = array( - 'Aast (64460)' => '64001', - 'Abère (64160)' => '64002', - 'Abidos (64150)' => '64003', - 'Abitain (64390)' => '64004', - 'Abjat-sur-Bandiat (24300)' => '24001', - 'Abos (64360)' => '64005', - 'Abzac (16500)' => '16001', - 'Abzac (33230)' => '33001', - 'Accous (64490)' => '64006', - 'Adilly (79200)' => '79002', - 'Adriers (86430)' => '86001', - 'Affieux (19260)' => '19001', - 'Agen (47000)' => '47001', - 'Agmé (47350)' => '47002', - 'Agnac (47800)' => '47003', - 'Agnos (64400)' => '64007', - 'Agonac (24460)' => '24002', - 'Agris (16110)' => '16003', - 'Agudelle (17500)' => '17002', - 'Ahaxe-Alciette-Bascassan (64220)' => '64008', - 'Ahetze (64210)' => '64009', - 'Ahun (23150)' => '23001', - 'Aïcirits-Camou-Suhast (64120)' => '64010', - 'Aiffres (79230)' => '79003', - 'Aignes-et-Puypéroux (16190)' => '16004', - 'Aigonnay (79370)' => '79004', - 'Aigre (16140)' => '16005', - 'Aigrefeuille-d\'Aunis (17290)' => '17003', - 'Aiguillon (47190)' => '47004', - 'Aillas (33124)' => '33002', - 'Aincille (64220)' => '64011', - 'Ainharp (64130)' => '64012', - 'Ainhice-Mongelos (64220)' => '64013', - 'Ainhoa (64250)' => '64014', - 'Aire-sur-l\'Adour (40800)' => '40001', - 'Airvault (79600)' => '79005', - 'Aix (19200)' => '19002', - 'Aixe-sur-Vienne (87700)' => '87001', - 'Ajain (23380)' => '23002', - 'Ajat (24210)' => '24004', - 'Albignac (19190)' => '19003', - 'Albussac (19380)' => '19004', - 'Alçay-Alçabéhéty-Sunharette (64470)' => '64015', - 'Aldudes (64430)' => '64016', - 'Allas-Bocage (17150)' => '17005', - 'Allas-Champagne (17500)' => '17006', - 'Allas-les-Mines (24220)' => '24006', - 'Allassac (19240)' => '19005', - 'Allemans (24600)' => '24007', - 'Allemans-du-Dropt (47800)' => '47005', - 'Alles-sur-Dordogne (24480)' => '24005', - 'Alleyrat (19200)' => '19006', - 'Alleyrat (23200)' => '23003', - 'Allez-et-Cazeneuve (47110)' => '47006', - 'Allonne (79130)' => '79007', - 'Allons (47420)' => '47007', - 'Alloue (16490)' => '16007', - 'Alos-Sibas-Abense (64470)' => '64017', - 'Altillac (19120)' => '19007', - 'Amailloux (79350)' => '79008', - 'Ambarès-et-Lagrave (33440)' => '33003', - 'Ambazac (87240)' => '87002', - 'Ambérac (16140)' => '16008', - 'Ambernac (16490)' => '16009', - 'Amberre (86110)' => '86002', - 'Ambès (33810)' => '33004', - 'Ambleville (16300)' => '16010', - 'Ambrugeat (19250)' => '19008', - 'Ambrus (47160)' => '47008', - 'Amendeuix-Oneix (64120)' => '64018', - 'Amorots-Succos (64120)' => '64019', - 'Amou (40330)' => '40002', - 'Amuré (79210)' => '79009', - 'Anais (16560)' => '16011', - 'Anais (17540)' => '17007', - 'Ance (64570)' => '64020', - 'Anché (86700)' => '86003', - 'Andernos-les-Bains (33510)' => '33005', - 'Andilly (17230)' => '17008', - 'Andiran (47170)' => '47009', - 'Andoins (64420)' => '64021', - 'Andrein (64390)' => '64022', - 'Angaïs (64510)' => '64023', - 'Angeac-Champagne (16130)' => '16012', - 'Angeac-Charente (16120)' => '16013', - 'Angeduc (16300)' => '16014', - 'Anglade (33390)' => '33006', - 'Angles-sur-l\'Anglin (86260)' => '86004', - 'Anglet (64600)' => '64024', - 'Angliers (17540)' => '17009', - 'Angliers (86330)' => '86005', - 'Angoisse (24270)' => '24008', - 'Angoulême (16000)' => '16015', - 'Angoulins (17690)' => '17010', - 'Angoumé (40990)' => '40003', - 'Angous (64190)' => '64025', - 'Angresse (40150)' => '40004', - 'Anhaux (64220)' => '64026', - 'Anlhiac (24160)' => '24009', - 'Annepont (17350)' => '17011', - 'Annesse-et-Beaulieu (24430)' => '24010', - 'Annezay (17380)' => '17012', - 'Anos (64160)' => '64027', - 'Anoye (64350)' => '64028', - 'Ansac-sur-Vienne (16500)' => '16016', - 'Antagnac (47700)' => '47010', - 'Antezant-la-Chapelle (17400)' => '17013', - 'Anthé (47370)' => '47011', - 'Antigny (86310)' => '86006', - 'Antonne-et-Trigonant (24420)' => '24011', - 'Antran (86100)' => '86007', - 'Anville (16170)' => '16017', - 'Anzême (23000)' => '23004', - 'Anzex (47700)' => '47012', - 'Aramits (64570)' => '64029', - 'Arancou (64270)' => '64031', - 'Araujuzon (64190)' => '64032', - 'Araux (64190)' => '64033', - 'Arbanats (33640)' => '33007', - 'Arbérats-Sillègue (64120)' => '64034', - 'Arbis (33760)' => '33008', - 'Arbonne (64210)' => '64035', - 'Arboucave (40320)' => '40005', - 'Arbouet-Sussaute (64120)' => '64036', - 'Arbus (64230)' => '64037', - 'Arcachon (33120)' => '33009', - 'Arçais (79210)' => '79010', - 'Arcangues (64200)' => '64038', - 'Arçay (86200)' => '86008', - 'Arces (17120)' => '17015', - 'Archiac (17520)' => '17016', - 'Archignac (24590)' => '24012', - 'Archigny (86210)' => '86009', - 'Archingeay (17380)' => '17017', - 'Arcins (33460)' => '33010', - 'Ardilleux (79110)' => '79011', - 'Ardillières (17290)' => '17018', - 'Ardin (79160)' => '79012', - 'Aren (64400)' => '64039', - 'Arengosse (40110)' => '40006', - 'Arès (33740)' => '33011', - 'Aressy (64320)' => '64041', - 'Arette (64570)' => '64040', - 'Arfeuille-Châtain (23700)' => '23005', - 'Argagnon (64300)' => '64042', - 'Argelos (40700)' => '40007', - 'Argelos (64450)' => '64043', - 'Argelouse (40430)' => '40008', - 'Argentat (19400)' => '19010', - 'Argenton (47250)' => '47013', - 'Argenton-l\'Église (79290)' => '79014', - 'Argentonnay (79150)' => '79013', - 'Arget (64410)' => '64044', - 'Arhansus (64120)' => '64045', - 'Arjuzanx (40110)' => '40009', - 'Armendarits (64640)' => '64046', - 'Armillac (47800)' => '47014', - 'Arnac-la-Poste (87160)' => '87003', - 'Arnac-Pompadour (19230)' => '19011', - 'Arnéguy (64220)' => '64047', - 'Arnos (64370)' => '64048', - 'Aroue-Ithorots-Olhaïby (64120)' => '64049', - 'Arrast-Larrebieu (64130)' => '64050', - 'Arraute-Charritte (64120)' => '64051', - 'Arrènes (23210)' => '23006', - 'Arricau-Bordes (64350)' => '64052', - 'Arrien (64420)' => '64053', - 'Arros-de-Nay (64800)' => '64054', - 'Arrosès (64350)' => '64056', - 'Ars (16130)' => '16018', - 'Ars (23480)' => '23007', - 'Ars-en-Ré (17590)' => '17019', - 'Arsac (33460)' => '33012', - 'Arsague (40330)' => '40011', - 'Artassenx (40090)' => '40012', - 'Arthenac (17520)' => '17020', - 'Arthez-d\'Armagnac (40190)' => '40013', - 'Arthez-d\'Asson (64800)' => '64058', - 'Arthez-de-Béarn (64370)' => '64057', - 'Artigueloutan (64420)' => '64059', - 'Artiguelouve (64230)' => '64060', - 'Artigues-près-Bordeaux (33370)' => '33013', - 'Artix (64170)' => '64061', - 'Arudy (64260)' => '64062', - 'Arue (40120)' => '40014', - 'Arvert (17530)' => '17021', - 'Arveyres (33500)' => '33015', - 'Arx (40310)' => '40015', - 'Arzacq-Arraziguet (64410)' => '64063', - 'Asasp-Arros (64660)' => '64064', - 'Ascain (64310)' => '64065', - 'Ascarat (64220)' => '64066', - 'Aslonnes (86340)' => '86010', - 'Asnières-en-Poitou (79170)' => '79015', - 'Asnières-la-Giraud (17400)' => '17022', - 'Asnières-sur-Blour (86430)' => '86011', - 'Asnières-sur-Nouère (16290)' => '16019', - 'Asnois (86250)' => '86012', - 'Asques (33240)' => '33016', - 'Assais-les-Jumeaux (79600)' => '79016', - 'Assat (64510)' => '64067', - 'Asson (64800)' => '64068', - 'Astaffort (47220)' => '47015', - 'Astaillac (19120)' => '19012', - 'Aste-Béon (64260)' => '64069', - 'Astis (64450)' => '64070', - 'Athos-Aspis (64390)' => '64071', - 'Aubagnan (40700)' => '40016', - 'Aubas (24290)' => '24014', - 'Aubazines (19190)' => '19013', - 'Aubertin (64290)' => '64072', - 'Aubeterre-sur-Dronne (16390)' => '16020', - 'Aubiac (33430)' => '33017', - 'Aubiac (47310)' => '47016', - 'Aubigné (79110)' => '79018', - 'Aubigny (79390)' => '79019', - 'Aubin (64230)' => '64073', - 'Aubous (64330)' => '64074', - 'Aubusson (23200)' => '23008', - 'Audaux (64190)' => '64075', - 'Audenge (33980)' => '33019', - 'Audignon (40500)' => '40017', - 'Audon (40400)' => '40018', - 'Audrix (24260)' => '24015', - 'Auga (64450)' => '64077', - 'Auge (23170)' => '23009', - 'Augé (79400)' => '79020', - 'Auge-Saint-Médard (16170)' => '16339', - 'Augères (23210)' => '23010', - 'Augignac (24300)' => '24016', - 'Augne (87120)' => '87004', - 'Aujac (17770)' => '17023', - 'Aulnay (17470)' => '17024', - 'Aulnay (86330)' => '86013', - 'Aulon (23210)' => '23011', - 'Aumagne (17770)' => '17025', - 'Aunac (16460)' => '16023', - 'Auradou (47140)' => '47017', - 'Aureil (87220)' => '87005', - 'Aureilhan (40200)' => '40019', - 'Auriac (19220)' => '19014', - 'Auriac (64450)' => '64078', - 'Auriac-du-Périgord (24290)' => '24018', - 'Auriac-sur-Dropt (47120)' => '47018', - 'Auriat (23400)' => '23012', - 'Aurice (40500)' => '40020', - 'Auriolles (33790)' => '33020', - 'Aurions-Idernes (64350)' => '64079', - 'Auros (33124)' => '33021', - 'Aussac-Vadalle (16560)' => '16024', - 'Aussevielle (64230)' => '64080', - 'Aussurucq (64130)' => '64081', - 'Auterrive (64270)' => '64082', - 'Autevielle-Saint-Martin-Bideren (64390)' => '64083', - 'Authon-Ébéon (17770)' => '17026', - 'Auzances (23700)' => '23013', - 'Availles-en-Châtellerault (86530)' => '86014', - 'Availles-Limouzine (86460)' => '86015', - 'Availles-Thouarsais (79600)' => '79022', - 'Avanton (86170)' => '86016', - 'Avensan (33480)' => '33022', - 'Avon (79800)' => '79023', - 'Avy (17800)' => '17027', - 'Aydie (64330)' => '64084', - 'Aydius (64490)' => '64085', - 'Ayen (19310)' => '19015', - 'Ayguemorte-les-Graves (33640)' => '33023', - 'Ayherre (64240)' => '64086', - 'Ayron (86190)' => '86017', - 'Aytré (17440)' => '17028', - 'Azat-Châtenet (23210)' => '23014', - 'Azat-le-Ris (87360)' => '87006', - 'Azay-le-Brûlé (79400)' => '79024', - 'Azay-sur-Thouet (79130)' => '79025', - 'Azerables (23160)' => '23015', - 'Azerat (24210)' => '24019', - 'Azur (40140)' => '40021', - 'Badefols-d\'Ans (24390)' => '24021', - 'Badefols-sur-Dordogne (24150)' => '24022', - 'Bagas (33190)' => '33024', - 'Bagnizeau (17160)' => '17029', - 'Bahus-Soubiran (40320)' => '40022', - 'Baigneaux (33760)' => '33025', - 'Baignes-Sainte-Radegonde (16360)' => '16025', - 'Baigts (40380)' => '40023', - 'Baigts-de-Béarn (64300)' => '64087', - 'Bajamont (47480)' => '47019', - 'Balansun (64300)' => '64088', - 'Balanzac (17600)' => '17030', - 'Baleix (64460)' => '64089', - 'Baleyssagues (47120)' => '47020', - 'Baliracq-Maumusson (64330)' => '64090', - 'Baliros (64510)' => '64091', - 'Balizac (33730)' => '33026', - 'Ballans (17160)' => '17031', - 'Balledent (87290)' => '87007', - 'Ballon (17290)' => '17032', - 'Balzac (16430)' => '16026', - 'Banca (64430)' => '64092', - 'Baneuil (24150)' => '24023', - 'Banize (23120)' => '23016', - 'Banos (40500)' => '40024', - 'Bar (19800)' => '19016', - 'Barbaste (47230)' => '47021', - 'Barbezières (16140)' => '16027', - 'Barbezieux-Saint-Hilaire (16300)' => '16028', - 'Barcus (64130)' => '64093', - 'Bardenac (16210)' => '16029', - 'Bardos (64520)' => '64094', - 'Bardou (24560)' => '24024', - 'Barie (33190)' => '33027', - 'Barinque (64160)' => '64095', - 'Baron (33750)' => '33028', - 'Barraute-Camu (64390)' => '64096', - 'Barret (16300)' => '16030', - 'Barro (16700)' => '16031', - 'Bars (24210)' => '24025', - 'Barsac (33720)' => '33030', - 'Barzan (17120)' => '17034', - 'Barzun (64530)' => '64097', - 'Bas-Mauco (40500)' => '40026', - 'Bascons (40090)' => '40025', - 'Bassac (16120)' => '16032', - 'Bassanne (33190)' => '33031', - 'Bassens (33530)' => '33032', - 'Bassercles (40700)' => '40027', - 'Basses (86200)' => '86018', - 'Bassignac-le-Bas (19430)' => '19017', - 'Bassignac-le-Haut (19220)' => '19018', - 'Bassillac (24330)' => '24026', - 'Bassillon-Vauzé (64350)' => '64098', - 'Bassussarry (64200)' => '64100', - 'Bastanès (64190)' => '64099', - 'Bastennes (40360)' => '40028', - 'Basville (23260)' => '23017', - 'Bats (40320)' => '40029', - 'Baudignan (40310)' => '40030', - 'Baudreix (64800)' => '64101', - 'Baurech (33880)' => '33033', - 'Bayac (24150)' => '24027', - 'Bayas (33230)' => '33034', - 'Bayers (16460)' => '16033', - 'Bayon-sur-Gironde (33710)' => '33035', - 'Bayonne (64100)' => '64102', - 'Bazac (16210)' => '16034', - 'Bazas (33430)' => '33036', - 'Bazauges (17490)' => '17035', - 'Bazelat (23160)' => '23018', - 'Bazens (47130)' => '47022', - 'Beaugas (47290)' => '47023', - 'Beaugeay (17620)' => '17036', - 'Beaulieu-sous-Parthenay (79420)' => '79029', - 'Beaulieu-sur-Dordogne (19120)' => '19019', - 'Beaulieu-sur-Sonnette (16450)' => '16035', - 'Beaumont (19390)' => '19020', - 'Beaumont (86490)' => '86019', - 'Beaumont-du-Lac (87120)' => '87009', - 'Beaumontois en Périgord (24440)' => '24028', - 'Beaupouyet (24400)' => '24029', - 'Beaupuy (47200)' => '47024', - 'Beauregard-de-Terrasson (24120)' => '24030', - 'Beauregard-et-Bassac (24140)' => '24031', - 'Beauronne (24400)' => '24032', - 'Beaussac (24340)' => '24033', - 'Beaussais-Vitré (79370)' => '79030', - 'Beautiran (33640)' => '33037', - 'Beauvais-sur-Matha (17490)' => '17037', - 'Beauville (47470)' => '47025', - 'Beauvoir-sur-Niort (79360)' => '79031', - 'Beauziac (47700)' => '47026', - 'Béceleuf (79160)' => '79032', - 'Bécheresse (16250)' => '16036', - 'Bédeille (64460)' => '64103', - 'Bedenac (17210)' => '17038', - 'Bedous (64490)' => '64104', - 'Bégaar (40400)' => '40031', - 'Bégadan (33340)' => '33038', - 'Bègles (33130)' => '33039', - 'Béguey (33410)' => '33040', - 'Béguios (64120)' => '64105', - 'Béhasque-Lapiste (64120)' => '64106', - 'Béhorléguy (64220)' => '64107', - 'Beissat (23260)' => '23019', - 'Beleymas (24140)' => '24034', - 'Belhade (40410)' => '40032', - 'Belin-Béliet (33830)' => '33042', - 'Bélis (40120)' => '40033', - 'Bellac (87300)' => '87011', - 'Bellebat (33760)' => '33043', - 'Bellechassagne (19290)' => '19021', - 'Bellefond (33760)' => '33044', - 'Bellefonds (86210)' => '86020', - 'Bellegarde-en-Marche (23190)' => '23020', - 'Belleville (79360)' => '79033', - 'Bellocq (64270)' => '64108', - 'Bellon (16210)' => '16037', - 'Belluire (17800)' => '17039', - 'Bélus (40300)' => '40034', - 'Belvès-de-Castillon (33350)' => '33045', - 'Benassay (86470)' => '86021', - 'Benayes (19510)' => '19022', - 'Bénéjacq (64800)' => '64109', - 'Bénesse-lès-Dax (40180)' => '40035', - 'Bénesse-Maremne (40230)' => '40036', - 'Benest (16350)' => '16038', - 'Bénévent-l\'Abbaye (23210)' => '23021', - 'Benon (17170)' => '17041', - 'Benquet (40280)' => '40037', - 'Bentayou-Sérée (64460)' => '64111', - 'Béost (64440)' => '64110', - 'Berbiguières (24220)' => '24036', - 'Bercloux (17770)' => '17042', - 'Bérenx (64300)' => '64112', - 'Bergerac (24100)' => '24037', - 'Bergouey (40250)' => '40038', - 'Bergouey-Viellenave (64270)' => '64113', - 'Bernac (16700)' => '16039', - 'Bernadets (64160)' => '64114', - 'Bernay-Saint-Martin (17330)' => '17043', - 'Berneuil (16480)' => '16040', - 'Berneuil (17460)' => '17044', - 'Berneuil (87300)' => '87012', - 'Bernos-Beaulac (33430)' => '33046', - 'Berrie (86120)' => '86022', - 'Berrogain-Laruns (64130)' => '64115', - 'Bersac-sur-Rivalier (87370)' => '87013', - 'Berson (33390)' => '33047', - 'Berthegon (86420)' => '86023', - 'Berthez (33124)' => '33048', - 'Bertric-Burée (24320)' => '24038', - 'Béruges (86190)' => '86024', - 'Bescat (64260)' => '64116', - 'Bésingrand (64150)' => '64117', - 'Bessac (16250)' => '16041', - 'Bessé (16140)' => '16042', - 'Besse (24550)' => '24039', - 'Bessines (79000)' => '79034', - 'Bessines-sur-Gartempe (87250)' => '87014', - 'Betbezer-d\'Armagnac (40240)' => '40039', - 'Bétête (23270)' => '23022', - 'Béthines (86310)' => '86025', - 'Bétracq (64350)' => '64118', - 'Beurlay (17250)' => '17045', - 'Beuste (64800)' => '64119', - 'Beuxes (86120)' => '86026', - 'Beychac-et-Caillau (33750)' => '33049', - 'Beylongue (40370)' => '40040', - 'Beynac (87700)' => '87015', - 'Beynac-et-Cazenac (24220)' => '24040', - 'Beynat (19190)' => '19023', - 'Beyrie-en-Béarn (64230)' => '64121', - 'Beyrie-sur-Joyeuse (64120)' => '64120', - 'Beyries (40700)' => '40041', - 'Beyssac (19230)' => '19024', - 'Beyssenac (19230)' => '19025', - 'Bézenac (24220)' => '24041', - 'Biard (86580)' => '86027', - 'Biarritz (64200)' => '64122', - 'Biarrotte (40390)' => '40042', - 'Bias (40170)' => '40043', - 'Bias (47300)' => '47027', - 'Biaudos (40390)' => '40044', - 'Bidache (64520)' => '64123', - 'Bidarray (64780)' => '64124', - 'Bidart (64210)' => '64125', - 'Bidos (64400)' => '64126', - 'Bielle (64260)' => '64127', - 'Bieujac (33210)' => '33050', - 'Biganos (33380)' => '33051', - 'Bignay (17400)' => '17046', - 'Bignoux (86800)' => '86028', - 'Bilhac (19120)' => '19026', - 'Bilhères (64260)' => '64128', - 'Billère (64140)' => '64129', - 'Bioussac (16700)' => '16044', - 'Birac (16120)' => '16045', - 'Birac (33430)' => '33053', - 'Birac-sur-Trec (47200)' => '47028', - 'Biras (24310)' => '24042', - 'Biriatou (64700)' => '64130', - 'Biron (17800)' => '17047', - 'Biron (24540)' => '24043', - 'Biron (64300)' => '64131', - 'Biscarrosse (40600)' => '40046', - 'Bizanos (64320)' => '64132', - 'Blaignac (33190)' => '33054', - 'Blaignan (33340)' => '33055', - 'Blanquefort (33290)' => '33056', - 'Blanquefort-sur-Briolance (47500)' => '47029', - 'Blanzac (87300)' => '87017', - 'Blanzac-lès-Matha (17160)' => '17048', - 'Blanzac-Porcheresse (16250)' => '16046', - 'Blanzaguet-Saint-Cybard (16320)' => '16047', - 'Blanzay (86400)' => '86029', - 'Blanzay-sur-Boutonne (17470)' => '17049', - 'Blasimon (33540)' => '33057', - 'Blaslay (86170)' => '86030', - 'Blaudeix (23140)' => '23023', - 'Blaye (33390)' => '33058', - 'Blaymont (47470)' => '47030', - 'Blésignac (33670)' => '33059', - 'Blessac (23200)' => '23024', - 'Blis-et-Born (24330)' => '24044', - 'Blond (87300)' => '87018', - 'Boé (47550)' => '47031', - 'Boeil-Bezing (64510)' => '64133', - 'Bois (17240)' => '17050', - 'Boisbreteau (16480)' => '16048', - 'Boismé (79300)' => '79038', - 'Boisné-La Tude (16320)' => '16082', - 'Boisredon (17150)' => '17052', - 'Boisse (24560)' => '24045', - 'Boisserolles (79360)' => '79039', - 'Boisseuil (87220)' => '87019', - 'Boisseuilh (24390)' => '24046', - 'Bommes (33210)' => '33060', - 'Bon-Encontre (47240)' => '47032', - 'Bonloc (64240)' => '64134', - 'Bonnac-la-Côte (87270)' => '87020', - 'Bonnat (23220)' => '23025', - 'Bonnefond (19170)' => '19027', - 'Bonnegarde (40330)' => '40047', - 'Bonnes (16390)' => '16049', - 'Bonnes (86300)' => '86031', - 'Bonnetan (33370)' => '33061', - 'Bonneuil (16120)' => '16050', - 'Bonneuil-Matours (86210)' => '86032', - 'Bonneville (16170)' => '16051', - 'Bonneville-et-Saint-Avit-de-Fumadières (24230)' => '24048', - 'Bonnut (64300)' => '64135', - 'Bonzac (33910)' => '33062', - 'Boos (40370)' => '40048', - 'Borce (64490)' => '64136', - 'Bord-Saint-Georges (23230)' => '23026', - 'Bordeaux (33000)' => '33063', - 'Bordères (64800)' => '64137', - 'Bordères-et-Lamensans (40270)' => '40049', - 'Bordes (64510)' => '64138', - 'Bords (17430)' => '17053', - 'Boresse-et-Martron (17270)' => '17054', - 'Borrèze (24590)' => '24050', - 'Bors (Canton de Baignes-Sainte-Radegonde) (16360)' => '16053', - 'Bors (Canton de Montmoreau-Saint-Cybard) (16190)' => '16052', - 'Bort-les-Orgues (19110)' => '19028', - 'Boscamnant (17360)' => '17055', - 'Bosdarros (64290)' => '64139', - 'Bosmie-l\'Aiguille (87110)' => '87021', - 'Bosmoreau-les-Mines (23400)' => '23027', - 'Bosroger (23200)' => '23028', - 'Bosset (24130)' => '24051', - 'Bossugan (33350)' => '33064', - 'Bostens (40090)' => '40050', - 'Boucau (64340)' => '64140', - 'Boudy-de-Beauregard (47290)' => '47033', - 'Boueilh-Boueilho-Lasque (64330)' => '64141', - 'Bouëx (16410)' => '16055', - 'Bougarber (64230)' => '64142', - 'Bouglon (47250)' => '47034', - 'Bougneau (17800)' => '17056', - 'Bougon (79800)' => '79042', - 'Bougue (40090)' => '40051', - 'Bouhet (17540)' => '17057', - 'Bouillac (24480)' => '24052', - 'Bouillé-Loretz (79290)' => '79043', - 'Bouillé-Saint-Paul (79290)' => '79044', - 'Bouillon (64410)' => '64143', - 'Bouin (79110)' => '79045', - 'Boulazac Isle Manoire (24750)' => '24053', - 'Bouliac (33270)' => '33065', - 'Boumourt (64370)' => '64144', - 'Bouniagues (24560)' => '24054', - 'Bourcefranc-le-Chapus (17560)' => '17058', - 'Bourdalat (40190)' => '40052', - 'Bourdeilles (24310)' => '24055', - 'Bourdelles (33190)' => '33066', - 'Bourdettes (64800)' => '64145', - 'Bouresse (86410)' => '86034', - 'Bourg (33710)' => '33067', - 'Bourg-Archambault (86390)' => '86035', - 'Bourg-Charente (16200)' => '16056', - 'Bourg-des-Maisons (24320)' => '24057', - 'Bourg-du-Bost (24600)' => '24058', - 'Bourganeuf (23400)' => '23030', - 'Bourgnac (24400)' => '24059', - 'Bourgneuf (17220)' => '17059', - 'Bourgougnague (47410)' => '47035', - 'Bourideys (33113)' => '33068', - 'Bourlens (47370)' => '47036', - 'Bournand (86120)' => '86036', - 'Bournel (47210)' => '47037', - 'Bourniquel (24150)' => '24060', - 'Bournos (64450)' => '64146', - 'Bourran (47320)' => '47038', - 'Bourriot-Bergonce (40120)' => '40053', - 'Bourrou (24110)' => '24061', - 'Boussac (23600)' => '23031', - 'Boussac-Bourg (23600)' => '23032', - 'Boussais (79600)' => '79047', - 'Boussès (47420)' => '47039', - 'Bouteilles-Saint-Sébastien (24320)' => '24062', - 'Boutenac-Touvent (17120)' => '17060', - 'Bouteville (16120)' => '16057', - 'Boutiers-Saint-Trojan (16100)' => '16058', - 'Bouzic (24250)' => '24063', - 'Brach (33480)' => '33070', - 'Bran (17210)' => '17061', - 'Branceilles (19500)' => '19029', - 'Branne (33420)' => '33071', - 'Brannens (33124)' => '33072', - 'Brantôme en Périgord (24310)' => '24064', - 'Brassempouy (40330)' => '40054', - 'Braud-et-Saint-Louis (33820)' => '33073', - 'Brax (47310)' => '47040', - 'Bresdon (17490)' => '17062', - 'Bressuire (79300)' => '79049', - 'Bretagne-de-Marsan (40280)' => '40055', - 'Bretignolles (79140)' => '79050', - 'Brettes (16240)' => '16059', - 'Breuil-la-Réorte (17700)' => '17063', - 'Breuil-Magné (17870)' => '17065', - 'Breuilaufa (87300)' => '87022', - 'Breuilh (24380)' => '24065', - 'Breuillet (17920)' => '17064', - 'Bréville (16370)' => '16060', - 'Brie (16590)' => '16061', - 'Brie (79100)' => '79054', - 'Brie-sous-Archiac (17520)' => '17066', - 'Brie-sous-Barbezieux (16300)' => '16062', - 'Brie-sous-Chalais (16210)' => '16063', - 'Brie-sous-Matha (17160)' => '17067', - 'Brie-sous-Mortagne (17120)' => '17068', - 'Brieuil-sur-Chizé (79170)' => '79055', - 'Brignac-la-Plaine (19310)' => '19030', - 'Brigueil-le-Chantre (86290)' => '86037', - 'Brigueuil (16420)' => '16064', - 'Brillac (16500)' => '16065', - 'Brion (86160)' => '86038', - 'Brion-près-Thouet (79290)' => '79056', - 'Brioux-sur-Boutonne (79170)' => '79057', - 'Briscous (64240)' => '64147', - 'Brive-la-Gaillarde (19100)' => '19031', - 'Brives-sur-Charente (17800)' => '17069', - 'Brivezac (19120)' => '19032', - 'Brizambourg (17770)' => '17070', - 'Brocas (40420)' => '40056', - 'Brossac (16480)' => '16066', - 'Brouchaud (24210)' => '24066', - 'Brouqueyran (33124)' => '33074', - 'Brousse (23700)' => '23034', - 'Bruch (47130)' => '47041', - 'Bruges (33520)' => '33075', - 'Bruges-Capbis-Mifaget (64800)' => '64148', - 'Brugnac (47260)' => '47042', - 'Brûlain (79230)' => '79058', - 'Brux (86510)' => '86039', - 'Buanes (40320)' => '40057', - 'Budelière (23170)' => '23035', - 'Budos (33720)' => '33076', - 'Bugeat (19170)' => '19033', - 'Bugnein (64190)' => '64149', - 'Bujaleuf (87460)' => '87024', - 'Bunus (64120)' => '64150', - 'Bunzac (16110)' => '16067', - 'Burgaronne (64390)' => '64151', - 'Burgnac (87800)' => '87025', - 'Burie (17770)' => '17072', - 'Buros (64160)' => '64152', - 'Burosse-Mendousse (64330)' => '64153', - 'Bussac (24350)' => '24069', - 'Bussac-Forêt (17210)' => '17074', - 'Bussac-sur-Charente (17100)' => '17073', - 'Busserolles (24360)' => '24070', - 'Bussière-Badil (24360)' => '24071', - 'Bussière-Dunoise (23320)' => '23036', - 'Bussière-Galant (87230)' => '87027', - 'Bussière-Nouvelle (23700)' => '23037', - 'Bussière-Poitevine (87320)' => '87028', - 'Bussière-Saint-Georges (23600)' => '23038', - 'Bussunarits-Sarrasquette (64220)' => '64154', - 'Bustince-Iriberry (64220)' => '64155', - 'Buxerolles (86180)' => '86041', - 'Buxeuil (37160)' => '86042', - 'Buzet-sur-Baïse (47160)' => '47043', - 'Buziet (64680)' => '64156', - 'Buzy (64260)' => '64157', - 'Cabanac-et-Villagrains (33650)' => '33077', - 'Cabara (33420)' => '33078', - 'Cabariot (17430)' => '17075', - 'Cabidos (64410)' => '64158', - 'Cachen (40120)' => '40058', - 'Cadarsac (33750)' => '33079', - 'Cadaujac (33140)' => '33080', - 'Cadillac (33410)' => '33081', - 'Cadillac-en-Fronsadais (33240)' => '33082', - 'Cadillon (64330)' => '64159', - 'Cagnotte (40300)' => '40059', - 'Cahuzac (47330)' => '47044', - 'Calès (24150)' => '24073', - 'Calignac (47600)' => '47045', - 'Callen (40430)' => '40060', - 'Calonges (47430)' => '47046', - 'Calviac-en-Périgord (24370)' => '24074', - 'Camarsac (33750)' => '33083', - 'Cambes (33880)' => '33084', - 'Cambes (47350)' => '47047', - 'Camblanes-et-Meynac (33360)' => '33085', - 'Cambo-les-Bains (64250)' => '64160', - 'Came (64520)' => '64161', - 'Camiac-et-Saint-Denis (33420)' => '33086', - 'Camiran (33190)' => '33087', - 'Camou-Cihigue (64470)' => '64162', - 'Campagnac-lès-Quercy (24550)' => '24075', - 'Campagne (24260)' => '24076', - 'Campagne (40090)' => '40061', - 'Campet-et-Lamolère (40090)' => '40062', - 'Camps-Saint-Mathurin-Léobazel (19430)' => '19034', - 'Camps-sur-l\'Isle (33660)' => '33088', - 'Campsegret (24140)' => '24077', - 'Campugnan (33390)' => '33089', - 'Cancon (47290)' => '47048', - 'Candresse (40180)' => '40063', - 'Canéjan (33610)' => '33090', - 'Canenx-et-Réaut (40090)' => '40064', - 'Cantenac (33460)' => '33091', - 'Cantillac (24530)' => '24079', - 'Cantois (33760)' => '33092', - 'Capbreton (40130)' => '40065', - 'Capdrot (24540)' => '24080', - 'Capian (33550)' => '33093', - 'Caplong (33220)' => '33094', - 'Captieux (33840)' => '33095', - 'Carbon-Blanc (33560)' => '33096', - 'Carcans (33121)' => '33097', - 'Carcarès-Sainte-Croix (40400)' => '40066', - 'Carcen-Ponson (40400)' => '40067', - 'Cardan (33410)' => '33098', - 'Cardesse (64360)' => '64165', - 'Carignan-de-Bordeaux (33360)' => '33099', - 'Carlux (24370)' => '24081', - 'Caro (64220)' => '64166', - 'Carrère (64160)' => '64167', - 'Carresse-Cassaber (64270)' => '64168', - 'Cars (33390)' => '33100', - 'Carsac-Aillac (24200)' => '24082', - 'Carsac-de-Gurson (24610)' => '24083', - 'Cartelègue (33390)' => '33101', - 'Carves (24170)' => '24084', - 'Cassen (40380)' => '40068', - 'Casseneuil (47440)' => '47049', - 'Casseuil (33190)' => '33102', - 'Cassignas (47340)' => '47050', - 'Castagnède (64270)' => '64170', - 'Castaignos-Souslens (40700)' => '40069', - 'Castandet (40270)' => '40070', - 'Casteide-Cami (64170)' => '64171', - 'Casteide-Candau (64370)' => '64172', - 'Casteide-Doat (64460)' => '64173', - 'Castel-Sarrazin (40330)' => '40074', - 'Castelculier (47240)' => '47051', - 'Casteljaloux (47700)' => '47052', - 'Castella (47340)' => '47053', - 'Castelmoron-d\'Albret (33540)' => '33103', - 'Castelmoron-sur-Lot (47260)' => '47054', - 'Castelnau-Chalosse (40360)' => '40071', - 'Castelnau-de-Médoc (33480)' => '33104', - 'Castelnau-sur-Gupie (47180)' => '47056', - 'Castelnau-Tursan (40320)' => '40072', - 'Castelnaud-de-Gratecambe (47290)' => '47055', - 'Castelnaud-la-Chapelle (24250)' => '24086', - 'Castelner (40700)' => '40073', - 'Castels (24220)' => '24087', - 'Castelviel (33540)' => '33105', - 'Castéra-Loubix (64460)' => '64174', - 'Castet (64260)' => '64175', - 'Castetbon (64190)' => '64176', - 'Castétis (64300)' => '64177', - 'Castetnau-Camblong (64190)' => '64178', - 'Castetner (64300)' => '64179', - 'Castetpugon (64330)' => '64180', - 'Castets (40260)' => '40075', - 'Castets-en-Dorthe (33210)' => '33106', - 'Castillon (Canton d\'Arthez-de-Béarn) (64370)' => '64181', - 'Castillon (Canton de Lembeye) (64350)' => '64182', - 'Castillon-de-Castets (33210)' => '33107', - 'Castillon-la-Bataille (33350)' => '33108', - 'Castillonnès (47330)' => '47057', - 'Castres-Gironde (33640)' => '33109', - 'Caubeyres (47160)' => '47058', - 'Caubios-Loos (64230)' => '64183', - 'Caubon-Saint-Sauveur (47120)' => '47059', - 'Caudecoste (47220)' => '47060', - 'Caudrot (33490)' => '33111', - 'Caumont (33540)' => '33112', - 'Caumont-sur-Garonne (47430)' => '47061', - 'Cauna (40500)' => '40076', - 'Caunay (79190)' => '79060', - 'Cauneille (40300)' => '40077', - 'Caupenne (40250)' => '40078', - 'Cause-de-Clérans (24150)' => '24088', - 'Cauvignac (33690)' => '33113', - 'Cauzac (47470)' => '47062', - 'Cavarc (47330)' => '47063', - 'Cavignac (33620)' => '33114', - 'Cazalis (33113)' => '33115', - 'Cazalis (40700)' => '40079', - 'Cazats (33430)' => '33116', - 'Cazaugitat (33790)' => '33117', - 'Cazères-sur-l\'Adour (40270)' => '40080', - 'Cazideroque (47370)' => '47064', - 'Cazoulès (24370)' => '24089', - 'Ceaux-en-Couhé (86700)' => '86043', - 'Ceaux-en-Loudun (86200)' => '86044', - 'Celle-Lévescault (86600)' => '86045', - 'Cellefrouin (16260)' => '16068', - 'Celles (17520)' => '17076', - 'Celles (24600)' => '24090', - 'Celles-sur-Belle (79370)' => '79061', - 'Cellettes (16230)' => '16069', - 'Cénac (33360)' => '33118', - 'Cénac-et-Saint-Julien (24250)' => '24091', - 'Cendrieux (24380)' => '24092', - 'Cenon (33150)' => '33119', - 'Cenon-sur-Vienne (86530)' => '86046', - 'Cercles (24320)' => '24093', - 'Cercoux (17270)' => '17077', - 'Cère (40090)' => '40081', - 'Cerizay (79140)' => '79062', - 'Cernay (86140)' => '86047', - 'Cérons (33720)' => '33120', - 'Cersay (79290)' => '79063', - 'Cescau (64170)' => '64184', - 'Cessac (33760)' => '33121', - 'Cestas (33610)' => '33122', - 'Cette-Eygun (64490)' => '64185', - 'Ceyroux (23210)' => '23042', - 'Cézac (33620)' => '33123', - 'Chabanais (16150)' => '16070', - 'Chabournay (86380)' => '86048', - 'Chabrac (16150)' => '16071', - 'Chabrignac (19350)' => '19035', - 'Chadenac (17800)' => '17078', - 'Chadurie (16250)' => '16072', - 'Chail (79500)' => '79064', - 'Chaillac-sur-Vienne (87200)' => '87030', - 'Chaillevette (17890)' => '17079', - 'Chalagnac (24380)' => '24094', - 'Chalais (16210)' => '16073', - 'Chalais (24800)' => '24095', - 'Chalais (86200)' => '86049', - 'Chalandray (86190)' => '86050', - 'Challignac (16300)' => '16074', - 'Châlus (87230)' => '87032', - 'Chamadelle (33230)' => '33124', - 'Chamberaud (23480)' => '23043', - 'Chamberet (19370)' => '19036', - 'Chambon (17290)' => '17080', - 'Chambon-Sainte-Croix (23220)' => '23044', - 'Chambon-sur-Voueize (23170)' => '23045', - 'Chambonchard (23110)' => '23046', - 'Chamborand (23240)' => '23047', - 'Chamboret (87140)' => '87033', - 'Chamboulive (19450)' => '19037', - 'Chameyrat (19330)' => '19038', - 'Chamouillac (17130)' => '17081', - 'Champagnac (17500)' => '17082', - 'Champagnac-de-Belair (24530)' => '24096', - 'Champagnac-la-Noaille (19320)' => '19039', - 'Champagnac-la-Prune (19320)' => '19040', - 'Champagnac-la-Rivière (87150)' => '87034', - 'Champagnat (23190)' => '23048', - 'Champagne (17620)' => '17083', - 'Champagne-et-Fontaine (24320)' => '24097', - 'Champagné-le-Sec (86510)' => '86051', - 'Champagne-Mouton (16350)' => '16076', - 'Champagné-Saint-Hilaire (86160)' => '86052', - 'Champagne-Vigny (16250)' => '16075', - 'Champagnolles (17240)' => '17084', - 'Champcevinel (24750)' => '24098', - 'Champdeniers-Saint-Denis (79220)' => '79066', - 'Champdolent (17430)' => '17085', - 'Champeaux-et-la-Chapelle-Pommier (24340)' => '24099', - 'Champigny-le-Sec (86170)' => '86053', - 'Champmillon (16290)' => '16077', - 'Champnétery (87400)' => '87035', - 'Champniers (16430)' => '16078', - 'Champniers (86400)' => '86054', - 'Champniers-et-Reilhac (24360)' => '24100', - 'Champs-Romain (24470)' => '24101', - 'Champsac (87230)' => '87036', - 'Champsanglard (23220)' => '23049', - 'Chanac-les-Mines (19150)' => '19041', - 'Chancelade (24650)' => '24102', - 'Chaniers (17610)' => '17086', - 'Chantecorps (79340)' => '79068', - 'Chanteix (19330)' => '19042', - 'Chanteloup (79320)' => '79069', - 'Chantemerle-sur-la-Soie (17380)' => '17087', - 'Chantérac (24190)' => '24104', - 'Chantillac (16360)' => '16079', - 'Chapdeuil (24320)' => '24105', - 'Chapelle-Spinasse (19300)' => '19046', - 'Chapelle-Viviers (86300)' => '86059', - 'Chaptelat (87270)' => '87038', - 'Chard (23700)' => '23053', - 'Charmé (16140)' => '16083', - 'Charrais (86170)' => '86060', - 'Charras (16380)' => '16084', - 'Charre (64190)' => '64186', - 'Charritte-de-Bas (64130)' => '64187', - 'Charron (17230)' => '17091', - 'Charron (23700)' => '23054', - 'Charroux (86250)' => '86061', - 'Chartrier-Ferrière (19600)' => '19047', - 'Chartuzac (17130)' => '17092', - 'Chassaignes (24600)' => '24114', - 'Chasseneuil-du-Poitou (86360)' => '86062', - 'Chasseneuil-sur-Bonnieure (16260)' => '16085', - 'Chassenon (16150)' => '16086', - 'Chassiecq (16350)' => '16087', - 'Chassors (16200)' => '16088', - 'Chasteaux (19600)' => '19049', - 'Chatain (86250)' => '86063', - 'Château-Chervix (87380)' => '87039', - 'Château-Garnier (86350)' => '86064', - 'Château-l\'Évêque (24460)' => '24115', - 'Château-Larcher (86370)' => '86065', - 'Châteaubernard (16100)' => '16089', - 'Châteauneuf-la-Forêt (87130)' => '87040', - 'Châteauneuf-sur-Charente (16120)' => '16090', - 'Châteauponsac (87290)' => '87041', - 'Châtelaillon-Plage (17340)' => '17094', - 'Châtelard (23700)' => '23055', - 'Châtellerault (86100)' => '86066', - 'Châtelus-le-Marcheix (23430)' => '23056', - 'Châtelus-Malvaleix (23270)' => '23057', - 'Chatenet (17210)' => '17095', - 'Châtignac (16480)' => '16091', - 'Châtillon (86700)' => '86067', - 'Châtillon-sur-Thouet (79200)' => '79080', - 'Châtres (24120)' => '24116', - 'Chauffour-sur-Vell (19500)' => '19050', - 'Chaumeil (19390)' => '19051', - 'Chaunac (17130)' => '17096', - 'Chaunay (86510)' => '86068', - 'Chauray (79180)' => '79081', - 'Chauvigny (86300)' => '86070', - 'Chavagnac (24120)' => '24117', - 'Chavanac (19290)' => '19052', - 'Chavanat (23250)' => '23060', - 'Chaveroche (19200)' => '19053', - 'Chazelles (16380)' => '16093', - 'Chef-Boutonne (79110)' => '79083', - 'Cheissoux (87460)' => '87043', - 'Chenac-Saint-Seurin-d\'Uzet (17120)' => '17098', - 'Chenailler-Mascheix (19120)' => '19054', - 'Chenay (79120)' => '79084', - 'Cheneché (86380)' => '86071', - 'Chénérailles (23130)' => '23061', - 'Chenevelles (86450)' => '86072', - 'Chéniers (23220)' => '23062', - 'Chenommet (16460)' => '16094', - 'Chenon (16460)' => '16095', - 'Chepniers (17210)' => '17099', - 'Chérac (17610)' => '17100', - 'Chéraute (64130)' => '64188', - 'Cherbonnières (17470)' => '17101', - 'Chérigné (79170)' => '79085', - 'Chermignac (17460)' => '17102', - 'Chéronnac (87600)' => '87044', - 'Cherval (24320)' => '24119', - 'Cherveix-Cubas (24390)' => '24120', - 'Cherves (86170)' => '86073', - 'Cherves-Châtelars (16310)' => '16096', - 'Cherves-Richemont (16370)' => '16097', - 'Chervettes (17380)' => '17103', - 'Cherveux (79410)' => '79086', - 'Chevanceaux (17210)' => '17104', - 'Chey (79120)' => '79087', - 'Chiché (79350)' => '79088', - 'Chillac (16480)' => '16099', - 'Chirac (16150)' => '16100', - 'Chirac-Bellevue (19160)' => '19055', - 'Chiré-en-Montreuil (86190)' => '86074', - 'Chives (17510)' => '17105', - 'Chizé (79170)' => '79090', - 'Chouppes (86110)' => '86075', - 'Chourgnac (24640)' => '24121', - 'Ciboure (64500)' => '64189', - 'Cierzac (17520)' => '17106', - 'Cieux (87520)' => '87045', - 'Ciré-d\'Aunis (17290)' => '17107', - 'Cirières (79140)' => '79091', - 'Cissac-Médoc (33250)' => '33125', - 'Cissé (86170)' => '86076', - 'Civaux (86320)' => '86077', - 'Civrac-de-Blaye (33920)' => '33126', - 'Civrac-en-Médoc (33340)' => '33128', - 'Civrac-sur-Dordogne (33350)' => '33127', - 'Civray (86400)' => '86078', - 'Cladech (24170)' => '24122', - 'Clairac (47320)' => '47065', - 'Clairavaux (23500)' => '23063', - 'Claix (16440)' => '16101', - 'Clam (17500)' => '17108', - 'Claracq (64330)' => '64190', - 'Classun (40320)' => '40082', - 'Clavé (79420)' => '79092', - 'Clavette (17220)' => '17109', - 'Clèdes (40320)' => '40083', - 'Clérac (17270)' => '17110', - 'Clergoux (19320)' => '19056', - 'Clermont (40180)' => '40084', - 'Clermont-d\'Excideuil (24160)' => '24124', - 'Clermont-de-Beauregard (24140)' => '24123', - 'Clermont-Dessous (47130)' => '47066', - 'Clermont-Soubiran (47270)' => '47067', - 'Clessé (79350)' => '79094', - 'Cleyrac (33540)' => '33129', - 'Clion (17240)' => '17111', - 'Cloué (86600)' => '86080', - 'Clugnat (23270)' => '23064', - 'Clussais-la-Pommeraie (79190)' => '79095', - 'Coarraze (64800)' => '64191', - 'Cocumont (47250)' => '47068', - 'Cognac (16100)' => '16102', - 'Cognac-la-Forêt (87310)' => '87046', - 'Coimères (33210)' => '33130', - 'Coirac (33540)' => '33131', - 'Coivert (17330)' => '17114', - 'Colayrac-Saint-Cirq (47450)' => '47069', - 'Collonges-la-Rouge (19500)' => '19057', - 'Colombier (24560)' => '24126', - 'Colombiers (17460)' => '17115', - 'Colombiers (86490)' => '86081', - 'Colondannes (23800)' => '23065', - 'Coly (24120)' => '24127', - 'Comberanche-et-Épeluche (24600)' => '24128', - 'Combiers (16320)' => '16103', - 'Combrand (79140)' => '79096', - 'Combressol (19250)' => '19058', - 'Commensacq (40210)' => '40085', - 'Compreignac (87140)' => '87047', - 'Comps (33710)' => '33132', - 'Concèze (19350)' => '19059', - 'Conchez-de-Béarn (64330)' => '64192', - 'Condac (16700)' => '16104', - 'Condat-sur-Ganaveix (19140)' => '19060', - 'Condat-sur-Trincou (24530)' => '24129', - 'Condat-sur-Vézère (24570)' => '24130', - 'Condat-sur-Vienne (87920)' => '87048', - 'Condéon (16360)' => '16105', - 'Condezaygues (47500)' => '47070', - 'Confolens (16500)' => '16106', - 'Confolent-Port-Dieu (19200)' => '19167', - 'Conne-de-Labarde (24560)' => '24132', - 'Connezac (24300)' => '24131', - 'Consac (17150)' => '17116', - 'Contré (17470)' => '17117', - 'Corbère-Abères (64350)' => '64193', - 'Corgnac-sur-l\'Isle (24800)' => '24134', - 'Corignac (17130)' => '17118', - 'Corme-Écluse (17600)' => '17119', - 'Corme-Royal (17600)' => '17120', - 'Cornil (19150)' => '19061', - 'Cornille (24750)' => '24135', - 'Corrèze (19800)' => '19062', - 'Coslédaà-Lube-Boast (64160)' => '64194', - 'Cosnac (19360)' => '19063', - 'Coubeyrac (33890)' => '33133', - 'Coubjours (24390)' => '24136', - 'Coublucq (64410)' => '64195', - 'Coudures (40500)' => '40086', - 'Couffy-sur-Sarsonne (19340)' => '19064', - 'Couhé (86700)' => '86082', - 'Coulaures (24420)' => '24137', - 'Coulgens (16560)' => '16107', - 'Coulombiers (86600)' => '86083', - 'Coulon (79510)' => '79100', - 'Coulonges (16330)' => '16108', - 'Coulonges (17800)' => '17122', - 'Coulonges (86290)' => '86084', - 'Coulonges-sur-l\'Autize (79160)' => '79101', - 'Coulonges-Thouarsais (79330)' => '79102', - 'Coulounieix-Chamiers (24660)' => '24138', - 'Coulx (47260)' => '47071', - 'Couquèques (33340)' => '33134', - 'Courant (17330)' => '17124', - 'Courbiac (47370)' => '47072', - 'Courbillac (16200)' => '16109', - 'Courcelles (17400)' => '17125', - 'Courcerac (17160)' => '17126', - 'Courcôme (16240)' => '16110', - 'Courçon (17170)' => '17127', - 'Courcoury (17100)' => '17128', - 'Courgeac (16190)' => '16111', - 'Courlac (16210)' => '16112', - 'Courlay (79440)' => '79103', - 'Courpiac (33760)' => '33135', - 'Courpignac (17130)' => '17129', - 'Cours (47360)' => '47073', - 'Cours (79220)' => '79104', - 'Cours-de-Monségur (33580)' => '33136', - 'Cours-de-Pile (24520)' => '24140', - 'Cours-les-Bains (33690)' => '33137', - 'Coursac (24430)' => '24139', - 'Courteix (19340)' => '19065', - 'Coussac-Bonneval (87500)' => '87049', - 'Coussay (86110)' => '86085', - 'Coussay-les-Bois (86270)' => '86086', - 'Couthures-sur-Garonne (47180)' => '47074', - 'Coutières (79340)' => '79105', - 'Coutras (33230)' => '33138', - 'Couture (16460)' => '16114', - 'Couture-d\'Argenson (79110)' => '79106', - 'Coutures (24320)' => '24141', - 'Coutures (33580)' => '33139', - 'Coux (17130)' => '17130', - 'Coux et Bigaroque-Mouzens (24220)' => '24142', - 'Couze-et-Saint-Front (24150)' => '24143', - 'Couzeix (87270)' => '87050', - 'Cozes (17120)' => '17131', - 'Cramchaban (17170)' => '17132', - 'Craon (86110)' => '86087', - 'Cravans (17260)' => '17133', - 'Crazannes (17350)' => '17134', - 'Créon (33670)' => '33140', - 'Créon-d\'Armagnac (40240)' => '40087', - 'Cressac-Saint-Genis (16250)' => '16115', - 'Cressat (23140)' => '23068', - 'Cressé (17160)' => '17135', - 'Creyssac (24350)' => '24144', - 'Creysse (24100)' => '24145', - 'Creyssensac-et-Pissot (24380)' => '24146', - 'Crézières (79110)' => '79107', - 'Criteuil-la-Magdeleine (16300)' => '16116', - 'Crocq (23260)' => '23069', - 'Croignon (33750)' => '33141', - 'Croix-Chapeau (17220)' => '17136', - 'Cromac (87160)' => '87053', - 'Crouseilles (64350)' => '64196', - 'Croutelle (86240)' => '86088', - 'Crozant (23160)' => '23070', - 'Croze (23500)' => '23071', - 'Cubjac (24640)' => '24147', - 'Cublac (19520)' => '19066', - 'Cubnezais (33620)' => '33142', - 'Cubzac-les-Ponts (33240)' => '33143', - 'Cudos (33430)' => '33144', - 'Cuhon (86110)' => '86089', - 'Cunèges (24240)' => '24148', - 'Cuq (47220)' => '47076', - 'Cuqueron (64360)' => '64197', - 'Curac (16210)' => '16117', - 'Curçay-sur-Dive (86120)' => '86090', - 'Curemonte (19500)' => '19067', - 'Cursan (33670)' => '33145', - 'Curzay-sur-Vonne (86600)' => '86091', - 'Cussac (87150)' => '87054', - 'Cussac-Fort-Médoc (33460)' => '33146', - 'Cuzorn (47500)' => '47077', - 'Daglan (24250)' => '24150', - 'Daignac (33420)' => '33147', - 'Damazan (47160)' => '47078', - 'Dampierre-sur-Boutonne (17470)' => '17138', - 'Dampniat (19360)' => '19068', - 'Dangé-Saint-Romain (86220)' => '86092', - 'Darazac (19220)' => '19069', - 'Dardenac (33420)' => '33148', - 'Darnac (87320)' => '87055', - 'Darnets (19300)' => '19070', - 'Daubèze (33540)' => '33149', - 'Dausse (47140)' => '47079', - 'Davignac (19250)' => '19071', - 'Dax (40100)' => '40088', - 'Denguin (64230)' => '64198', - 'Dercé (86420)' => '86093', - 'Deviat (16190)' => '16118', - 'Dévillac (47210)' => '47080', - 'Dienné (86410)' => '86094', - 'Dieulivol (33580)' => '33150', - 'Dignac (16410)' => '16119', - 'Dinsac (87210)' => '87056', - 'Dirac (16410)' => '16120', - 'Dissay (86130)' => '86095', - 'Diusse (64330)' => '64199', - 'Doazit (40700)' => '40089', - 'Doazon (64370)' => '64200', - 'Doeuil-sur-le-Mignon (17330)' => '17139', - 'Dognen (64190)' => '64201', - 'Doissat (24170)' => '24151', - 'Dolmayrac (47110)' => '47081', - 'Dolus-d\'Oléron (17550)' => '17140', - 'Domeyrot (23140)' => '23072', - 'Domezain-Berraute (64120)' => '64202', - 'Domme (24250)' => '24152', - 'Dompierre-les-Églises (87190)' => '87057', - 'Dompierre-sur-Charente (17610)' => '17141', - 'Dompierre-sur-Mer (17139)' => '17142', - 'Domps (87120)' => '87058', - 'Dondas (47470)' => '47082', - 'Donnezac (33860)' => '33151', - 'Dontreix (23700)' => '23073', - 'Donzac (33410)' => '33152', - 'Donzacq (40360)' => '40090', - 'Donzenac (19270)' => '19072', - 'Douchapt (24350)' => '24154', - 'Doudrac (47210)' => '47083', - 'Doulezon (33350)' => '33153', - 'Doumy (64450)' => '64203', - 'Dournazac (87230)' => '87060', - 'Doussay (86140)' => '86096', - 'Douville (24140)' => '24155', - 'Doux (79390)' => '79108', - 'Douzains (47330)' => '47084', - 'Douzat (16290)' => '16121', - 'Douzillac (24190)' => '24157', - 'Droux (87190)' => '87061', - 'Duhort-Bachen (40800)' => '40091', - 'Dumes (40500)' => '40092', - 'Dun-le-Palestel (23800)' => '23075', - 'Durance (47420)' => '47085', - 'Duras (47120)' => '47086', - 'Dussac (24270)' => '24158', - 'Eaux-Bonnes (64440)' => '64204', - 'Ébréon (16140)' => '16122', - 'Échallat (16170)' => '16123', - 'Échebrune (17800)' => '17145', - 'Échillais (17620)' => '17146', - 'Échiré (79410)' => '79109', - 'Échourgnac (24410)' => '24159', - 'Écoyeux (17770)' => '17147', - 'Écuras (16220)' => '16124', - 'Écurat (17810)' => '17148', - 'Édon (16320)' => '16125', - 'Égletons (19300)' => '19073', - 'Église-Neuve-d\'Issac (24400)' => '24161', - 'Église-Neuve-de-Vergt (24380)' => '24160', - 'Empuré (16240)' => '16127', - 'Engayrac (47470)' => '47087', - 'Ensigné (79170)' => '79111', - 'Épannes (79270)' => '79112', - 'Épargnes (17120)' => '17152', - 'Épenède (16490)' => '16128', - 'Éraville (16120)' => '16129', - 'Escalans (40310)' => '40093', - 'Escassefort (47350)' => '47088', - 'Escaudes (33840)' => '33155', - 'Escaunets (65500)' => '65160', - 'Esclottes (47120)' => '47089', - 'Escoire (24420)' => '24162', - 'Escos (64270)' => '64205', - 'Escot (64490)' => '64206', - 'Escou (64870)' => '64207', - 'Escoubès (64160)' => '64208', - 'Escource (40210)' => '40094', - 'Escoussans (33760)' => '33156', - 'Escout (64870)' => '64209', - 'Escurès (64350)' => '64210', - 'Eslourenties-Daban (64420)' => '64211', - 'Esnandes (17137)' => '17153', - 'Espagnac (19150)' => '19075', - 'Espartignac (19140)' => '19076', - 'Espéchède (64160)' => '64212', - 'Espelette (64250)' => '64213', - 'Espès-Undurein (64130)' => '64214', - 'Espiens (47600)' => '47090', - 'Espiet (33420)' => '33157', - 'Espiute (64390)' => '64215', - 'Espoey (64420)' => '64216', - 'Esquiule (64400)' => '64217', - 'Esse (16500)' => '16131', - 'Essouvert (17400)' => '17277', - 'Estérençuby (64220)' => '64218', - 'Estialescq (64290)' => '64219', - 'Estibeaux (40290)' => '40095', - 'Estigarde (40240)' => '40096', - 'Estillac (47310)' => '47091', - 'Estivals (19600)' => '19077', - 'Estivaux (19410)' => '19078', - 'Estos (64400)' => '64220', - 'Étagnac (16150)' => '16132', - 'Étaules (17750)' => '17155', - 'Étauliers (33820)' => '33159', - 'Etcharry (64120)' => '64221', - 'Etchebar (64470)' => '64222', - 'Étouars (24360)' => '24163', - 'Étriac (16250)' => '16133', - 'Etsaut (64490)' => '64223', - 'Eugénie-les-Bains (40320)' => '40097', - 'Évaux-les-Bains (23110)' => '23076', - 'Excideuil (24160)' => '24164', - 'Exideuil (16150)' => '16134', - 'Exireuil (79400)' => '79114', - 'Exoudun (79800)' => '79115', - 'Expiremont (17130)' => '17156', - 'Eybouleuf (87400)' => '87062', - 'Eyburie (19140)' => '19079', - 'Eygurande (19340)' => '19080', - 'Eygurande-et-Gardedeuil (24700)' => '24165', - 'Eyjeaux (87220)' => '87063', - 'Eyliac (24330)' => '24166', - 'Eymet (24500)' => '24167', - 'Eymouthiers (16220)' => '16135', - 'Eymoutiers (87120)' => '87064', - 'Eynesse (33220)' => '33160', - 'Eyrans (33390)' => '33161', - 'Eyrein (19800)' => '19081', - 'Eyres-Moncube (40500)' => '40098', - 'Eysines (33320)' => '33162', - 'Eysus (64400)' => '64224', - 'Eyvirat (24460)' => '24170', - 'Eyzerac (24800)' => '24171', - 'Faleyras (33760)' => '33163', - 'Fals (47220)' => '47092', - 'Fanlac (24290)' => '24174', - 'Fargues (33210)' => '33164', - 'Fargues (40500)' => '40099', - 'Fargues-Saint-Hilaire (33370)' => '33165', - 'Fargues-sur-Ourbise (47700)' => '47093', - 'Fauguerolles (47400)' => '47094', - 'Fauillet (47400)' => '47095', - 'Faurilles (24560)' => '24176', - 'Faux (24560)' => '24177', - 'Faux-la-Montagne (23340)' => '23077', - 'Faux-Mazuras (23400)' => '23078', - 'Favars (19330)' => '19082', - 'Faye-l\'Abbesse (79350)' => '79116', - 'Faye-sur-Ardin (79160)' => '79117', - 'Féas (64570)' => '64225', - 'Felletin (23500)' => '23079', - 'Fénery (79450)' => '79118', - 'Féniers (23100)' => '23080', - 'Fenioux (17350)' => '17157', - 'Fenioux (79160)' => '79119', - 'Ferrensac (47330)' => '47096', - 'Ferrières (17170)' => '17158', - 'Festalemps (24410)' => '24178', - 'Feugarolles (47230)' => '47097', - 'Feuillade (16380)' => '16137', - 'Feyt (19340)' => '19083', - 'Feytiat (87220)' => '87065', - 'Fichous-Riumayou (64410)' => '64226', - 'Fieux (47600)' => '47098', - 'Firbeix (24450)' => '24180', - 'Flaugeac (24240)' => '24181', - 'Flaujagues (33350)' => '33168', - 'Flavignac (87230)' => '87066', - 'Flayat (23260)' => '23081', - 'Fléac (16730)' => '16138', - 'Fléac-sur-Seugne (17800)' => '17159', - 'Fleix (86300)' => '86098', - 'Fleurac (16200)' => '16139', - 'Fleurac (24580)' => '24183', - 'Fleurat (23320)' => '23082', - 'Fleuré (86340)' => '86099', - 'Floirac (17120)' => '17160', - 'Floirac (33270)' => '33167', - 'Florimont-Gaumier (24250)' => '24184', - 'Floudès (33190)' => '33169', - 'Folles (87250)' => '87067', - 'Fomperron (79340)' => '79121', - 'Fongrave (47260)' => '47099', - 'Fonroque (24500)' => '24186', - 'Fontaine-Chalendray (17510)' => '17162', - 'Fontaine-le-Comte (86240)' => '86100', - 'Fontaines-d\'Ozillac (17500)' => '17163', - 'Fontanières (23110)' => '23083', - 'Fontclaireau (16230)' => '16140', - 'Fontcouverte (17100)' => '17164', - 'Fontenet (17400)' => '17165', - 'Fontenille (16230)' => '16141', - 'Fontenille-Saint-Martin-d\'Entraigues (79110)' => '79122', - 'Fontet (33190)' => '33170', - 'Forges (17290)' => '17166', - 'Forgès (19380)' => '19084', - 'Fors (79230)' => '79125', - 'Fossemagne (24210)' => '24188', - 'Fossès-et-Baleyssac (33190)' => '33171', - 'Fougueyrolles (33220)' => '24189', - 'Foulayronnes (47510)' => '47100', - 'Fouleix (24380)' => '24190', - 'Fouquebrune (16410)' => '16143', - 'Fouqueure (16140)' => '16144', - 'Fouras (17450)' => '17168', - 'Fourques-sur-Garonne (47200)' => '47101', - 'Fours (33390)' => '33172', - 'Foussignac (16200)' => '16145', - 'Fraisse (24130)' => '24191', - 'Francescas (47600)' => '47102', - 'François (79260)' => '79128', - 'Francs (33570)' => '33173', - 'Fransèches (23480)' => '23086', - 'Fréchou (47600)' => '47103', - 'Frégimont (47360)' => '47104', - 'Frespech (47140)' => '47105', - 'Fresselines (23450)' => '23087', - 'Fressines (79370)' => '79129', - 'Fromental (87250)' => '87068', - 'Fronsac (33126)' => '33174', - 'Frontenac (33760)' => '33175', - 'Frontenay-Rohan-Rohan (79270)' => '79130', - 'Frozes (86190)' => '86102', - 'Fumel (47500)' => '47106', - 'Gaas (40350)' => '40101', - 'Gabarnac (33410)' => '33176', - 'Gabarret (40310)' => '40102', - 'Gabaston (64160)' => '64227', - 'Gabat (64120)' => '64228', - 'Gabillou (24210)' => '24192', - 'Gageac-et-Rouillac (24240)' => '24193', - 'Gaillan-en-Médoc (33340)' => '33177', - 'Gaillères (40090)' => '40103', - 'Gajac (33430)' => '33178', - 'Gajoubert (87330)' => '87069', - 'Galapian (47190)' => '47107', - 'Galgon (33133)' => '33179', - 'Gamarde-les-Bains (40380)' => '40104', - 'Gamarthe (64220)' => '64229', - 'Gan (64290)' => '64230', - 'Gans (33430)' => '33180', - 'Garat (16410)' => '16146', - 'Gardegan-et-Tourtirac (33350)' => '33181', - 'Gardères (65320)' => '65185', - 'Gardes-le-Pontaroux (16320)' => '16147', - 'Gardonne (24680)' => '24194', - 'Garein (40420)' => '40105', - 'Garindein (64130)' => '64231', - 'Garlède-Mondebat (64450)' => '64232', - 'Garlin (64330)' => '64233', - 'Garos (64410)' => '64234', - 'Garrey (40180)' => '40106', - 'Garris (64120)' => '64235', - 'Garrosse (40110)' => '40107', - 'Gartempe (23320)' => '23088', - 'Gastes (40160)' => '40108', - 'Gaugeac (24540)' => '24195', - 'Gaujac (47200)' => '47108', - 'Gaujacq (40330)' => '40109', - 'Gauriac (33710)' => '33182', - 'Gauriaguet (33240)' => '33183', - 'Gavaudun (47150)' => '47109', - 'Gayon (64350)' => '64236', - 'Geaune (40320)' => '40110', - 'Geay (17250)' => '17171', - 'Geay (79330)' => '79131', - 'Gelos (64110)' => '64237', - 'Geloux (40090)' => '40111', - 'Gémozac (17260)' => '17172', - 'Genac-Bignac (16170)' => '16148', - 'Gençay (86160)' => '86103', - 'Générac (33920)' => '33184', - 'Génis (24160)' => '24196', - 'Génissac (33420)' => '33185', - 'Genneton (79150)' => '79132', - 'Genouillac (16270)' => '16149', - 'Genouillac (23350)' => '23089', - 'Genouillé (17430)' => '17174', - 'Genouillé (86250)' => '86104', - 'Gensac (33890)' => '33186', - 'Gensac-la-Pallue (16130)' => '16150', - 'Genté (16130)' => '16151', - 'Gentioux-Pigerolles (23340)' => '23090', - 'Ger (64530)' => '64238', - 'Gerderest (64160)' => '64239', - 'Gère-Bélesten (64260)' => '64240', - 'Germignac (17520)' => '17175', - 'Germond-Rouvre (79220)' => '79133', - 'Géronce (64400)' => '64241', - 'Gestas (64190)' => '64242', - 'Géus-d\'Arzacq (64370)' => '64243', - 'Geüs-d\'Oloron (64400)' => '64244', - 'Gibourne (17160)' => '17176', - 'Gibret (40380)' => '40112', - 'Gimel-les-Cascades (19800)' => '19085', - 'Gimeux (16130)' => '16152', - 'Ginestet (24130)' => '24197', - 'Gioux (23500)' => '23091', - 'Gironde-sur-Dropt (33190)' => '33187', - 'Giscos (33840)' => '33188', - 'Givrezac (17260)' => '17178', - 'Gizay (86340)' => '86105', - 'Glandon (87500)' => '87071', - 'Glanges (87380)' => '87072', - 'Glénay (79330)' => '79134', - 'Glénic (23380)' => '23092', - 'Glénouze (86200)' => '86106', - 'Goès (64400)' => '64245', - 'Gomer (64420)' => '64246', - 'Gond-Pontouvre (16160)' => '16154', - 'Gondeville (16200)' => '16153', - 'Gontaud-de-Nogaret (47400)' => '47110', - 'Goos (40180)' => '40113', - 'Gornac (33540)' => '33189', - 'Gorre (87310)' => '87073', - 'Gotein-Libarrenx (64130)' => '64247', - 'Goualade (33840)' => '33190', - 'Gouex (86320)' => '86107', - 'Goulles (19430)' => '19086', - 'Gourbera (40990)' => '40114', - 'Gourdon-Murat (19170)' => '19087', - 'Gourgé (79200)' => '79135', - 'Gournay-Loizé (79110)' => '79136', - 'Gours (33660)' => '33191', - 'Gourville (16170)' => '16156', - 'Gourvillette (17490)' => '17180', - 'Gousse (40465)' => '40115', - 'Gout-Rossignol (24320)' => '24199', - 'Gouts (40400)' => '40116', - 'Gouzon (23230)' => '23093', - 'Gradignan (33170)' => '33192', - 'Grand-Brassac (24350)' => '24200', - 'Grandjean (17350)' => '17181', - 'Grandsaigne (19300)' => '19088', - 'Granges-d\'Ans (24390)' => '24202', - 'Granges-sur-Lot (47260)' => '47111', - 'Granzay-Gript (79360)' => '79137', - 'Grassac (16380)' => '16158', - 'Grateloup-Saint-Gayrand (47400)' => '47112', - 'Graves-Saint-Amant (16120)' => '16297', - 'Grayan-et-l\'Hôpital (33590)' => '33193', - 'Grayssas (47270)' => '47113', - 'Grenade-sur-l\'Adour (40270)' => '40117', - 'Grézac (17120)' => '17183', - 'Grèzes (24120)' => '24204', - 'Grézet-Cavagnan (47250)' => '47114', - 'Grézillac (33420)' => '33194', - 'Grignols (24110)' => '24205', - 'Grignols (33690)' => '33195', - 'Grives (24170)' => '24206', - 'Groléjac (24250)' => '24207', - 'Gros-Chastang (19320)' => '19089', - 'Grun-Bordas (24380)' => '24208', - 'Guéret (23000)' => '23096', - 'Guérin (47250)' => '47115', - 'Guesnes (86420)' => '86109', - 'Guéthary (64210)' => '64249', - 'Guiche (64520)' => '64250', - 'Guillac (33420)' => '33196', - 'Guillos (33720)' => '33197', - 'Guimps (16300)' => '16160', - 'Guinarthe-Parenties (64390)' => '64251', - 'Guitinières (17500)' => '17187', - 'Guîtres (33230)' => '33198', - 'Guizengeard (16480)' => '16161', - 'Gujan-Mestras (33470)' => '33199', - 'Gumond (19320)' => '19090', - 'Gurat (16320)' => '16162', - 'Gurmençon (64400)' => '64252', - 'Gurs (64190)' => '64253', - 'Habas (40290)' => '40118', - 'Hagetaubin (64370)' => '64254', - 'Hagetmau (40700)' => '40119', - 'Haimps (17160)' => '17188', - 'Haims (86310)' => '86110', - 'Halsou (64480)' => '64255', - 'Hanc (79110)' => '79140', - 'Hasparren (64240)' => '64256', - 'Hastingues (40300)' => '40120', - 'Hauriet (40250)' => '40121', - 'Haut-de-Bosdarros (64800)' => '64257', - 'Haut-Mauco (40280)' => '40122', - 'Hautefage (19400)' => '19091', - 'Hautefage-la-Tour (47340)' => '47117', - 'Hautefaye (24300)' => '24209', - 'Hautefort (24390)' => '24210', - 'Hautesvignes (47400)' => '47118', - 'Haux (33550)' => '33201', - 'Haux (64470)' => '64258', - 'Hélette (64640)' => '64259', - 'Hendaye (64700)' => '64260', - 'Herm (40990)' => '40123', - 'Herré (40310)' => '40124', - 'Herrère (64680)' => '64261', - 'Heugas (40180)' => '40125', - 'Hiers-Brouage (17320)' => '17189', - 'Hiersac (16290)' => '16163', - 'Hiesse (16490)' => '16164', - 'Higuères-Souye (64160)' => '64262', - 'Hinx (40180)' => '40126', - 'Hontanx (40190)' => '40127', - 'Horsarrieu (40700)' => '40128', - 'Hosta (64120)' => '64265', - 'Hostens (33125)' => '33202', - 'Houeillès (47420)' => '47119', - 'Houlette (16200)' => '16165', - 'Hours (64420)' => '64266', - 'Hourtin (33990)' => '33203', - 'Hure (33190)' => '33204', - 'Ibarrolle (64120)' => '64267', - 'Idaux-Mendy (64130)' => '64268', - 'Idron (64320)' => '64269', - 'Igon (64800)' => '64270', - 'Iholdy (64640)' => '64271', - 'Île-d\'Aix (17123)' => '17004', - 'Ilharre (64120)' => '64272', - 'Illats (33720)' => '33205', - 'Ingrandes (86220)' => '86111', - 'Irais (79600)' => '79141', - 'Irissarry (64780)' => '64273', - 'Irouléguy (64220)' => '64274', - 'Isle (87170)' => '87075', - 'Isle-Saint-Georges (33640)' => '33206', - 'Ispoure (64220)' => '64275', - 'Issac (24400)' => '24211', - 'Issigeac (24560)' => '24212', - 'Issor (64570)' => '64276', - 'Issoudun-Létrieix (23130)' => '23097', - 'Isturits (64240)' => '64277', - 'Iteuil (86240)' => '86113', - 'Itxassou (64250)' => '64279', - 'Izeste (64260)' => '64280', - 'Izon (33450)' => '33207', - 'Jabreilles-les-Bordes (87370)' => '87076', - 'Jalesches (23270)' => '23098', - 'Janailhac (87800)' => '87077', - 'Janaillat (23250)' => '23099', - 'Jardres (86800)' => '86114', - 'Jarnac (16200)' => '16167', - 'Jarnac-Champagne (17520)' => '17192', - 'Jarnages (23140)' => '23100', - 'Jasses (64190)' => '64281', - 'Jatxou (64480)' => '64282', - 'Jau-Dignac-et-Loirac (33590)' => '33208', - 'Jauldes (16560)' => '16168', - 'Jaunay-Clan (86130)' => '86115', - 'Jaure (24140)' => '24213', - 'Javerdat (87520)' => '87078', - 'Javerlhac-et-la-Chapelle-Saint-Robert (24300)' => '24214', - 'Javrezac (16100)' => '16169', - 'Jaxu (64220)' => '64283', - 'Jayac (24590)' => '24215', - 'Jazeneuil (86600)' => '86116', - 'Jazennes (17260)' => '17196', - 'Jonzac (17500)' => '17197', - 'Josse (40230)' => '40129', - 'Jouac (87890)' => '87080', - 'Jouhet (86500)' => '86117', - 'Jouillat (23220)' => '23101', - 'Jourgnac (87800)' => '87081', - 'Journet (86290)' => '86118', - 'Journiac (24260)' => '24217', - 'Joussé (86350)' => '86119', - 'Jugazan (33420)' => '33209', - 'Jugeals-Nazareth (19500)' => '19093', - 'Juicq (17770)' => '17198', - 'Juignac (16190)' => '16170', - 'Juillac (19350)' => '19094', - 'Juillac (33890)' => '33210', - 'Juillac-le-Coq (16130)' => '16171', - 'Juillé (16230)' => '16173', - 'Juillé (79170)' => '79142', - 'Julienne (16200)' => '16174', - 'Jumilhac-le-Grand (24630)' => '24218', - 'Jurançon (64110)' => '64284', - 'Juscorps (79230)' => '79144', - 'Jusix (47180)' => '47120', - 'Jussas (17130)' => '17199', - 'Juxue (64120)' => '64285', - 'L\'Absie (79240)' => '79001', - 'L\'Église-aux-Bois (19170)' => '19074', - 'L\'Éguille (17600)' => '17151', - 'L\'Hôpital-d\'Orion (64270)' => '64263', - 'L\'Hôpital-Saint-Blaise (64130)' => '64264', - 'L\'Houmeau (17137)' => '17190', - 'L\'Isle-d\'Espagnac (16340)' => '16166', - 'L\'Isle-Jourdain (86150)' => '86112', - 'La Bachellerie (24210)' => '24020', - 'La Barde (17360)' => '17033', - 'La Bastide-Clairence (64240)' => '64289', - 'La Bataille (79110)' => '79027', - 'La Bazeuge (87210)' => '87008', - 'La Boissière-d\'Ans (24640)' => '24047', - 'La Boissière-en-Gâtine (79310)' => '79040', - 'La Brède (33650)' => '33213', - 'La Brée-les-Bains (17840)' => '17486', - 'La Brionne (23000)' => '23033', - 'La Brousse (17160)' => '17071', - 'La Bussière (86310)' => '86040', - 'La Cassagne (24120)' => '24085', - 'La Celle-Dunoise (23800)' => '23039', - 'La Celle-sous-Gouzon (23230)' => '23040', - 'La Cellette (23350)' => '23041', - 'La Chapelle (16140)' => '16081', - 'La Chapelle-Aubareil (24290)' => '24106', - 'La Chapelle-aux-Brocs (19360)' => '19043', - 'La Chapelle-aux-Saints (19120)' => '19044', - 'La Chapelle-Baloue (23160)' => '23050', - 'La Chapelle-Bâton (79220)' => '79070', - 'La Chapelle-Bâton (86250)' => '86055', - 'La Chapelle-Bertrand (79200)' => '79071', - 'La Chapelle-des-Pots (17100)' => '17089', - 'La Chapelle-Faucher (24530)' => '24107', - 'La Chapelle-Gonaguet (24350)' => '24108', - 'La Chapelle-Grésignac (24320)' => '24109', - 'La Chapelle-Montabourlet (24320)' => '24110', - 'La Chapelle-Montbrandeix (87440)' => '87037', - 'La Chapelle-Montmoreau (24300)' => '24111', - 'La Chapelle-Montreuil (86470)' => '86056', - 'La Chapelle-Moulière (86210)' => '86058', - 'La Chapelle-Pouilloux (79190)' => '79074', - 'La Chapelle-Saint-Étienne (79240)' => '79075', - 'La Chapelle-Saint-Géraud (19430)' => '19045', - 'La Chapelle-Saint-Jean (24390)' => '24113', - 'La Chapelle-Saint-Laurent (79430)' => '79076', - 'La Chapelle-Saint-Martial (23250)' => '23051', - 'La Chapelle-Taillefert (23000)' => '23052', - 'La Chapelle-Thireuil (79160)' => '79077', - 'La Chaussade (23200)' => '23059', - 'La Chaussée (86330)' => '86069', - 'La Chèvrerie (16240)' => '16098', - 'La Clisse (17600)' => '17112', - 'La Clotte (17360)' => '17113', - 'La Coquille (24450)' => '24133', - 'La Couarde (79800)' => '79098', - 'La Couarde-sur-Mer (17670)' => '17121', - 'La Couronne (16400)' => '16113', - 'La Courtine (23100)' => '23067', - 'La Crèche (79260)' => '79048', - 'La Croisille-sur-Briance (87130)' => '87051', - 'La Croix-Blanche (47340)' => '47075', - 'La Croix-Comtesse (17330)' => '17137', - 'La Croix-sur-Gartempe (87210)' => '87052', - 'La Dornac (24120)' => '24153', - 'La Douze (24330)' => '24156', - 'La Faye (16700)' => '16136', - 'La Ferrière-Airoux (86160)' => '86097', - 'La Ferrière-en-Parthenay (79390)' => '79120', - 'La Feuillade (24120)' => '24179', - 'La Flotte (17630)' => '17161', - 'La Force (24130)' => '24222', - 'La Forêt-de-Tessé (16240)' => '16142', - 'La Forêt-du-Temple (23360)' => '23084', - 'La Forêt-sur-Sèvre (79380)' => '79123', - 'La Foye-Monjault (79360)' => '79127', - 'La Frédière (17770)' => '17169', - 'La Genétouze (17360)' => '17173', - 'La Geneytouse (87400)' => '87070', - 'La Gonterie-Boulouneix (24310)' => '24198', - 'La Grève-sur-Mignon (17170)' => '17182', - 'La Grimaudière (86330)' => '86108', - 'La Gripperie-Saint-Symphorien (17620)' => '17184', - 'La Jard (17460)' => '17191', - 'La Jarne (17220)' => '17193', - 'La Jarrie (17220)' => '17194', - 'La Jarrie-Audouin (17330)' => '17195', - 'La Jemaye (24410)' => '24216', - 'La Jonchère-Saint-Maurice (87340)' => '87079', - 'La Laigne (17170)' => '17201', - 'La Lande-de-Fronsac (33240)' => '33219', - 'La Magdeleine (16240)' => '16197', - 'La Mazière-aux-Bons-Hommes (23260)' => '23129', - 'La Meyze (87800)' => '87096', - 'La Mothe-Saint-Héray (79800)' => '79184', - 'La Nouaille (23500)' => '23144', - 'La Péruse (16270)' => '16259', - 'La Petite-Boissière (79700)' => '79207', - 'La Peyratte (79200)' => '79208', - 'La Porcherie (87380)' => '87120', - 'La Pouge (23250)' => '23157', - 'La Puye (86260)' => '86202', - 'La Réole (33190)' => '33352', - 'La Réunion (47700)' => '47222', - 'La Rivière (33126)' => '33356', - 'La Roche-Canillac (19320)' => '19174', - 'La Roche-Chalais (24490)' => '24354', - 'La Roche-l\'Abeille (87800)' => '87127', - 'La Roche-Posay (86270)' => '86207', - 'La Roche-Rigault (86200)' => '86079', - 'La Rochebeaucourt-et-Argentine (24340)' => '24353', - 'La Rochefoucauld (16110)' => '16281', - 'La Rochelle (17000)' => '17300', - 'La Rochénard (79270)' => '79229', - 'La Rochette (16110)' => '16282', - 'La Ronde (17170)' => '17303', - 'La Roque-Gageac (24250)' => '24355', - 'La Roquille (33220)' => '33360', - 'La Saunière (23000)' => '23169', - 'La Sauve (33670)' => '33505', - 'La Sauvetat-de-Savères (47270)' => '47289', - 'La Sauvetat-du-Dropt (47800)' => '47290', - 'La Sauvetat-sur-Lède (47150)' => '47291', - 'La Serre-Bussière-Vieille (23190)' => '23172', - 'La Souterraine (23300)' => '23176', - 'La Tâche (16260)' => '16377', - 'La Teste-de-Buch (33260)' => '33529', - 'La Tour-Blanche (24320)' => '24554', - 'La Tremblade (17390)' => '17452', - 'La Trimouille (86290)' => '86273', - 'La Vallée (17250)' => '17455', - 'La Vergne (17400)' => '17465', - 'La Villedieu (17470)' => '17471', - 'La Villedieu (23340)' => '23264', - 'La Villedieu-du-Clain (86340)' => '86290', - 'La Villeneuve (23260)' => '23265', - 'La Villetelle (23260)' => '23266', - 'Laà-Mondrans (64300)' => '64286', - 'Laàs (64390)' => '64287', - 'Labarde (33460)' => '33211', - 'Labastide-Castel-Amouroux (47250)' => '47121', - 'Labastide-Cézéracq (64170)' => '64288', - 'Labastide-Chalosse (40700)' => '40130', - 'Labastide-d\'Armagnac (40240)' => '40131', - 'Labastide-Monréjeau (64170)' => '64290', - 'Labastide-Villefranche (64270)' => '64291', - 'Labatmale (64530)' => '64292', - 'Labatut (40300)' => '40132', - 'Labatut (64460)' => '64293', - 'Labenne (40530)' => '40133', - 'Labescau (33690)' => '33212', - 'Labets-Biscay (64120)' => '64294', - 'Labeyrie (64300)' => '64295', - 'Labouheyre (40210)' => '40134', - 'Labretonie (47350)' => '47122', - 'Labrit (40420)' => '40135', - 'Lacadée (64300)' => '64296', - 'Lacajunte (40320)' => '40136', - 'Lacanau (33680)' => '33214', - 'Lacapelle-Biron (47150)' => '47123', - 'Lacarre (64220)' => '64297', - 'Lacarry-Arhan-Charritte-de-Haut (64470)' => '64298', - 'Lacaussade (47150)' => '47124', - 'Lacelle (19170)' => '19095', - 'Lacépède (47360)' => '47125', - 'Lachaise (16300)' => '16176', - 'Lachapelle (47350)' => '47126', - 'Lacommande (64360)' => '64299', - 'Lacq (64170)' => '64300', - 'Lacquy (40120)' => '40137', - 'Lacrabe (40700)' => '40138', - 'Lacropte (24380)' => '24220', - 'Ladapeyre (23270)' => '23102', - 'Ladaux (33760)' => '33215', - 'Ladignac-le-Long (87500)' => '87082', - 'Ladignac-sur-Rondelles (19150)' => '19096', - 'Ladiville (16120)' => '16177', - 'Lados (33124)' => '33216', - 'Lafage-sur-Sombre (19320)' => '19097', - 'Lafat (23800)' => '23103', - 'Lafitte-sur-Lot (47320)' => '47127', - 'Lafox (47240)' => '47128', - 'Lagarde-Enval (19150)' => '19098', - 'Lagarde-sur-le-Né (16300)' => '16178', - 'Lagarrigue (47190)' => '47129', - 'Lageon (79200)' => '79145', - 'Lagleygeolle (19500)' => '19099', - 'Laglorieuse (40090)' => '40139', - 'Lagor (64150)' => '64301', - 'Lagorce (33230)' => '33218', - 'Lagord (17140)' => '17200', - 'Lagos (64800)' => '64302', - 'Lagrange (40240)' => '40140', - 'Lagraulière (19700)' => '19100', - 'Lagruère (47400)' => '47130', - 'Laguenne (19150)' => '19101', - 'Laguinge-Restoue (64470)' => '64303', - 'Lagupie (47180)' => '47131', - 'Lahonce (64990)' => '64304', - 'Lahontan (64270)' => '64305', - 'Lahosse (40250)' => '40141', - 'Lahourcade (64150)' => '64306', - 'Lalande-de-Pomerol (33500)' => '33222', - 'Lalandusse (47330)' => '47132', - 'Lalinde (24150)' => '24223', - 'Lalongue (64350)' => '64307', - 'Lalonquette (64450)' => '64308', - 'Laluque (40465)' => '40142', - 'Lamarque (33460)' => '33220', - 'Lamayou (64460)' => '64309', - 'Lamazière-Basse (19160)' => '19102', - 'Lamazière-Haute (19340)' => '19103', - 'Lamongerie (19510)' => '19104', - 'Lamontjoie (47310)' => '47133', - 'Lamonzie-Montastruc (24520)' => '24224', - 'Lamonzie-Saint-Martin (24680)' => '24225', - 'Lamothe (40250)' => '40143', - 'Lamothe-Landerron (33190)' => '33221', - 'Lamothe-Montravel (24230)' => '24226', - 'Landerrouat (33790)' => '33223', - 'Landerrouet-sur-Ségur (33540)' => '33224', - 'Landes (17380)' => '17202', - 'Landiras (33720)' => '33225', - 'Landrais (17290)' => '17203', - 'Langoiran (33550)' => '33226', - 'Langon (33210)' => '33227', - 'Lanne-en-Barétous (64570)' => '64310', - 'Lannecaube (64350)' => '64311', - 'Lanneplaà (64300)' => '64312', - 'Lannes (47170)' => '47134', - 'Lanouaille (24270)' => '24227', - 'Lanquais (24150)' => '24228', - 'Lansac (33710)' => '33228', - 'Lantabat (64640)' => '64313', - 'Lanteuil (19190)' => '19105', - 'Lanton (33138)' => '33229', - 'Laparade (47260)' => '47135', - 'Laperche (47800)' => '47136', - 'Lapleau (19550)' => '19106', - 'Laplume (47310)' => '47137', - 'Lapouyade (33620)' => '33230', - 'Laprade (16390)' => '16180', - 'Larbey (40250)' => '40144', - 'Larceveau-Arros-Cibits (64120)' => '64314', - 'Larche (19600)' => '19107', - 'Largeasse (79240)' => '79147', - 'Laroche-près-Feyt (19340)' => '19108', - 'Laroin (64110)' => '64315', - 'Laroque (33410)' => '33231', - 'Laroque-Timbaut (47340)' => '47138', - 'Larrau (64560)' => '64316', - 'Larressore (64480)' => '64317', - 'Larreule (64410)' => '64318', - 'Larribar-Sorhapuru (64120)' => '64319', - 'Larrivière-Saint-Savin (40270)' => '40145', - 'Lartigue (33840)' => '33232', - 'Laruns (64440)' => '64320', - 'Laruscade (33620)' => '33233', - 'Larzac (24170)' => '24230', - 'Lascaux (19130)' => '19109', - 'Lasclaveries (64450)' => '64321', - 'Lasse (64220)' => '64322', - 'Lasserre (47600)' => '47139', - 'Lasserre (64350)' => '64323', - 'Lasseube (64290)' => '64324', - 'Lasseubetat (64290)' => '64325', - 'Lathus-Saint-Rémy (86390)' => '86120', - 'Latillé (86190)' => '86121', - 'Latresne (33360)' => '33234', - 'Latrille (40800)' => '40146', - 'Latronche (19160)' => '19110', - 'Laugnac (47360)' => '47140', - 'Laurède (40250)' => '40147', - 'Lauret (40320)' => '40148', - 'Laurière (87370)' => '87083', - 'Laussou (47150)' => '47141', - 'Lauthiers (86300)' => '86122', - 'Lauzun (47410)' => '47142', - 'Laval-sur-Luzège (19550)' => '19111', - 'Lavalade (24540)' => '24231', - 'Lavardac (47230)' => '47143', - 'Lavaufranche (23600)' => '23104', - 'Lavaur (24550)' => '24232', - 'Lavausseau (86470)' => '86123', - 'Lavaveix-les-Mines (23150)' => '23105', - 'Lavazan (33690)' => '33235', - 'Lavergne (47800)' => '47144', - 'Laveyssière (24130)' => '24233', - 'Lavignac (87230)' => '87084', - 'Lavoux (86800)' => '86124', - 'Lay-Lamidou (64190)' => '64326', - 'Layrac (47390)' => '47145', - 'Le Barp (33114)' => '33029', - 'Le Beugnon (79130)' => '79035', - 'Le Bois-Plage-en-Ré (17580)' => '17051', - 'Le Bouchage (16350)' => '16054', - 'Le Bourdeix (24300)' => '24056', - 'Le Bourdet (79210)' => '79046', - 'Le Bourg-d\'Hem (23220)' => '23029', - 'Le Bouscat (33110)' => '33069', - 'Le Breuil-Bernard (79320)' => '79051', - 'Le Bugue (24260)' => '24067', - 'Le Buis (87140)' => '87023', - 'Le Buisson-de-Cadouin (24480)' => '24068', - 'Le Busseau (79240)' => '79059', - 'Le Chalard (87500)' => '87031', - 'Le Change (24640)' => '24103', - 'Le Chastang (19190)' => '19048', - 'Le Château-d\'Oléron (17480)' => '17093', - 'Le Châtenet-en-Dognon (87400)' => '87042', - 'Le Chauchet (23130)' => '23058', - 'Le Chay (17600)' => '17097', - 'Le Chillou (79600)' => '79089', - 'Le Compas (23700)' => '23066', - 'Le Donzeil (23480)' => '23074', - 'Le Dorat (87210)' => '87059', - 'Le Douhet (17100)' => '17143', - 'Le Fieu (33230)' => '33166', - 'Le Fleix (24130)' => '24182', - 'Le Fouilloux (17270)' => '17167', - 'Le Frêche (40190)' => '40100', - 'Le Gicq (17160)' => '17177', - 'Le Grand-Bourg (23240)' => '23095', - 'Le Grand-Madieu (16450)' => '16157', - 'Le Grand-Village-Plage (17370)' => '17485', - 'Le Gua (17600)' => '17185', - 'Le Gué-d\'Alleré (17540)' => '17186', - 'Le Haillan (33185)' => '33200', - 'Le Jardin (19300)' => '19092', - 'Le Lardin-Saint-Lazare (24570)' => '24229', - 'Le Leuy (40250)' => '40153', - 'Le Lindois (16310)' => '16188', - 'Le Lonzac (19470)' => '19118', - 'Le Mas-d\'Agenais (47430)' => '47159', - 'Le Mas-d\'Artige (23100)' => '23125', - 'Le Monteil-au-Vicomte (23460)' => '23134', - 'Le Mung (17350)' => '17252', - 'Le Nizan (33430)' => '33305', - 'Le Palais-sur-Vienne (87410)' => '87113', - 'Le Passage (47520)' => '47201', - 'Le Pescher (19190)' => '19163', - 'Le Pian-Médoc (33290)' => '33322', - 'Le Pian-sur-Garonne (33490)' => '33323', - 'Le Pin (17210)' => '17276', - 'Le Pin (79140)' => '79210', - 'Le Pizou (24700)' => '24329', - 'Le Porge (33680)' => '33333', - 'Le Pout (33670)' => '33335', - 'Le Puy (33580)' => '33345', - 'Le Retail (79130)' => '79226', - 'Le Rochereau (86170)' => '86208', - 'Le Sen (40420)' => '40297', - 'Le Seure (17770)' => '17426', - 'Le Taillan-Médoc (33320)' => '33519', - 'Le Tallud (79200)' => '79322', - 'Le Tâtre (16360)' => '16380', - 'Le Teich (33470)' => '33527', - 'Le Temple (33680)' => '33528', - 'Le Temple-sur-Lot (47110)' => '47306', - 'Le Thou (17290)' => '17447', - 'Le Tourne (33550)' => '33534', - 'Le Tuzan (33125)' => '33536', - 'Le Vanneau-Irleau (79270)' => '79337', - 'Le Verdon-sur-Mer (33123)' => '33544', - 'Le Vert (79170)' => '79346', - 'Le Vieux-Cérier (16350)' => '16403', - 'Le Vigeant (86150)' => '86289', - 'Le Vigen (87110)' => '87205', - 'Le Vignau (40270)' => '40329', - 'Lecumberry (64220)' => '64327', - 'Lédat (47300)' => '47146', - 'Ledeuix (64400)' => '64328', - 'Lée (64320)' => '64329', - 'Lées-Athas (64490)' => '64330', - 'Lège-Cap-Ferret (33950)' => '33236', - 'Léguillac-de-Cercles (24340)' => '24235', - 'Léguillac-de-l\'Auche (24110)' => '24236', - 'Leigné-les-Bois (86450)' => '86125', - 'Leigné-sur-Usseau (86230)' => '86127', - 'Leignes-sur-Fontaine (86300)' => '86126', - 'Lembeye (64350)' => '64331', - 'Lembras (24100)' => '24237', - 'Lème (64450)' => '64332', - 'Lempzours (24800)' => '24238', - 'Lencloître (86140)' => '86128', - 'Lencouacq (40120)' => '40149', - 'Léogeats (33210)' => '33237', - 'Léognan (33850)' => '33238', - 'Léon (40550)' => '40150', - 'Léoville (17500)' => '17204', - 'Lépaud (23170)' => '23106', - 'Lépinas (23150)' => '23107', - 'Léren (64270)' => '64334', - 'Lerm-et-Musset (33840)' => '33239', - 'Les Adjots (16700)' => '16002', - 'Les Alleuds (79190)' => '79006', - 'Les Angles-sur-Corrèze (19000)' => '19009', - 'Les Artigues-de-Lussac (33570)' => '33014', - 'Les Billanges (87340)' => '87016', - 'Les Billaux (33500)' => '33052', - 'Les Cars (87230)' => '87029', - 'Les Éduts (17510)' => '17149', - 'Les Églises-d\'Argenteuil (17400)' => '17150', - 'Les Églisottes-et-Chalaures (33230)' => '33154', - 'Les Essards (16210)' => '16130', - 'Les Essards (17250)' => '17154', - 'Les Esseintes (33190)' => '33158', - 'Les Eyzies-de-Tayac-Sireuil (24620)' => '24172', - 'Les Farges (24290)' => '24175', - 'Les Forges (79340)' => '79124', - 'Les Fosses (79360)' => '79126', - 'Les Gonds (17100)' => '17179', - 'Les Gours (16140)' => '16155', - 'Les Grands-Chézeaux (87160)' => '87074', - 'Les Graulges (24340)' => '24203', - 'Les Groseillers (79220)' => '79139', - 'Les Lèches (24400)' => '24234', - 'Les Lèves-et-Thoumeyragues (33220)' => '33242', - 'Les Mars (23700)' => '23123', - 'Les Mathes (17570)' => '17225', - 'Les Métairies (16200)' => '16220', - 'Les Nouillers (17380)' => '17266', - 'Les Ormes (86220)' => '86183', - 'Les Peintures (33230)' => '33315', - 'Les Pins (16260)' => '16261', - 'Les Portes-en-Ré (17880)' => '17286', - 'Les Salles-de-Castillon (33350)' => '33499', - 'Les Salles-Lavauguyon (87440)' => '87189', - 'Les Touches-de-Périgny (17160)' => '17451', - 'Les Trois-Moutiers (86120)' => '86274', - 'Lescar (64230)' => '64335', - 'Lescun (64490)' => '64336', - 'Lesgor (40400)' => '40151', - 'Lésignac-Durand (16310)' => '16183', - 'Lésigny (86270)' => '86129', - 'Lesparre-Médoc (33340)' => '33240', - 'Lesperon (40260)' => '40152', - 'Lespielle (64350)' => '64337', - 'Lespourcy (64160)' => '64338', - 'Lessac (16500)' => '16181', - 'Lestards (19170)' => '19112', - 'Lestelle-Bétharram (64800)' => '64339', - 'Lesterps (16420)' => '16182', - 'Lestiac-sur-Garonne (33550)' => '33241', - 'Leugny (86220)' => '86130', - 'Lévignac-de-Guyenne (47120)' => '47147', - 'Lévignacq (40170)' => '40154', - 'Leyrat (23600)' => '23108', - 'Leyritz-Moncassin (47700)' => '47148', - 'Lezay (79120)' => '79148', - 'Lhommaizé (86410)' => '86131', - 'Lhoumois (79390)' => '79149', - 'Libourne (33500)' => '33243', - 'Lichans-Sunhar (64470)' => '64340', - 'Lichères (16460)' => '16184', - 'Lichos (64130)' => '64341', - 'Licq-Athérey (64560)' => '64342', - 'Liginiac (19160)' => '19113', - 'Liglet (86290)' => '86132', - 'Lignan-de-Bazas (33430)' => '33244', - 'Lignan-de-Bordeaux (33360)' => '33245', - 'Lignareix (19200)' => '19114', - 'Ligné (16140)' => '16185', - 'Ligneyrac (19500)' => '19115', - 'Lignières-Sonneville (16130)' => '16186', - 'Ligueux (33220)' => '33246', - 'Ligugé (86240)' => '86133', - 'Limalonges (79190)' => '79150', - 'Limendous (64420)' => '64343', - 'Limeuil (24510)' => '24240', - 'Limeyrat (24210)' => '24241', - 'Limoges (87000)' => '87085', - 'Linard (23220)' => '23109', - 'Linards (87130)' => '87086', - 'Linars (16730)' => '16187', - 'Linazay (86400)' => '86134', - 'Liniers (86800)' => '86135', - 'Linxe (40260)' => '40155', - 'Liorac-sur-Louyre (24520)' => '24242', - 'Liourdres (19120)' => '19116', - 'Lioux-les-Monges (23700)' => '23110', - 'Liposthey (40410)' => '40156', - 'Lisle (24350)' => '24243', - 'Lissac-sur-Couze (19600)' => '19117', - 'Listrac-de-Durèze (33790)' => '33247', - 'Listrac-Médoc (33480)' => '33248', - 'Lit-et-Mixe (40170)' => '40157', - 'Livron (64530)' => '64344', - 'Lizant (86400)' => '86136', - 'Lizières (23240)' => '23111', - 'Lohitzun-Oyhercq (64120)' => '64345', - 'Loire-les-Marais (17870)' => '17205', - 'Loiré-sur-Nie (17470)' => '17206', - 'Loix (17111)' => '17207', - 'Lolme (24540)' => '24244', - 'Lombia (64160)' => '64346', - 'Lonçon (64410)' => '64347', - 'Londigny (16700)' => '16189', - 'Longèves (17230)' => '17208', - 'Longré (16240)' => '16190', - 'Longueville (47200)' => '47150', - 'Lonnes (16230)' => '16191', - 'Lons (64140)' => '64348', - 'Lonzac (17520)' => '17209', - 'Lorignac (17240)' => '17210', - 'Lorigné (79190)' => '79152', - 'Lormont (33310)' => '33249', - 'Losse (40240)' => '40158', - 'Lostanges (19500)' => '19119', - 'Loubejac (24550)' => '24245', - 'Loubens (33190)' => '33250', - 'Loubès-Bernac (47120)' => '47151', - 'Loubieng (64300)' => '64349', - 'Loubigné (79110)' => '79153', - 'Loubillé (79110)' => '79154', - 'Louchats (33125)' => '33251', - 'Loudun (86200)' => '86137', - 'Louer (40380)' => '40159', - 'Lougratte (47290)' => '47152', - 'Louhossoa (64250)' => '64350', - 'Louignac (19310)' => '19120', - 'Louin (79600)' => '79156', - 'Loulay (17330)' => '17211', - 'Loupes (33370)' => '33252', - 'Loupiac (33410)' => '33253', - 'Loupiac-de-la-Réole (33190)' => '33254', - 'Lourdios-Ichère (64570)' => '64351', - 'Lourdoueix-Saint-Pierre (23360)' => '23112', - 'Lourenties (64420)' => '64352', - 'Lourquen (40250)' => '40160', - 'Louvie-Juzon (64260)' => '64353', - 'Louvie-Soubiron (64440)' => '64354', - 'Louvigny (64410)' => '64355', - 'Louzac-Saint-André (16100)' => '16193', - 'Louzignac (17160)' => '17212', - 'Louzy (79100)' => '79157', - 'Lozay (17330)' => '17213', - 'Lubbon (40240)' => '40161', - 'Lubersac (19210)' => '19121', - 'Luc-Armau (64350)' => '64356', - 'Lucarré (64350)' => '64357', - 'Lucbardez-et-Bargues (40090)' => '40162', - 'Lucgarier (64420)' => '64358', - 'Luchapt (86430)' => '86138', - 'Luchat (17600)' => '17214', - 'Luché-sur-Brioux (79170)' => '79158', - 'Luché-Thouarsais (79330)' => '79159', - 'Lucmau (33840)' => '33255', - 'Lucq-de-Béarn (64360)' => '64359', - 'Ludon-Médoc (33290)' => '33256', - 'Lüe (40210)' => '40163', - 'Lugaignac (33420)' => '33257', - 'Lugasson (33760)' => '33258', - 'Luglon (40630)' => '40165', - 'Lugon-et-l\'Île-du-Carnay (33240)' => '33259', - 'Lugos (33830)' => '33260', - 'Lunas (24130)' => '24246', - 'Lupersat (23190)' => '23113', - 'Lupsault (16140)' => '16194', - 'Luquet (65320)' => '65292', - 'Lurbe-Saint-Christau (64660)' => '64360', - 'Lusignac (24320)' => '24247', - 'Lusignan (86600)' => '86139', - 'Lusignan-Petit (47360)' => '47154', - 'Lussac (16450)' => '16195', - 'Lussac (17500)' => '17215', - 'Lussac (33570)' => '33261', - 'Lussac-les-Châteaux (86320)' => '86140', - 'Lussac-les-Églises (87360)' => '87087', - 'Lussagnet (40270)' => '40166', - 'Lussagnet-Lusson (64160)' => '64361', - 'Lussant (17430)' => '17216', - 'Lussas-et-Nontronneau (24300)' => '24248', - 'Lussat (23170)' => '23114', - 'Lusseray (79170)' => '79160', - 'Luxé (16230)' => '16196', - 'Luxe-Sumberraute (64120)' => '64362', - 'Luxey (40430)' => '40167', - 'Luzay (79100)' => '79161', - 'Lys (64260)' => '64363', - 'Macau (33460)' => '33262', - 'Macaye (64240)' => '64364', - 'Macqueville (17490)' => '17217', - 'Madaillan (47360)' => '47155', - 'Madirac (33670)' => '33263', - 'Madranges (19470)' => '19122', - 'Magescq (40140)' => '40168', - 'Magnac-Bourg (87380)' => '87088', - 'Magnac-Laval (87190)' => '87089', - 'Magnac-Lavalette-Villars (16320)' => '16198', - 'Magnac-sur-Touvre (16600)' => '16199', - 'Magnat-l\'Étrange (23260)' => '23115', - 'Magné (79460)' => '79162', - 'Magné (86160)' => '86141', - 'Mailhac-sur-Benaize (87160)' => '87090', - 'Maillas (40120)' => '40169', - 'Maillé (86190)' => '86142', - 'Maillères (40120)' => '40170', - 'Maine-de-Boixe (16230)' => '16200', - 'Mainsat (23700)' => '23116', - 'Mainxe (16200)' => '16202', - 'Mainzac (16380)' => '16203', - 'Mairé (86270)' => '86143', - 'Mairé-Levescault (79190)' => '79163', - 'Maison-Feyne (23800)' => '23117', - 'Maisonnais-sur-Tardoire (87440)' => '87091', - 'Maisonnay (79500)' => '79164', - 'Maisonneuve (86170)' => '86144', - 'Maisonnisses (23150)' => '23118', - 'Maisontiers (79600)' => '79165', - 'Malaussanne (64410)' => '64365', - 'Malaville (16120)' => '16204', - 'Malemort (19360)' => '19123', - 'Malleret (23260)' => '23119', - 'Malleret-Boussac (23600)' => '23120', - 'Malval (23220)' => '23121', - 'Manaurie (24620)' => '24249', - 'Mano (40410)' => '40171', - 'Manot (16500)' => '16205', - 'Mansac (19520)' => '19124', - 'Mansat-la-Courrière (23400)' => '23122', - 'Mansle (16230)' => '16206', - 'Mant (40700)' => '40172', - 'Manzac-sur-Vern (24110)' => '24251', - 'Marans (17230)' => '17218', - 'Maransin (33230)' => '33264', - 'Marc-la-Tour (19150)' => '19127', - 'Marçay (86370)' => '86145', - 'Marcellus (47200)' => '47156', - 'Marcenais (33620)' => '33266', - 'Marcheprime (33380)' => '33555', - 'Marcillac (33860)' => '33267', - 'Marcillac-la-Croisille (19320)' => '19125', - 'Marcillac-la-Croze (19500)' => '19126', - 'Marcillac-Lanville (16140)' => '16207', - 'Marcillac-Saint-Quentin (24200)' => '24252', - 'Marennes (17320)' => '17219', - 'Mareuil (16170)' => '16208', - 'Mareuil (24340)' => '24253', - 'Margaux (33460)' => '33268', - 'Margerides (19200)' => '19128', - 'Margueron (33220)' => '33269', - 'Marignac (17800)' => '17220', - 'Marigny (79360)' => '79166', - 'Marigny-Brizay (86380)' => '86146', - 'Marigny-Chemereau (86370)' => '86147', - 'Marillac-le-Franc (16110)' => '16209', - 'Marimbault (33430)' => '33270', - 'Marions (33690)' => '33271', - 'Marmande (47200)' => '47157', - 'Marmont-Pachas (47220)' => '47158', - 'Marnac (24220)' => '24254', - 'Marnay (86160)' => '86148', - 'Marnes (79600)' => '79167', - 'Marpaps (40330)' => '40173', - 'Marquay (24620)' => '24255', - 'Marsac (16570)' => '16210', - 'Marsac (23210)' => '23124', - 'Marsac-sur-l\'Isle (24430)' => '24256', - 'Marsais (17700)' => '17221', - 'Marsalès (24540)' => '24257', - 'Marsaneix (24750)' => '24258', - 'Marsas (33620)' => '33272', - 'Marsilly (17137)' => '17222', - 'Martaizé (86330)' => '86149', - 'Marthon (16380)' => '16211', - 'Martignas-sur-Jalle (33127)' => '33273', - 'Martillac (33650)' => '33274', - 'Martres (33760)' => '33275', - 'Marval (87440)' => '87092', - 'Masbaraud-Mérignat (23400)' => '23126', - 'Mascaraàs-Haron (64330)' => '64366', - 'Maslacq (64300)' => '64367', - 'Masléon (87130)' => '87093', - 'Masparraute (64120)' => '64368', - 'Maspie-Lalonquère-Juillacq (64350)' => '64369', - 'Masquières (47370)' => '47160', - 'Massac (17490)' => '17223', - 'Massais (79150)' => '79168', - 'Masseilles (33690)' => '33276', - 'Massels (47140)' => '47161', - 'Masseret (19510)' => '19129', - 'Massignac (16310)' => '16212', - 'Massognes (86170)' => '86150', - 'Massoulès (47140)' => '47162', - 'Massugas (33790)' => '33277', - 'Matha (17160)' => '17224', - 'Maucor (64160)' => '64370', - 'Maulay (86200)' => '86151', - 'Mauléon (79700)' => '79079', - 'Mauléon-Licharre (64130)' => '64371', - 'Mauprévoir (86460)' => '86152', - 'Maure (64460)' => '64372', - 'Maurens (24140)' => '24259', - 'Mauriac (33540)' => '33278', - 'Mauries (40320)' => '40174', - 'Maurrin (40270)' => '40175', - 'Maussac (19250)' => '19130', - 'Mautes (23190)' => '23127', - 'Mauvezin-d\'Armagnac (40240)' => '40176', - 'Mauvezin-sur-Gupie (47200)' => '47163', - 'Mauzac-et-Grand-Castang (24150)' => '24260', - 'Mauzé-sur-le-Mignon (79210)' => '79170', - 'Mauzé-Thouarsais (79100)' => '79171', - 'Mauzens-et-Miremont (24260)' => '24261', - 'Mayac (24420)' => '24262', - 'Maylis (40250)' => '40177', - 'Mazeirat (23150)' => '23128', - 'Mazeray (17400)' => '17226', - 'Mazères (33210)' => '33279', - 'Mazères-Lezons (64110)' => '64373', - 'Mazerolles (16310)' => '16213', - 'Mazerolles (17800)' => '17227', - 'Mazerolles (40090)' => '40178', - 'Mazerolles (64230)' => '64374', - 'Mazerolles (86320)' => '86153', - 'Mazeuil (86110)' => '86154', - 'Mazeyrolles (24550)' => '24263', - 'Mazières (16270)' => '16214', - 'Mazières-en-Gâtine (79310)' => '79172', - 'Mazières-Naresse (47210)' => '47164', - 'Mazières-sur-Béronne (79500)' => '79173', - 'Mazion (33390)' => '33280', - 'Méasnes (23360)' => '23130', - 'Médillac (16210)' => '16215', - 'Médis (17600)' => '17228', - 'Mées (40990)' => '40179', - 'Méharin (64120)' => '64375', - 'Meilhac (87800)' => '87094', - 'Meilhan (40400)' => '40180', - 'Meilhan-sur-Garonne (47180)' => '47165', - 'Meilhards (19510)' => '19131', - 'Meillon (64510)' => '64376', - 'Melle (79500)' => '79174', - 'Melleran (79190)' => '79175', - 'Mendionde (64240)' => '64377', - 'Menditte (64130)' => '64378', - 'Mendive (64220)' => '64379', - 'Ménesplet (24700)' => '24264', - 'Ménigoute (79340)' => '79176', - 'Ménoire (19190)' => '19132', - 'Mensignac (24350)' => '24266', - 'Méracq (64410)' => '64380', - 'Mercoeur (19430)' => '19133', - 'Mérignac (16200)' => '16216', - 'Mérignac (17210)' => '17229', - 'Mérignac (33700)' => '33281', - 'Mérignas (33350)' => '33282', - 'Mérinchal (23420)' => '23131', - 'Méritein (64190)' => '64381', - 'Merlines (19340)' => '19134', - 'Merpins (16100)' => '16217', - 'Meschers-sur-Gironde (17132)' => '17230', - 'Mescoules (24240)' => '24267', - 'Mesnac (16370)' => '16218', - 'Mesplède (64370)' => '64382', - 'Messac (17130)' => '17231', - 'Messanges (40660)' => '40181', - 'Messé (79120)' => '79177', - 'Messemé (86200)' => '86156', - 'Mesterrieux (33540)' => '33283', - 'Mestes (19200)' => '19135', - 'Meursac (17120)' => '17232', - 'Meux (17500)' => '17233', - 'Meuzac (87380)' => '87095', - 'Meymac (19250)' => '19136', - 'Meyrals (24220)' => '24268', - 'Meyrignac-l\'Église (19800)' => '19137', - 'Meyssac (19500)' => '19138', - 'Mézin (47170)' => '47167', - 'Mézos (40170)' => '40182', - 'Mialet (24450)' => '24269', - 'Mialos (64410)' => '64383', - 'Mignaloux-Beauvoir (86550)' => '86157', - 'Migné-Auxances (86440)' => '86158', - 'Migré (17330)' => '17234', - 'Migron (17770)' => '17235', - 'Milhac-d\'Auberoche (24330)' => '24270', - 'Milhac-de-Nontron (24470)' => '24271', - 'Millac (86150)' => '86159', - 'Millevaches (19290)' => '19139', - 'Mimbaste (40350)' => '40183', - 'Mimizan (40200)' => '40184', - 'Minzac (24610)' => '24272', - 'Mios (33380)' => '33284', - 'Miossens-Lanusse (64450)' => '64385', - 'Mirambeau (17150)' => '17236', - 'Miramont-de-Guyenne (47800)' => '47168', - 'Miramont-Sensacq (40320)' => '40185', - 'Mirebeau (86110)' => '86160', - 'Mirepeix (64800)' => '64386', - 'Missé (79100)' => '79178', - 'Misson (40290)' => '40186', - 'Moëze (17780)' => '17237', - 'Moirax (47310)' => '47169', - 'Moissannes (87400)' => '87099', - 'Molières (24480)' => '24273', - 'Moliets-et-Maa (40660)' => '40187', - 'Momas (64230)' => '64387', - 'Mombrier (33710)' => '33285', - 'Momuy (40700)' => '40188', - 'Momy (64350)' => '64388', - 'Monassut-Audiracq (64160)' => '64389', - 'Monbahus (47290)' => '47170', - 'Monbalen (47340)' => '47171', - 'Monbazillac (24240)' => '24274', - 'Moncaup (64350)' => '64390', - 'Moncaut (47310)' => '47172', - 'Moncayolle-Larrory-Mendibieu (64130)' => '64391', - 'Monceaux-sur-Dordogne (19400)' => '19140', - 'Moncla (64330)' => '64392', - 'Monclar (47380)' => '47173', - 'Moncontour (86330)' => '86161', - 'Moncoutant (79320)' => '79179', - 'Moncrabeau (47600)' => '47174', - 'Mondion (86230)' => '86162', - 'Monein (64360)' => '64393', - 'Monestier (24240)' => '24276', - 'Monestier-Merlines (19340)' => '19141', - 'Monestier-Port-Dieu (19110)' => '19142', - 'Monfaucon (24130)' => '24277', - 'Monflanquin (47150)' => '47175', - 'Mongaillard (47230)' => '47176', - 'Mongauzy (33190)' => '33287', - 'Monget (40700)' => '40189', - 'Monheurt (47160)' => '47177', - 'Monmadalès (24560)' => '24278', - 'Monmarvès (24560)' => '24279', - 'Monpazier (24540)' => '24280', - 'Monpezat (64350)' => '64394', - 'Monplaisant (24170)' => '24293', - 'Monprimblanc (33410)' => '33288', - 'Mons (16140)' => '16221', - 'Mons (17160)' => '17239', - 'Monsac (24440)' => '24281', - 'Monsaguel (24560)' => '24282', - 'Monsec (24340)' => '24283', - 'Monségur (33580)' => '33289', - 'Monségur (40700)' => '40190', - 'Monségur (47150)' => '47178', - 'Monségur (64460)' => '64395', - 'Monsempron-Libos (47500)' => '47179', - 'Mont (64300)' => '64396', - 'Mont-de-Marsan (40000)' => '40192', - 'Mont-Disse (64330)' => '64401', - 'Montagnac-d\'Auberoche (24210)' => '24284', - 'Montagnac-la-Crempse (24140)' => '24285', - 'Montagnac-sur-Auvignon (47600)' => '47180', - 'Montagnac-sur-Lède (47150)' => '47181', - 'Montagne (33570)' => '33290', - 'Montagoudin (33190)' => '33291', - 'Montagrier (24350)' => '24286', - 'Montagut (64410)' => '64397', - 'Montaignac-Saint-Hippolyte (19300)' => '19143', - 'Montaigut-le-Blanc (23320)' => '23132', - 'Montalembert (79190)' => '79180', - 'Montamisé (86360)' => '86163', - 'Montaner (64460)' => '64398', - 'Montardon (64121)' => '64399', - 'Montastruc (47380)' => '47182', - 'Montauriol (47330)' => '47183', - 'Montaut (24560)' => '24287', - 'Montaut (40500)' => '40191', - 'Montaut (47210)' => '47184', - 'Montaut (64800)' => '64400', - 'Montayral (47500)' => '47185', - 'Montazeau (24230)' => '24288', - 'Montboucher (23400)' => '23133', - 'Montboyer (16620)' => '16222', - 'Montbron (16220)' => '16223', - 'Montcaret (24230)' => '24289', - 'Montégut (40190)' => '40193', - 'Montemboeuf (16310)' => '16225', - 'Montendre (17130)' => '17240', - 'Montesquieu (47130)' => '47186', - 'Monteton (47120)' => '47187', - 'Montferrand-du-Périgord (24440)' => '24290', - 'Montfort (64190)' => '64403', - 'Montfort-en-Chalosse (40380)' => '40194', - 'Montgaillard (40500)' => '40195', - 'Montgibaud (19210)' => '19144', - 'Montguyon (17270)' => '17241', - 'Monthoiron (86210)' => '86164', - 'Montignac (24290)' => '24291', - 'Montignac (33760)' => '33292', - 'Montignac-Charente (16330)' => '16226', - 'Montignac-de-Lauzun (47800)' => '47188', - 'Montignac-le-Coq (16390)' => '16227', - 'Montignac-Toupinerie (47350)' => '47189', - 'Montigné (16170)' => '16228', - 'Montils (17800)' => '17242', - 'Montjean (16240)' => '16229', - 'Montlieu-la-Garde (17210)' => '17243', - 'Montmérac (16300)' => '16224', - 'Montmoreau-Saint-Cybard (16190)' => '16230', - 'Montmorillon (86500)' => '86165', - 'Montory (64470)' => '64404', - 'Montpellier-de-Médillan (17260)' => '17244', - 'Montpeyroux (24610)' => '24292', - 'Montpezat (47360)' => '47190', - 'Montpon-Ménestérol (24700)' => '24294', - 'Montpouillan (47200)' => '47191', - 'Montravers (79140)' => '79183', - 'Montrem (24110)' => '24295', - 'Montreuil-Bonnin (86470)' => '86166', - 'Montrol-Sénard (87330)' => '87100', - 'Montrollet (16420)' => '16231', - 'Montroy (17220)' => '17245', - 'Monts-sur-Guesnes (86420)' => '86167', - 'Montsoué (40500)' => '40196', - 'Montussan (33450)' => '33293', - 'Monviel (47290)' => '47192', - 'Moragne (17430)' => '17246', - 'Morcenx (40110)' => '40197', - 'Morganx (40700)' => '40198', - 'Morizès (33190)' => '33294', - 'Morlaàs (64160)' => '64405', - 'Morlanne (64370)' => '64406', - 'Mornac (16600)' => '16232', - 'Mornac-sur-Seudre (17113)' => '17247', - 'Mortagne-sur-Gironde (17120)' => '17248', - 'Mortemart (87330)' => '87101', - 'Mortiers (17500)' => '17249', - 'Morton (86120)' => '86169', - 'Mortroux (23220)' => '23136', - 'Mosnac (16120)' => '16233', - 'Mosnac (17240)' => '17250', - 'Mougon (79370)' => '79185', - 'Mouguerre (64990)' => '64407', - 'Mouhous (64330)' => '64408', - 'Mouillac (33240)' => '33295', - 'Mouleydier (24520)' => '24296', - 'Moulidars (16290)' => '16234', - 'Mouliets-et-Villemartin (33350)' => '33296', - 'Moulin-Neuf (24700)' => '24297', - 'Moulinet (47290)' => '47193', - 'Moulis-en-Médoc (33480)' => '33297', - 'Moulismes (86500)' => '86170', - 'Moulon (33420)' => '33298', - 'Moumour (64400)' => '64409', - 'Mourens (33410)' => '33299', - 'Mourenx (64150)' => '64410', - 'Mourioux-Vieilleville (23210)' => '23137', - 'Mouscardès (40290)' => '40199', - 'Moussac (86150)' => '86171', - 'Moustey (40410)' => '40200', - 'Moustier (47800)' => '47194', - 'Moustier-Ventadour (19300)' => '19145', - 'Mouterre-Silly (86200)' => '86173', - 'Mouterre-sur-Blourde (86430)' => '86172', - 'Mouthiers-sur-Boëme (16440)' => '16236', - 'Moutier-d\'Ahun (23150)' => '23138', - 'Moutier-Malcard (23220)' => '23139', - 'Moutier-Rozeille (23200)' => '23140', - 'Moutiers-sous-Chantemerle (79320)' => '79188', - 'Mouton (16460)' => '16237', - 'Moutonneau (16460)' => '16238', - 'Mouzon (16310)' => '16239', - 'Mugron (40250)' => '40201', - 'Muron (17430)' => '17253', - 'Musculdy (64130)' => '64411', - 'Mussidan (24400)' => '24299', - 'Nabas (64190)' => '64412', - 'Nabinaud (16390)' => '16240', - 'Nabirat (24250)' => '24300', - 'Nachamps (17380)' => '17254', - 'Nadaillac (24590)' => '24301', - 'Nailhac (24390)' => '24302', - 'Naillat (23800)' => '23141', - 'Naintré (86530)' => '86174', - 'Nalliers (86310)' => '86175', - 'Nanclars (16230)' => '16241', - 'Nancras (17600)' => '17255', - 'Nanteuil (79400)' => '79189', - 'Nanteuil-Auriac-de-Bourzac (24320)' => '24303', - 'Nanteuil-en-Vallée (16700)' => '16242', - 'Nantheuil (24800)' => '24304', - 'Nanthiat (24800)' => '24305', - 'Nantiat (87140)' => '87103', - 'Nantillé (17770)' => '17256', - 'Narcastet (64510)' => '64413', - 'Narp (64190)' => '64414', - 'Narrosse (40180)' => '40202', - 'Nassiet (40330)' => '40203', - 'Nastringues (24230)' => '24306', - 'Naujac-sur-Mer (33990)' => '33300', - 'Naujan-et-Postiac (33420)' => '33301', - 'Naussannes (24440)' => '24307', - 'Navailles-Angos (64450)' => '64415', - 'Navarrenx (64190)' => '64416', - 'Naves (19460)' => '19146', - 'Nay (64800)' => '64417', - 'Néac (33500)' => '33302', - 'Nedde (87120)' => '87104', - 'Négrondes (24460)' => '24308', - 'Néoux (23200)' => '23142', - 'Nérac (47600)' => '47195', - 'Nerbis (40250)' => '40204', - 'Nercillac (16200)' => '16243', - 'Néré (17510)' => '17257', - 'Nérigean (33750)' => '33303', - 'Nérignac (86150)' => '86176', - 'Nersac (16440)' => '16244', - 'Nespouls (19600)' => '19147', - 'Neuffons (33580)' => '33304', - 'Neuillac (17520)' => '17258', - 'Neulles (17500)' => '17259', - 'Neuvic (19160)' => '19148', - 'Neuvic (24190)' => '24309', - 'Neuvic-Entier (87130)' => '87105', - 'Neuvicq (17270)' => '17260', - 'Neuvicq-le-Château (17490)' => '17261', - 'Neuville (19380)' => '19149', - 'Neuville-de-Poitou (86170)' => '86177', - 'Neuvy-Bouin (79130)' => '79190', - 'Nexon (87800)' => '87106', - 'Nicole (47190)' => '47196', - 'Nieuil (16270)' => '16245', - 'Nieuil-l\'Espoir (86340)' => '86178', - 'Nieul (87510)' => '87107', - 'Nieul-le-Virouil (17150)' => '17263', - 'Nieul-lès-Saintes (17810)' => '17262', - 'Nieul-sur-Mer (17137)' => '17264', - 'Nieulle-sur-Seudre (17600)' => '17265', - 'Niort (79000)' => '79191', - 'Noailhac (19500)' => '19150', - 'Noaillac (33190)' => '33306', - 'Noaillan (33730)' => '33307', - 'Noailles (19600)' => '19151', - 'Noguères (64150)' => '64418', - 'Nomdieu (47600)' => '47197', - 'Nonac (16190)' => '16246', - 'Nonards (19120)' => '19152', - 'Nonaville (16120)' => '16247', - 'Nontron (24300)' => '24311', - 'Noth (23300)' => '23143', - 'Notre-Dame-de-Sanilhac (24660)' => '24312', - 'Nouaillé-Maupertuis (86340)' => '86180', - 'Nouhant (23170)' => '23145', - 'Nouic (87330)' => '87108', - 'Nousse (40380)' => '40205', - 'Nousty (64420)' => '64419', - 'Nouzerines (23600)' => '23146', - 'Nouzerolles (23360)' => '23147', - 'Nouziers (23350)' => '23148', - 'Nuaillé-d\'Aunis (17540)' => '17267', - 'Nuaillé-sur-Boutonne (17470)' => '17268', - 'Nueil-les-Aubiers (79250)' => '79195', - 'Nueil-sous-Faye (86200)' => '86181', - 'Objat (19130)' => '19153', - 'Oeyregave (40300)' => '40206', - 'Oeyreluy (40180)' => '40207', - 'Ogenne-Camptort (64190)' => '64420', - 'Ogeu-les-Bains (64680)' => '64421', - 'Oiron (79100)' => '79196', - 'Oloron-Sainte-Marie (64400)' => '64422', - 'Omet (33410)' => '33308', - 'Onard (40380)' => '40208', - 'Ondres (40440)' => '40209', - 'Onesse-Laharie (40110)' => '40210', - 'Oraàs (64390)' => '64423', - 'Oradour (16140)' => '16248', - 'Oradour-Fanais (16500)' => '16249', - 'Oradour-Saint-Genest (87210)' => '87109', - 'Oradour-sur-Glane (87520)' => '87110', - 'Oradour-sur-Vayres (87150)' => '87111', - 'Orches (86230)' => '86182', - 'Ordiarp (64130)' => '64424', - 'Ordonnac (33340)' => '33309', - 'Orègue (64120)' => '64425', - 'Orgedeuil (16220)' => '16250', - 'Orgnac-sur-Vézère (19410)' => '19154', - 'Origne (33113)' => '33310', - 'Orignolles (17210)' => '17269', - 'Orin (64400)' => '64426', - 'Oriolles (16480)' => '16251', - 'Orion (64390)' => '64427', - 'Orist (40300)' => '40211', - 'Orival (16210)' => '16252', - 'Orliac (24170)' => '24313', - 'Orliac-de-Bar (19390)' => '19155', - 'Orliaguet (24370)' => '24314', - 'Oroux (79390)' => '79197', - 'Orriule (64390)' => '64428', - 'Orsanco (64120)' => '64429', - 'Orthevielle (40300)' => '40212', - 'Orthez (64300)' => '64430', - 'Orx (40230)' => '40213', - 'Os-Marsillon (64150)' => '64431', - 'Ossages (40290)' => '40214', - 'Ossas-Suhare (64470)' => '64432', - 'Osse-en-Aspe (64490)' => '64433', - 'Ossenx (64190)' => '64434', - 'Osserain-Rivareyte (64390)' => '64435', - 'Ossès (64780)' => '64436', - 'Ostabat-Asme (64120)' => '64437', - 'Ouillon (64160)' => '64438', - 'Ousse (64320)' => '64439', - 'Ousse-Suzan (40110)' => '40215', - 'Ouzilly (86380)' => '86184', - 'Oyré (86220)' => '86186', - 'Ozenx-Montestrucq (64300)' => '64440', - 'Ozillac (17500)' => '17270', - 'Ozourt (40380)' => '40216', - 'Pageas (87230)' => '87112', - 'Pagolle (64120)' => '64441', - 'Paillé (17470)' => '17271', - 'Paillet (33550)' => '33311', - 'Pailloles (47440)' => '47198', - 'Paizay-le-Chapt (79170)' => '79198', - 'Paizay-le-Sec (86300)' => '86187', - 'Paizay-le-Tort (79500)' => '79199', - 'Paizay-Naudouin-Embourie (16240)' => '16253', - 'Palazinges (19190)' => '19156', - 'Palisse (19160)' => '19157', - 'Palluaud (16390)' => '16254', - 'Pamplie (79220)' => '79200', - 'Pamproux (79800)' => '79201', - 'Panazol (87350)' => '87114', - 'Pandrignes (19150)' => '19158', - 'Parbayse (64360)' => '64442', - 'Parcoul-Chenaud (24410)' => '24316', - 'Pardaillan (47120)' => '47199', - 'Pardies (64150)' => '64443', - 'Pardies-Piétat (64800)' => '64444', - 'Parempuyre (33290)' => '33312', - 'Parentis-en-Born (40160)' => '40217', - 'Parleboscq (40310)' => '40218', - 'Parranquet (47210)' => '47200', - 'Parsac-Rimondeix (23140)' => '23149', - 'Parthenay (79200)' => '79202', - 'Parzac (16450)' => '16255', - 'Pas-de-Jeu (79100)' => '79203', - 'Passirac (16480)' => '16256', - 'Pau (64000)' => '64445', - 'Pauillac (33250)' => '33314', - 'Paulhiac (47150)' => '47202', - 'Paulin (24590)' => '24317', - 'Paunat (24510)' => '24318', - 'Paussac-et-Saint-Vivien (24310)' => '24319', - 'Payré (86700)' => '86188', - 'Payros-Cazautets (40320)' => '40219', - 'Payroux (86350)' => '86189', - 'Pays de Belvès (24170)' => '24035', - 'Payzac (24270)' => '24320', - 'Pazayac (24120)' => '24321', - 'Pécorade (40320)' => '40220', - 'Pellegrue (33790)' => '33316', - 'Penne-d\'Agenais (47140)' => '47203', - 'Pensol (87440)' => '87115', - 'Péré (17700)' => '17272', - 'Péret-Bel-Air (19300)' => '19159', - 'Pérignac (16250)' => '16258', - 'Pérignac (17800)' => '17273', - 'Périgné (79170)' => '79204', - 'Périgny (17180)' => '17274', - 'Périgueux (24000)' => '24322', - 'Périssac (33240)' => '33317', - 'Pérols-sur-Vézère (19170)' => '19160', - 'Perpezac-le-Blanc (19310)' => '19161', - 'Perpezac-le-Noir (19410)' => '19162', - 'Perquie (40190)' => '40221', - 'Pers (79190)' => '79205', - 'Persac (86320)' => '86190', - 'Pessac (33600)' => '33318', - 'Pessac-sur-Dordogne (33890)' => '33319', - 'Pessines (17810)' => '17275', - 'Petit-Bersac (24600)' => '24323', - 'Petit-Palais-et-Cornemps (33570)' => '33320', - 'Peujard (33240)' => '33321', - 'Pey (40300)' => '40222', - 'Peyrabout (23000)' => '23150', - 'Peyrat-de-Bellac (87300)' => '87116', - 'Peyrat-la-Nonière (23130)' => '23151', - 'Peyrat-le-Château (87470)' => '87117', - 'Peyre (40700)' => '40223', - 'Peyrehorade (40300)' => '40224', - 'Peyrelevade (19290)' => '19164', - 'Peyrelongue-Abos (64350)' => '64446', - 'Peyrière (47350)' => '47204', - 'Peyrignac (24210)' => '24324', - 'Peyrilhac (87510)' => '87118', - 'Peyrillac-et-Millac (24370)' => '24325', - 'Peyrissac (19260)' => '19165', - 'Peyzac-le-Moustier (24620)' => '24326', - 'Pezuls (24510)' => '24327', - 'Philondenx (40320)' => '40225', - 'Piégut-Pluviers (24360)' => '24328', - 'Pierre-Buffière (87260)' => '87119', - 'Pierrefitte (19450)' => '19166', - 'Pierrefitte (23130)' => '23152', - 'Pierrefitte (79330)' => '79209', - 'Piets-Plasence-Moustrou (64410)' => '64447', - 'Pillac (16390)' => '16260', - 'Pimbo (40320)' => '40226', - 'Pindères (47700)' => '47205', - 'Pindray (86500)' => '86191', - 'Pinel-Hauterive (47380)' => '47206', - 'Pineuilh (33220)' => '33324', - 'Pionnat (23140)' => '23154', - 'Pioussay (79110)' => '79211', - 'Pisany (17600)' => '17278', - 'Pissos (40410)' => '40227', - 'Plaisance (24560)' => '24168', - 'Plaisance (86500)' => '86192', - 'Plassac (17240)' => '17279', - 'Plassac (33390)' => '33325', - 'Plassac-Rouffiac (16250)' => '16263', - 'Plassay (17250)' => '17280', - 'Plazac (24580)' => '24330', - 'Pleine-Selve (33820)' => '33326', - 'Pleumartin (86450)' => '86193', - 'Pleuville (16490)' => '16264', - 'Pliboux (79190)' => '79212', - 'Podensac (33720)' => '33327', - 'Poey-d\'Oloron (64400)' => '64449', - 'Poey-de-Lescar (64230)' => '64448', - 'Poitiers (86000)' => '86194', - 'Polignac (17210)' => '17281', - 'Pomarez (40360)' => '40228', - 'Pomerol (33500)' => '33328', - 'Pommiers-Moulons (17130)' => '17282', - 'Pompaire (79200)' => '79213', - 'Pompéjac (33730)' => '33329', - 'Pompiey (47230)' => '47207', - 'Pompignac (33370)' => '33330', - 'Pompogne (47420)' => '47208', - 'Pomport (24240)' => '24331', - 'Pomps (64370)' => '64450', - 'Pondaurat (33190)' => '33331', - 'Pons (17800)' => '17283', - 'Ponson-Debat-Pouts (64460)' => '64451', - 'Ponson-Dessus (64460)' => '64452', - 'Pont-du-Casse (47480)' => '47209', - 'Pont-l\'Abbé-d\'Arnoult (17250)' => '17284', - 'Pontacq (64530)' => '64453', - 'Pontarion (23250)' => '23155', - 'Pontcharraud (23260)' => '23156', - 'Pontenx-les-Forges (40200)' => '40229', - 'Ponteyraud (24410)' => '24333', - 'Pontiacq-Viellepinte (64460)' => '64454', - 'Pontonx-sur-l\'Adour (40465)' => '40230', - 'Pontours (24150)' => '24334', - 'Porchères (33660)' => '33332', - 'Port-d\'Envaux (17350)' => '17285', - 'Port-de-Lanne (40300)' => '40231', - 'Port-de-Piles (86220)' => '86195', - 'Port-des-Barques (17730)' => '17484', - 'Port-Sainte-Foy-et-Ponchapt (33220)' => '24335', - 'Port-Sainte-Marie (47130)' => '47210', - 'Portet (64330)' => '64455', - 'Portets (33640)' => '33334', - 'Pouançay (86120)' => '86196', - 'Pouant (86200)' => '86197', - 'Poudenas (47170)' => '47211', - 'Poudenx (40700)' => '40232', - 'Pouffonds (79500)' => '79214', - 'Pougne-Hérisson (79130)' => '79215', - 'Pouillac (17210)' => '17287', - 'Pouillé (86800)' => '86198', - 'Pouillon (40350)' => '40233', - 'Pouliacq (64410)' => '64456', - 'Poullignac (16190)' => '16267', - 'Poursac (16700)' => '16268', - 'Poursay-Garnaud (17400)' => '17288', - 'Poursiugues-Boucoue (64410)' => '64457', - 'Poussanges (23500)' => '23158', - 'Poussignac (47700)' => '47212', - 'Pouydesseaux (40120)' => '40234', - 'Poyanne (40380)' => '40235', - 'Poyartin (40380)' => '40236', - 'Pradines (19170)' => '19168', - 'Prahecq (79230)' => '79216', - 'Prailles (79370)' => '79217', - 'Pranzac (16110)' => '16269', - 'Prats-de-Carlux (24370)' => '24336', - 'Prats-du-Périgord (24550)' => '24337', - 'Prayssas (47360)' => '47213', - 'Préchac (33730)' => '33336', - 'Préchacq-Josbaig (64190)' => '64458', - 'Préchacq-les-Bains (40465)' => '40237', - 'Préchacq-Navarrenx (64190)' => '64459', - 'Précilhon (64400)' => '64460', - 'Préguillac (17460)' => '17289', - 'Preignac (33210)' => '33337', - 'Pressac (86460)' => '86200', - 'Pressignac (16150)' => '16270', - 'Pressignac-Vicq (24150)' => '24338', - 'Pressigny (79390)' => '79218', - 'Preyssac-d\'Excideuil (24160)' => '24339', - 'Priaires (79210)' => '79219', - 'Prignac (17160)' => '17290', - 'Prignac-en-Médoc (33340)' => '33338', - 'Prignac-et-Marcamps (33710)' => '33339', - 'Prigonrieux (24130)' => '24340', - 'Prin-Deyrançon (79210)' => '79220', - 'Prinçay (86420)' => '86201', - 'Prissé-la-Charrière (79360)' => '79078', - 'Proissans (24200)' => '24341', - 'Puch-d\'Agenais (47160)' => '47214', - 'Pugnac (33710)' => '33341', - 'Pugny (79320)' => '79222', - 'Puihardy (79160)' => '79223', - 'Puilboreau (17138)' => '17291', - 'Puisseguin (33570)' => '33342', - 'Pujo-le-Plan (40190)' => '40238', - 'Pujols (33350)' => '33344', - 'Pujols (47300)' => '47215', - 'Pujols-sur-Ciron (33210)' => '33343', - 'Puy-d\'Arnac (19120)' => '19169', - 'Puy-du-Lac (17380)' => '17292', - 'Puy-Malsignat (23130)' => '23159', - 'Puybarban (33190)' => '33346', - 'Puymiclan (47350)' => '47216', - 'Puymirol (47270)' => '47217', - 'Puymoyen (16400)' => '16271', - 'Puynormand (33660)' => '33347', - 'Puyol-Cazalet (40320)' => '40239', - 'Puyoô (64270)' => '64461', - 'Puyravault (17700)' => '17293', - 'Puyréaux (16230)' => '16272', - 'Puyrenier (24340)' => '24344', - 'Puyrolland (17380)' => '17294', - 'Puysserampion (47800)' => '47218', - 'Queaux (86150)' => '86203', - 'Queyrac (33340)' => '33348', - 'Queyssac (24140)' => '24345', - 'Queyssac-les-Vignes (19120)' => '19170', - 'Quinçay (86190)' => '86204', - 'Quinsac (24530)' => '24346', - 'Quinsac (33360)' => '33349', - 'Raix (16240)' => '16273', - 'Ramous (64270)' => '64462', - 'Rampieux (24440)' => '24347', - 'Rancogne (16110)' => '16274', - 'Rancon (87290)' => '87121', - 'Ranton (86200)' => '86205', - 'Ranville-Breuillaud (16140)' => '16275', - 'Raslay (86120)' => '86206', - 'Rauzan (33420)' => '33350', - 'Rayet (47210)' => '47219', - 'Razac-d\'Eymet (24500)' => '24348', - 'Razac-de-Saussignac (24240)' => '24349', - 'Razac-sur-l\'Isle (24430)' => '24350', - 'Razès (87640)' => '87122', - 'Razimet (47160)' => '47220', - 'Réaup-Lisse (47170)' => '47221', - 'Réaux sur Trèfle (17500)' => '17295', - 'Rébénacq (64260)' => '64463', - 'Reffannes (79420)' => '79225', - 'Reignac (16360)' => '16276', - 'Reignac (33860)' => '33351', - 'Rempnat (87120)' => '87123', - 'Renung (40270)' => '40240', - 'Réparsac (16200)' => '16277', - 'Rétaud (17460)' => '17296', - 'Reterre (23110)' => '23160', - 'Retjons (40120)' => '40164', - 'Reygade (19430)' => '19171', - 'Ribagnac (24240)' => '24351', - 'Ribarrouy (64330)' => '64464', - 'Ribérac (24600)' => '24352', - 'Rilhac-Lastours (87800)' => '87124', - 'Rilhac-Rancon (87570)' => '87125', - 'Rilhac-Treignac (19260)' => '19172', - 'Rilhac-Xaintrie (19220)' => '19173', - 'Rimbez-et-Baudiets (40310)' => '40242', - 'Rimons (33580)' => '33353', - 'Riocaud (33220)' => '33354', - 'Rion-des-Landes (40370)' => '40243', - 'Rions (33410)' => '33355', - 'Rioux (17460)' => '17298', - 'Rioux-Martin (16210)' => '16279', - 'Riupeyrous (64160)' => '64465', - 'Rivedoux-Plage (17940)' => '17297', - 'Rivehaute (64190)' => '64466', - 'Rives (47210)' => '47223', - 'Rivière-Saas-et-Gourby (40180)' => '40244', - 'Rivières (16110)' => '16280', - 'Roaillan (33210)' => '33357', - 'Roche-le-Peyroux (19160)' => '19175', - 'Rochechouart (87600)' => '87126', - 'Rochefort (17300)' => '17299', - 'Roches (23270)' => '23162', - 'Roches-Prémarie-Andillé (86340)' => '86209', - 'Roiffé (86120)' => '86210', - 'Rom (79120)' => '79230', - 'Romagne (33760)' => '33358', - 'Romagne (86700)' => '86211', - 'Romans (79260)' => '79231', - 'Romazières (17510)' => '17301', - 'Romegoux (17250)' => '17302', - 'Romestaing (47250)' => '47224', - 'Ronsenac (16320)' => '16283', - 'Rontignon (64110)' => '64467', - 'Roquebrune (33580)' => '33359', - 'Roquefort (40120)' => '40245', - 'Roquefort (47310)' => '47225', - 'Roquiague (64130)' => '64468', - 'Rosiers-d\'Égletons (19300)' => '19176', - 'Rosiers-de-Juillac (19350)' => '19177', - 'Rouffiac (16210)' => '16284', - 'Rouffiac (17800)' => '17304', - 'Rouffignac (17130)' => '17305', - 'Rouffignac-de-Sigoulès (24240)' => '24357', - 'Rouffignac-Saint-Cernin-de-Reilhac (24580)' => '24356', - 'Rougnac (16320)' => '16285', - 'Rougnat (23700)' => '23164', - 'Rouillac (16170)' => '16286', - 'Rouillé (86480)' => '86213', - 'Roullet-Saint-Estèphe (16440)' => '16287', - 'Roumagne (47800)' => '47226', - 'Roumazières-Loubert (16270)' => '16192', - 'Roussac (87140)' => '87128', - 'Roussines (16310)' => '16289', - 'Rouzède (16220)' => '16290', - 'Royan (17200)' => '17306', - 'Royère-de-Vassivière (23460)' => '23165', - 'Royères (87400)' => '87129', - 'Roziers-Saint-Georges (87130)' => '87130', - 'Ruch (33350)' => '33361', - 'Rudeau-Ladosse (24340)' => '24221', - 'Ruelle-sur-Touvre (16600)' => '16291', - 'Ruffec (16700)' => '16292', - 'Ruffiac (47700)' => '47227', - 'Sablonceaux (17600)' => '17307', - 'Sablons (33910)' => '33362', - 'Sabres (40630)' => '40246', - 'Sadillac (24500)' => '24359', - 'Sadirac (33670)' => '33363', - 'Sadroc (19270)' => '19178', - 'Sagelat (24170)' => '24360', - 'Sagnat (23800)' => '23166', - 'Saillac (19500)' => '19179', - 'Saillans (33141)' => '33364', - 'Saillat-sur-Vienne (87720)' => '87131', - 'Saint Aulaye-Puymangou (24410)' => '24376', - 'Saint Maurice Étusson (79150)' => '79280', - 'Saint-Abit (64800)' => '64469', - 'Saint-Adjutory (16310)' => '16293', - 'Saint-Agnant (17620)' => '17308', - 'Saint-Agnant-de-Versillat (23300)' => '23177', - 'Saint-Agnant-près-Crocq (23260)' => '23178', - 'Saint-Agne (24520)' => '24361', - 'Saint-Agnet (40800)' => '40247', - 'Saint-Aignan (33126)' => '33365', - 'Saint-Aigulin (17360)' => '17309', - 'Saint-Alpinien (23200)' => '23179', - 'Saint-Amand (23200)' => '23180', - 'Saint-Amand-de-Coly (24290)' => '24364', - 'Saint-Amand-de-Vergt (24380)' => '24365', - 'Saint-Amand-Jartoudeix (23400)' => '23181', - 'Saint-Amand-le-Petit (87120)' => '87132', - 'Saint-Amand-Magnazeix (87290)' => '87133', - 'Saint-Amand-sur-Sèvre (79700)' => '79235', - 'Saint-Amant-de-Boixe (16330)' => '16295', - 'Saint-Amant-de-Bonnieure (16230)' => '16296', - 'Saint-Amant-de-Montmoreau (16190)' => '16294', - 'Saint-Amant-de-Nouère (16170)' => '16298', - 'Saint-André-d\'Allas (24200)' => '24366', - 'Saint-André-de-Cubzac (33240)' => '33366', - 'Saint-André-de-Double (24190)' => '24367', - 'Saint-André-de-Lidon (17260)' => '17310', - 'Saint-André-de-Seignanx (40390)' => '40248', - 'Saint-André-du-Bois (33490)' => '33367', - 'Saint-André-et-Appelles (33220)' => '33369', - 'Saint-André-sur-Sèvre (79380)' => '79236', - 'Saint-Androny (33390)' => '33370', - 'Saint-Angeau (16230)' => '16300', - 'Saint-Angel (19200)' => '19180', - 'Saint-Antoine-Cumond (24410)' => '24368', - 'Saint-Antoine-d\'Auberoche (24330)' => '24369', - 'Saint-Antoine-de-Breuilh (24230)' => '24370', - 'Saint-Antoine-de-Ficalba (47340)' => '47228', - 'Saint-Antoine-du-Queyret (33790)' => '33372', - 'Saint-Antoine-sur-l\'Isle (33660)' => '33373', - 'Saint-Aquilin (24110)' => '24371', - 'Saint-Armou (64160)' => '64470', - 'Saint-Astier (24110)' => '24372', - 'Saint-Astier (47120)' => '47229', - 'Saint-Aubin (40250)' => '40249', - 'Saint-Aubin (47150)' => '47230', - 'Saint-Aubin-de-Blaye (33820)' => '33374', - 'Saint-Aubin-de-Branne (33420)' => '33375', - 'Saint-Aubin-de-Cadelech (24500)' => '24373', - 'Saint-Aubin-de-Lanquais (24560)' => '24374', - 'Saint-Aubin-de-Médoc (33160)' => '33376', - 'Saint-Aubin-de-Nabirat (24250)' => '24375', - 'Saint-Aubin-du-Plain (79300)' => '79238', - 'Saint-Aubin-le-Cloud (79450)' => '79239', - 'Saint-Augustin (17570)' => '17311', - 'Saint-Augustin (19390)' => '19181', - 'Saint-Aulaire (19130)' => '19182', - 'Saint-Aulais-la-Chapelle (16300)' => '16301', - 'Saint-Auvent (87310)' => '87135', - 'Saint-Avit (16210)' => '16302', - 'Saint-Avit (40090)' => '40250', - 'Saint-Avit (47350)' => '47231', - 'Saint-Avit-de-Soulège (33220)' => '33377', - 'Saint-Avit-de-Tardes (23200)' => '23182', - 'Saint-Avit-de-Vialard (24260)' => '24377', - 'Saint-Avit-le-Pauvre (23480)' => '23183', - 'Saint-Avit-Rivière (24540)' => '24378', - 'Saint-Avit-Saint-Nazaire (33220)' => '33378', - 'Saint-Avit-Sénieur (24440)' => '24379', - 'Saint-Barbant (87330)' => '87136', - 'Saint-Bard (23260)' => '23184', - 'Saint-Barthélemy (40390)' => '40251', - 'Saint-Barthélemy-d\'Agenais (47350)' => '47232', - 'Saint-Barthélemy-de-Bellegarde (24700)' => '24380', - 'Saint-Barthélemy-de-Bussière (24360)' => '24381', - 'Saint-Bazile (87150)' => '87137', - 'Saint-Bazile-de-la-Roche (19320)' => '19183', - 'Saint-Bazile-de-Meyssac (19500)' => '19184', - 'Saint-Benoît (86280)' => '86214', - 'Saint-Boès (64300)' => '64471', - 'Saint-Bonnet (16300)' => '16303', - 'Saint-Bonnet-Avalouze (19150)' => '19185', - 'Saint-Bonnet-Briance (87260)' => '87138', - 'Saint-Bonnet-de-Bellac (87300)' => '87139', - 'Saint-Bonnet-Elvert (19380)' => '19186', - 'Saint-Bonnet-l\'Enfantier (19410)' => '19188', - 'Saint-Bonnet-la-Rivière (19130)' => '19187', - 'Saint-Bonnet-les-Tours-de-Merle (19430)' => '19189', - 'Saint-Bonnet-près-Bort (19200)' => '19190', - 'Saint-Bonnet-sur-Gironde (17150)' => '17312', - 'Saint-Brice (16100)' => '16304', - 'Saint-Brice (33540)' => '33379', - 'Saint-Brice-sur-Vienne (87200)' => '87140', - 'Saint-Bris-des-Bois (17770)' => '17313', - 'Saint-Caprais-de-Blaye (33820)' => '33380', - 'Saint-Caprais-de-Bordeaux (33880)' => '33381', - 'Saint-Caprais-de-Lerm (47270)' => '47234', - 'Saint-Capraise-d\'Eymet (24500)' => '24383', - 'Saint-Capraise-de-Lalinde (24150)' => '24382', - 'Saint-Cassien (24540)' => '24384', - 'Saint-Castin (64160)' => '64472', - 'Saint-Cernin-de-l\'Herm (24550)' => '24386', - 'Saint-Cernin-de-Labarde (24560)' => '24385', - 'Saint-Cernin-de-Larche (19600)' => '19191', - 'Saint-Césaire (17770)' => '17314', - 'Saint-Chabrais (23130)' => '23185', - 'Saint-Chamant (19380)' => '19192', - 'Saint-Chamassy (24260)' => '24388', - 'Saint-Christoly-de-Blaye (33920)' => '33382', - 'Saint-Christoly-Médoc (33340)' => '33383', - 'Saint-Christophe (16420)' => '16306', - 'Saint-Christophe (17220)' => '17315', - 'Saint-Christophe (23000)' => '23186', - 'Saint-Christophe (86230)' => '86217', - 'Saint-Christophe-de-Double (33230)' => '33385', - 'Saint-Christophe-des-Bardes (33330)' => '33384', - 'Saint-Christophe-sur-Roc (79220)' => '79241', - 'Saint-Cibard (33570)' => '33386', - 'Saint-Ciers-Champagne (17520)' => '17316', - 'Saint-Ciers-d\'Abzac (33910)' => '33387', - 'Saint-Ciers-de-Canesse (33710)' => '33388', - 'Saint-Ciers-du-Taillon (17240)' => '17317', - 'Saint-Ciers-sur-Bonnieure (16230)' => '16307', - 'Saint-Ciers-sur-Gironde (33820)' => '33389', - 'Saint-Cirgues-la-Loutre (19220)' => '19193', - 'Saint-Cirq (24260)' => '24389', - 'Saint-Clair (86330)' => '86218', - 'Saint-Claud (16450)' => '16308', - 'Saint-Clément (19700)' => '19194', - 'Saint-Clément-des-Baleines (17590)' => '17318', - 'Saint-Colomb-de-Lauzun (47410)' => '47235', - 'Saint-Côme (33430)' => '33391', - 'Saint-Coutant (16350)' => '16310', - 'Saint-Coutant (79120)' => '79243', - 'Saint-Coutant-le-Grand (17430)' => '17320', - 'Saint-Crépin (17380)' => '17321', - 'Saint-Crépin-d\'Auberoche (24330)' => '24390', - 'Saint-Crépin-de-Richemont (24310)' => '24391', - 'Saint-Crépin-et-Carlucet (24590)' => '24392', - 'Saint-Cricq-Chalosse (40700)' => '40253', - 'Saint-Cricq-du-Gave (40300)' => '40254', - 'Saint-Cricq-Villeneuve (40190)' => '40255', - 'Saint-Cybardeaux (16170)' => '16312', - 'Saint-Cybranet (24250)' => '24395', - 'Saint-Cyprien (19130)' => '19195', - 'Saint-Cyprien (24220)' => '24396', - 'Saint-Cyr (86130)' => '86219', - 'Saint-Cyr (87310)' => '87141', - 'Saint-Cyr-du-Doret (17170)' => '17322', - 'Saint-Cyr-la-Lande (79100)' => '79244', - 'Saint-Cyr-la-Roche (19130)' => '19196', - 'Saint-Cyr-les-Champagnes (24270)' => '24397', - 'Saint-Denis-d\'Oléron (17650)' => '17323', - 'Saint-Denis-de-Pile (33910)' => '33393', - 'Saint-Denis-des-Murs (87400)' => '87142', - 'Saint-Dizant-du-Bois (17150)' => '17324', - 'Saint-Dizant-du-Gua (17240)' => '17325', - 'Saint-Dizier-la-Tour (23130)' => '23187', - 'Saint-Dizier-les-Domaines (23270)' => '23188', - 'Saint-Dizier-Leyrenne (23400)' => '23189', - 'Saint-Domet (23190)' => '23190', - 'Saint-Dos (64270)' => '64474', - 'Saint-Éloi (23000)' => '23191', - 'Saint-Éloy-les-Tuileries (19210)' => '19198', - 'Saint-Émilion (33330)' => '33394', - 'Saint-Esteben (64640)' => '64476', - 'Saint-Estèphe (24360)' => '24398', - 'Saint-Estèphe (33180)' => '33395', - 'Saint-Étienne-aux-Clos (19200)' => '19199', - 'Saint-Étienne-d\'Orthe (40300)' => '40256', - 'Saint-Étienne-de-Baïgorry (64430)' => '64477', - 'Saint-Étienne-de-Fougères (47380)' => '47239', - 'Saint-Étienne-de-Fursac (23290)' => '23192', - 'Saint-Étienne-de-Lisse (33330)' => '33396', - 'Saint-Étienne-de-Puycorbier (24400)' => '24399', - 'Saint-Étienne-de-Villeréal (47210)' => '47240', - 'Saint-Étienne-la-Cigogne (79360)' => '79247', - 'Saint-Étienne-la-Geneste (19160)' => '19200', - 'Saint-Eugène (17520)' => '17326', - 'Saint-Eutrope (16190)' => '16314', - 'Saint-Eutrope-de-Born (47210)' => '47241', - 'Saint-Exupéry (33190)' => '33398', - 'Saint-Exupéry-les-Roches (19200)' => '19201', - 'Saint-Faust (64110)' => '64478', - 'Saint-Félix (16480)' => '16315', - 'Saint-Félix (17330)' => '17327', - 'Saint-Félix-de-Bourdeilles (24340)' => '24403', - 'Saint-Félix-de-Foncaude (33540)' => '33399', - 'Saint-Félix-de-Reillac-et-Mortemart (24260)' => '24404', - 'Saint-Félix-de-Villadeix (24510)' => '24405', - 'Saint-Ferme (33580)' => '33400', - 'Saint-Fiel (23000)' => '23195', - 'Saint-Fort-sur-Gironde (17240)' => '17328', - 'Saint-Fort-sur-le-Né (16130)' => '16316', - 'Saint-Fraigne (16140)' => '16317', - 'Saint-Fréjoux (19200)' => '19204', - 'Saint-Frion (23500)' => '23196', - 'Saint-Front (16460)' => '16318', - 'Saint-Front-d\'Alemps (24460)' => '24408', - 'Saint-Front-de-Pradoux (24400)' => '24409', - 'Saint-Front-la-Rivière (24300)' => '24410', - 'Saint-Front-sur-Lémance (47500)' => '47242', - 'Saint-Front-sur-Nizonne (24300)' => '24411', - 'Saint-Froult (17780)' => '17329', - 'Saint-Gaudent (86400)' => '86220', - 'Saint-Gein (40190)' => '40259', - 'Saint-Gelais (79410)' => '79249', - 'Saint-Génard (79500)' => '79251', - 'Saint-Gence (87510)' => '87143', - 'Saint-Généroux (79600)' => '79252', - 'Saint-Genès-de-Blaye (33390)' => '33405', - 'Saint-Genès-de-Castillon (33350)' => '33406', - 'Saint-Genès-de-Fronsac (33240)' => '33407', - 'Saint-Genès-de-Lombaud (33670)' => '33408', - 'Saint-Genest-d\'Ambière (86140)' => '86221', - 'Saint-Genest-sur-Roselle (87260)' => '87144', - 'Saint-Geniès (24590)' => '24412', - 'Saint-Geniez-ô-Merle (19220)' => '19205', - 'Saint-Genis-d\'Hiersac (16570)' => '16320', - 'Saint-Genis-de-Saintonge (17240)' => '17331', - 'Saint-Genis-du-Bois (33760)' => '33409', - 'Saint-Georges (16700)' => '16321', - 'Saint-Georges (47370)' => '47328', - 'Saint-Georges-Antignac (17240)' => '17332', - 'Saint-Georges-Blancaneix (24130)' => '24413', - 'Saint-Georges-d\'Oléron (17190)' => '17337', - 'Saint-Georges-de-Didonne (17110)' => '17333', - 'Saint-Georges-de-Longuepierre (17470)' => '17334', - 'Saint-Georges-de-Montclard (24140)' => '24414', - 'Saint-Georges-de-Noisné (79400)' => '79253', - 'Saint-Georges-de-Rex (79210)' => '79254', - 'Saint-Georges-des-Agoûts (17150)' => '17335', - 'Saint-Georges-des-Coteaux (17810)' => '17336', - 'Saint-Georges-du-Bois (17700)' => '17338', - 'Saint-Georges-la-Pouge (23250)' => '23197', - 'Saint-Georges-lès-Baillargeaux (86130)' => '86222', - 'Saint-Georges-les-Landes (87160)' => '87145', - 'Saint-Georges-Nigremont (23500)' => '23198', - 'Saint-Geours-d\'Auribat (40380)' => '40260', - 'Saint-Geours-de-Maremne (40230)' => '40261', - 'Saint-Géraud (47120)' => '47245', - 'Saint-Géraud-de-Corps (24700)' => '24415', - 'Saint-Germain (86310)' => '86223', - 'Saint-Germain-Beaupré (23160)' => '23199', - 'Saint-Germain-d\'Esteuil (33340)' => '33412', - 'Saint-Germain-de-Belvès (24170)' => '24416', - 'Saint-Germain-de-Grave (33490)' => '33411', - 'Saint-Germain-de-la-Rivière (33240)' => '33414', - 'Saint-Germain-de-Longue-Chaume (79200)' => '79255', - 'Saint-Germain-de-Lusignan (17500)' => '17339', - 'Saint-Germain-de-Marencennes (17700)' => '17340', - 'Saint-Germain-de-Montbron (16380)' => '16323', - 'Saint-Germain-de-Vibrac (17500)' => '17341', - 'Saint-Germain-des-Prés (24160)' => '24417', - 'Saint-Germain-du-Puch (33750)' => '33413', - 'Saint-Germain-du-Salembre (24190)' => '24418', - 'Saint-Germain-du-Seudre (17240)' => '17342', - 'Saint-Germain-et-Mons (24520)' => '24419', - 'Saint-Germain-Lavolps (19290)' => '19206', - 'Saint-Germain-les-Belles (87380)' => '87146', - 'Saint-Germain-les-Vergnes (19330)' => '19207', - 'Saint-Germier (79340)' => '79256', - 'Saint-Gervais (33240)' => '33415', - 'Saint-Gervais-les-Trois-Clochers (86230)' => '86224', - 'Saint-Géry (24400)' => '24420', - 'Saint-Geyrac (24330)' => '24421', - 'Saint-Gilles-les-Forêts (87130)' => '87147', - 'Saint-Girons-d\'Aiguevives (33920)' => '33416', - 'Saint-Girons-en-Béarn (64300)' => '64479', - 'Saint-Gladie-Arrive-Munein (64390)' => '64480', - 'Saint-Goin (64400)' => '64481', - 'Saint-Gor (40120)' => '40262', - 'Saint-Gourson (16700)' => '16325', - 'Saint-Goussaud (23430)' => '23200', - 'Saint-Grégoire-d\'Ardennes (17240)' => '17343', - 'Saint-Groux (16230)' => '16326', - 'Saint-Hilaire-Bonneval (87260)' => '87148', - 'Saint-Hilaire-d\'Estissac (24140)' => '24422', - 'Saint-Hilaire-de-la-Noaille (33190)' => '33418', - 'Saint-Hilaire-de-Lusignan (47450)' => '47246', - 'Saint-Hilaire-de-Villefranche (17770)' => '17344', - 'Saint-Hilaire-du-Bois (17500)' => '17345', - 'Saint-Hilaire-du-Bois (33540)' => '33419', - 'Saint-Hilaire-Foissac (19550)' => '19208', - 'Saint-Hilaire-la-Palud (79210)' => '79257', - 'Saint-Hilaire-la-Plaine (23150)' => '23201', - 'Saint-Hilaire-la-Treille (87190)' => '87149', - 'Saint-Hilaire-le-Château (23250)' => '23202', - 'Saint-Hilaire-les-Courbes (19170)' => '19209', - 'Saint-Hilaire-les-Places (87800)' => '87150', - 'Saint-Hilaire-Luc (19160)' => '19210', - 'Saint-Hilaire-Peyroux (19560)' => '19211', - 'Saint-Hilaire-Taurieux (19400)' => '19212', - 'Saint-Hippolyte (17430)' => '17346', - 'Saint-Hippolyte (33330)' => '33420', - 'Saint-Jacques-de-Thouars (79100)' => '79258', - 'Saint-Jal (19700)' => '19213', - 'Saint-Jammes (64160)' => '64482', - 'Saint-Jean-d\'Angély (17400)' => '17347', - 'Saint-Jean-d\'Angle (17620)' => '17348', - 'Saint-Jean-d\'Ataux (24190)' => '24424', - 'Saint-Jean-d\'Estissac (24140)' => '24426', - 'Saint-Jean-d\'Eyraud (24140)' => '24427', - 'Saint-Jean-d\'Illac (33127)' => '33422', - 'Saint-Jean-de-Blaignac (33420)' => '33421', - 'Saint-Jean-de-Côle (24800)' => '24425', - 'Saint-Jean-de-Duras (47120)' => '47247', - 'Saint-Jean-de-Lier (40380)' => '40263', - 'Saint-Jean-de-Liversay (17170)' => '17349', - 'Saint-Jean-de-Luz (64500)' => '64483', - 'Saint-Jean-de-Marsacq (40230)' => '40264', - 'Saint-Jean-de-Sauves (86330)' => '86225', - 'Saint-Jean-de-Thouars (79100)' => '79259', - 'Saint-Jean-de-Thurac (47270)' => '47248', - 'Saint-Jean-le-Vieux (64220)' => '64484', - 'Saint-Jean-Ligoure (87260)' => '87151', - 'Saint-Jean-Pied-de-Port (64220)' => '64485', - 'Saint-Jean-Poudge (64330)' => '64486', - 'Saint-Jory-de-Chalais (24800)' => '24428', - 'Saint-Jory-las-Bloux (24160)' => '24429', - 'Saint-Jouin-de-Marnes (79600)' => '79260', - 'Saint-Jouin-de-Milly (79380)' => '79261', - 'Saint-Jouvent (87510)' => '87152', - 'Saint-Julien-aux-Bois (19220)' => '19214', - 'Saint-Julien-Beychevelle (33250)' => '33423', - 'Saint-Julien-d\'Armagnac (40240)' => '40265', - 'Saint-Julien-d\'Eymet (24500)' => '24433', - 'Saint-Julien-de-Crempse (24140)' => '24431', - 'Saint-Julien-de-l\'Escap (17400)' => '17350', - 'Saint-Julien-de-Lampon (24370)' => '24432', - 'Saint-Julien-en-Born (40170)' => '40266', - 'Saint-Julien-l\'Ars (86800)' => '86226', - 'Saint-Julien-la-Genête (23110)' => '23203', - 'Saint-Julien-le-Châtel (23130)' => '23204', - 'Saint-Julien-le-Pèlerin (19430)' => '19215', - 'Saint-Julien-le-Petit (87460)' => '87153', - 'Saint-Julien-le-Vendômois (19210)' => '19216', - 'Saint-Julien-Maumont (19500)' => '19217', - 'Saint-Julien-près-Bort (19110)' => '19218', - 'Saint-Junien (87200)' => '87154', - 'Saint-Junien-la-Bregère (23400)' => '23205', - 'Saint-Junien-les-Combes (87300)' => '87155', - 'Saint-Just (24320)' => '24434', - 'Saint-Just-Ibarre (64120)' => '64487', - 'Saint-Just-le-Martel (87590)' => '87156', - 'Saint-Just-Luzac (17320)' => '17351', - 'Saint-Justin (40240)' => '40267', - 'Saint-Laon (86200)' => '86227', - 'Saint-Laurent (23000)' => '23206', - 'Saint-Laurent (47130)' => '47249', - 'Saint-Laurent-Bretagne (64160)' => '64488', - 'Saint-Laurent-d\'Arce (33240)' => '33425', - 'Saint-Laurent-de-Belzagot (16190)' => '16328', - 'Saint-Laurent-de-Céris (16450)' => '16329', - 'Saint-Laurent-de-Cognac (16100)' => '16330', - 'Saint-Laurent-de-Gosse (40390)' => '40268', - 'Saint-Laurent-de-Jourdes (86410)' => '86228', - 'Saint-Laurent-de-la-Barrière (17380)' => '17352', - 'Saint-Laurent-de-la-Prée (17450)' => '17353', - 'Saint-Laurent-des-Combes (16480)' => '16331', - 'Saint-Laurent-des-Combes (33330)' => '33426', - 'Saint-Laurent-des-Hommes (24400)' => '24436', - 'Saint-Laurent-des-Vignes (24100)' => '24437', - 'Saint-Laurent-du-Bois (33540)' => '33427', - 'Saint-Laurent-du-Plan (33190)' => '33428', - 'Saint-Laurent-la-Vallée (24170)' => '24438', - 'Saint-Laurent-les-Églises (87240)' => '87157', - 'Saint-Laurent-Médoc (33112)' => '33424', - 'Saint-Laurent-sur-Gorre (87310)' => '87158', - 'Saint-Laurs (79160)' => '79263', - 'Saint-Léger (16250)' => '16332', - 'Saint-Léger (17800)' => '17354', - 'Saint-Léger (47160)' => '47250', - 'Saint-Léger-Bridereix (23300)' => '23207', - 'Saint-Léger-de-Balson (33113)' => '33429', - 'Saint-Léger-de-la-Martinière (79500)' => '79264', - 'Saint-Léger-de-Montbrillais (86120)' => '86229', - 'Saint-Léger-de-Montbrun (79100)' => '79265', - 'Saint-Léger-la-Montagne (87340)' => '87159', - 'Saint-Léger-le-Guérétois (23000)' => '23208', - 'Saint-Léger-Magnazeix (87190)' => '87160', - 'Saint-Léomer (86290)' => '86230', - 'Saint-Léon (33670)' => '33431', - 'Saint-Léon (47160)' => '47251', - 'Saint-Léon-d\'Issigeac (24560)' => '24441', - 'Saint-Léon-sur-l\'Isle (24110)' => '24442', - 'Saint-Léon-sur-Vézère (24290)' => '24443', - 'Saint-Léonard-de-Noblat (87400)' => '87161', - 'Saint-Lin (79420)' => '79267', - 'Saint-Lon-les-Mines (40300)' => '40269', - 'Saint-Loubert (33210)' => '33432', - 'Saint-Loubès (33450)' => '33433', - 'Saint-Loubouer (40320)' => '40270', - 'Saint-Louis-de-Montferrand (33440)' => '33434', - 'Saint-Louis-en-l\'Isle (24400)' => '24444', - 'Saint-Loup (17380)' => '17356', - 'Saint-Loup (23130)' => '23209', - 'Saint-Loup-Lamairé (79600)' => '79268', - 'Saint-Macaire (33490)' => '33435', - 'Saint-Macoux (86400)' => '86231', - 'Saint-Magne (33125)' => '33436', - 'Saint-Magne-de-Castillon (33350)' => '33437', - 'Saint-Maigrin (17520)' => '17357', - 'Saint-Maime-de-Péreyrol (24380)' => '24459', - 'Saint-Maixant (23200)' => '23210', - 'Saint-Maixant (33490)' => '33438', - 'Saint-Maixent-de-Beugné (79160)' => '79269', - 'Saint-Maixent-l\'École (79400)' => '79270', - 'Saint-Mandé-sur-Brédoire (17470)' => '17358', - 'Saint-Marc-à-Frongier (23200)' => '23211', - 'Saint-Marc-à-Loubaud (23460)' => '23212', - 'Saint-Marc-la-Lande (79310)' => '79271', - 'Saint-Marcel-du-Périgord (24510)' => '24445', - 'Saint-Marcory (24540)' => '24446', - 'Saint-Mard (17700)' => '17359', - 'Saint-Marien (23600)' => '23213', - 'Saint-Mariens (33620)' => '33439', - 'Saint-Martial (16190)' => '16334', - 'Saint-Martial (17330)' => '17361', - 'Saint-Martial (33490)' => '33440', - 'Saint-Martial-d\'Albarède (24160)' => '24448', - 'Saint-Martial-d\'Artenset (24700)' => '24449', - 'Saint-Martial-de-Gimel (19150)' => '19220', - 'Saint-Martial-de-Mirambeau (17150)' => '17362', - 'Saint-Martial-de-Nabirat (24250)' => '24450', - 'Saint-Martial-de-Valette (24300)' => '24451', - 'Saint-Martial-de-Vitaterne (17500)' => '17363', - 'Saint-Martial-Entraygues (19400)' => '19221', - 'Saint-Martial-le-Mont (23150)' => '23214', - 'Saint-Martial-le-Vieux (23100)' => '23215', - 'Saint-Martial-sur-Isop (87330)' => '87163', - 'Saint-Martial-sur-Né (17520)' => '17364', - 'Saint-Martial-Viveyrol (24320)' => '24452', - 'Saint-Martin-Château (23460)' => '23216', - 'Saint-Martin-Curton (47700)' => '47254', - 'Saint-Martin-d\'Arberoue (64640)' => '64489', - 'Saint-Martin-d\'Arrossa (64780)' => '64490', - 'Saint-Martin-d\'Ary (17270)' => '17365', - 'Saint-Martin-d\'Oney (40090)' => '40274', - 'Saint-Martin-de-Beauville (47270)' => '47255', - 'Saint-Martin-de-Bernegoue (79230)' => '79273', - 'Saint-Martin-de-Coux (17360)' => '17366', - 'Saint-Martin-de-Fressengeas (24800)' => '24453', - 'Saint-Martin-de-Gurson (24610)' => '24454', - 'Saint-Martin-de-Hinx (40390)' => '40272', - 'Saint-Martin-de-Juillers (17400)' => '17367', - 'Saint-Martin-de-Jussac (87200)' => '87164', - 'Saint-Martin-de-Laye (33910)' => '33442', - 'Saint-Martin-de-Lerm (33540)' => '33443', - 'Saint-Martin-de-Mâcon (79100)' => '79274', - 'Saint-Martin-de-Ré (17410)' => '17369', - 'Saint-Martin-de-Ribérac (24600)' => '24455', - 'Saint-Martin-de-Saint-Maixent (79400)' => '79276', - 'Saint-Martin-de-Sanzay (79290)' => '79277', - 'Saint-Martin-de-Seignanx (40390)' => '40273', - 'Saint-Martin-de-Sescas (33490)' => '33444', - 'Saint-Martin-de-Villeréal (47210)' => '47256', - 'Saint-Martin-des-Combes (24140)' => '24456', - 'Saint-Martin-du-Bois (33910)' => '33445', - 'Saint-Martin-du-Clocher (16700)' => '16335', - 'Saint-Martin-du-Fouilloux (79420)' => '79278', - 'Saint-Martin-du-Puy (33540)' => '33446', - 'Saint-Martin-l\'Ars (86350)' => '86234', - 'Saint-Martin-l\'Astier (24400)' => '24457', - 'Saint-Martin-la-Méanne (19320)' => '19222', - 'Saint-Martin-Lacaussade (33390)' => '33441', - 'Saint-Martin-le-Mault (87360)' => '87165', - 'Saint-Martin-le-Pin (24300)' => '24458', - 'Saint-Martin-le-Vieux (87700)' => '87166', - 'Saint-Martin-lès-Melle (79500)' => '79279', - 'Saint-Martin-Petit (47180)' => '47257', - 'Saint-Martin-Sainte-Catherine (23430)' => '23217', - 'Saint-Martin-Sepert (19210)' => '19223', - 'Saint-Martin-Terressus (87400)' => '87167', - 'Saint-Mary (16260)' => '16336', - 'Saint-Mathieu (87440)' => '87168', - 'Saint-Maurice-de-Lestapel (47290)' => '47259', - 'Saint-Maurice-des-Lions (16500)' => '16337', - 'Saint-Maurice-la-Clouère (86160)' => '86235', - 'Saint-Maurice-la-Souterraine (23300)' => '23219', - 'Saint-Maurice-les-Brousses (87800)' => '87169', - 'Saint-Maurice-près-Crocq (23260)' => '23218', - 'Saint-Maurice-sur-Adour (40270)' => '40275', - 'Saint-Maurin (47270)' => '47260', - 'Saint-Maxire (79410)' => '79281', - 'Saint-Méard (87130)' => '87170', - 'Saint-Méard-de-Drône (24600)' => '24460', - 'Saint-Méard-de-Gurçon (24610)' => '24461', - 'Saint-Médard (16300)' => '16338', - 'Saint-Médard (17500)' => '17372', - 'Saint-Médard (64370)' => '64491', - 'Saint-Médard (79370)' => '79282', - 'Saint-Médard-d\'Aunis (17220)' => '17373', - 'Saint-Médard-d\'Excideuil (24160)' => '24463', - 'Saint-Médard-d\'Eyrans (33650)' => '33448', - 'Saint-Médard-de-Guizières (33230)' => '33447', - 'Saint-Médard-de-Mussidan (24400)' => '24462', - 'Saint-Médard-en-Jalles (33160)' => '33449', - 'Saint-Médard-la-Rochette (23200)' => '23220', - 'Saint-Même-les-Carrières (16720)' => '16340', - 'Saint-Merd-de-Lapleau (19320)' => '19225', - 'Saint-Merd-la-Breuille (23100)' => '23221', - 'Saint-Merd-les-Oussines (19170)' => '19226', - 'Saint-Mesmin (24270)' => '24464', - 'Saint-Mexant (19330)' => '19227', - 'Saint-Michel (16470)' => '16341', - 'Saint-Michel (64220)' => '64492', - 'Saint-Michel-de-Castelnau (33840)' => '33450', - 'Saint-Michel-de-Double (24400)' => '24465', - 'Saint-Michel-de-Fronsac (33126)' => '33451', - 'Saint-Michel-de-Lapujade (33190)' => '33453', - 'Saint-Michel-de-Montaigne (24230)' => '24466', - 'Saint-Michel-de-Rieufret (33720)' => '33452', - 'Saint-Michel-de-Veisse (23480)' => '23222', - 'Saint-Michel-de-Villadeix (24380)' => '24468', - 'Saint-Michel-Escalus (40550)' => '40276', - 'Saint-Moreil (23400)' => '23223', - 'Saint-Morillon (33650)' => '33454', - 'Saint-Nazaire-sur-Charente (17780)' => '17375', - 'Saint-Nexans (24520)' => '24472', - 'Saint-Nicolas-de-la-Balerme (47220)' => '47262', - 'Saint-Oradoux-de-Chirouze (23100)' => '23224', - 'Saint-Oradoux-près-Crocq (23260)' => '23225', - 'Saint-Ouen-d\'Aunis (17230)' => '17376', - 'Saint-Ouen-la-Thène (17490)' => '17377', - 'Saint-Ouen-sur-Gartempe (87300)' => '87172', - 'Saint-Palais (33820)' => '33456', - 'Saint-Palais (64120)' => '64493', - 'Saint-Palais-de-Négrignac (17210)' => '17378', - 'Saint-Palais-de-Phiolin (17800)' => '17379', - 'Saint-Palais-du-Né (16300)' => '16342', - 'Saint-Palais-sur-Mer (17420)' => '17380', - 'Saint-Pancrace (24530)' => '24474', - 'Saint-Pandelon (40180)' => '40277', - 'Saint-Pantaléon-de-Lapleau (19160)' => '19228', - 'Saint-Pantaléon-de-Larche (19600)' => '19229', - 'Saint-Pantaly-d\'Ans (24640)' => '24475', - 'Saint-Pantaly-d\'Excideuil (24160)' => '24476', - 'Saint-Pardon-de-Conques (33210)' => '33457', - 'Saint-Pardoult (17400)' => '17381', - 'Saint-Pardoux (79310)' => '79285', - 'Saint-Pardoux (87250)' => '87173', - 'Saint-Pardoux-Corbier (19210)' => '19230', - 'Saint-Pardoux-d\'Arnet (23260)' => '23226', - 'Saint-Pardoux-de-Drône (24600)' => '24477', - 'Saint-Pardoux-du-Breuil (47200)' => '47263', - 'Saint-Pardoux-et-Vielvic (24170)' => '24478', - 'Saint-Pardoux-Isaac (47800)' => '47264', - 'Saint-Pardoux-l\'Ortigier (19270)' => '19234', - 'Saint-Pardoux-la-Croisille (19320)' => '19231', - 'Saint-Pardoux-la-Rivière (24470)' => '24479', - 'Saint-Pardoux-le-Neuf (19200)' => '19232', - 'Saint-Pardoux-le-Neuf (23200)' => '23228', - 'Saint-Pardoux-le-Vieux (19200)' => '19233', - 'Saint-Pardoux-les-Cards (23150)' => '23229', - 'Saint-Pardoux-Morterolles (23400)' => '23227', - 'Saint-Pastour (47290)' => '47265', - 'Saint-Paul (19150)' => '19235', - 'Saint-Paul (33390)' => '33458', - 'Saint-Paul (87260)' => '87174', - 'Saint-Paul-de-Serre (24380)' => '24480', - 'Saint-Paul-en-Born (40200)' => '40278', - 'Saint-Paul-en-Gâtine (79240)' => '79286', - 'Saint-Paul-la-Roche (24800)' => '24481', - 'Saint-Paul-lès-Dax (40990)' => '40279', - 'Saint-Paul-Lizonne (24320)' => '24482', - 'Saint-Pé-de-Léren (64270)' => '64494', - 'Saint-Pé-Saint-Simon (47170)' => '47266', - 'Saint-Pée-sur-Nivelle (64310)' => '64495', - 'Saint-Perdon (40090)' => '40280', - 'Saint-Perdoux (24560)' => '24483', - 'Saint-Pey-d\'Armens (33330)' => '33459', - 'Saint-Pey-de-Castets (33350)' => '33460', - 'Saint-Philippe-d\'Aiguille (33350)' => '33461', - 'Saint-Philippe-du-Seignal (33220)' => '33462', - 'Saint-Pierre-Bellevue (23460)' => '23232', - 'Saint-Pierre-Chérignat (23430)' => '23230', - 'Saint-Pierre-d\'Amilly (17700)' => '17382', - 'Saint-Pierre-d\'Aurillac (33490)' => '33463', - 'Saint-Pierre-d\'Exideuil (86400)' => '86237', - 'Saint-Pierre-d\'Eyraud (24130)' => '24487', - 'Saint-Pierre-d\'Irube (64990)' => '64496', - 'Saint-Pierre-d\'Oléron (17310)' => '17385', - 'Saint-Pierre-de-Bat (33760)' => '33464', - 'Saint-Pierre-de-Buzet (47160)' => '47267', - 'Saint-Pierre-de-Chignac (24330)' => '24484', - 'Saint-Pierre-de-Clairac (47270)' => '47269', - 'Saint-Pierre-de-Côle (24800)' => '24485', - 'Saint-Pierre-de-Frugie (24450)' => '24486', - 'Saint-Pierre-de-Fursac (23290)' => '23231', - 'Saint-Pierre-de-Juillers (17400)' => '17383', - 'Saint-Pierre-de-l\'Isle (17330)' => '17384', - 'Saint-Pierre-de-Maillé (86260)' => '86236', - 'Saint-Pierre-de-Mons (33210)' => '33465', - 'Saint-Pierre-des-Échaubrognes (79700)' => '79289', - 'Saint-Pierre-du-Mont (40280)' => '40281', - 'Saint-Pierre-du-Palais (17270)' => '17386', - 'Saint-Pierre-le-Bost (23600)' => '23233', - 'Saint-Pierre-sur-Dropt (47120)' => '47271', - 'Saint-Pompain (79160)' => '79290', - 'Saint-Pompont (24170)' => '24488', - 'Saint-Porchaire (17250)' => '17387', - 'Saint-Preuil (16130)' => '16343', - 'Saint-Priest (23110)' => '23234', - 'Saint-Priest-de-Gimel (19800)' => '19236', - 'Saint-Priest-la-Feuille (23300)' => '23235', - 'Saint-Priest-la-Plaine (23240)' => '23236', - 'Saint-Priest-les-Fougères (24450)' => '24489', - 'Saint-Priest-Ligoure (87800)' => '87176', - 'Saint-Priest-Palus (23400)' => '23237', - 'Saint-Priest-sous-Aixe (87700)' => '87177', - 'Saint-Priest-Taurion (87480)' => '87178', - 'Saint-Privat (19220)' => '19237', - 'Saint-Privat-des-Prés (24410)' => '24490', - 'Saint-Projet-Saint-Constant (16110)' => '16344', - 'Saint-Quantin-de-Rançanne (17800)' => '17388', - 'Saint-Quentin-de-Baron (33750)' => '33466', - 'Saint-Quentin-de-Caplong (33220)' => '33467', - 'Saint-Quentin-de-Chalais (16210)' => '16346', - 'Saint-Quentin-du-Dropt (47330)' => '47272', - 'Saint-Quentin-la-Chabanne (23500)' => '23238', - 'Saint-Quentin-sur-Charente (16150)' => '16345', - 'Saint-Rabier (24210)' => '24491', - 'Saint-Raphaël (24160)' => '24493', - 'Saint-Rémy (19290)' => '19238', - 'Saint-Rémy (24700)' => '24494', - 'Saint-Rémy (79410)' => '79293', - 'Saint-Rémy-sur-Creuse (86220)' => '86241', - 'Saint-Robert (19310)' => '19239', - 'Saint-Robert (47340)' => '47273', - 'Saint-Rogatien (17220)' => '17391', - 'Saint-Romain (16210)' => '16347', - 'Saint-Romain (86250)' => '86242', - 'Saint-Romain-de-Benet (17600)' => '17393', - 'Saint-Romain-de-Monpazier (24540)' => '24495', - 'Saint-Romain-et-Saint-Clément (24800)' => '24496', - 'Saint-Romain-la-Virvée (33240)' => '33470', - 'Saint-Romain-le-Noble (47270)' => '47274', - 'Saint-Romain-sur-Gironde (17240)' => '17392', - 'Saint-Romans-des-Champs (79230)' => '79294', - 'Saint-Romans-lès-Melle (79500)' => '79295', - 'Saint-Salvadour (19700)' => '19240', - 'Saint-Salvy (47360)' => '47275', - 'Saint-Sardos (47360)' => '47276', - 'Saint-Saturnin (16290)' => '16348', - 'Saint-Saturnin-du-Bois (17700)' => '17394', - 'Saint-Saud-Lacoussière (24470)' => '24498', - 'Saint-Sauvant (17610)' => '17395', - 'Saint-Sauvant (86600)' => '86244', - 'Saint-Sauveur (24520)' => '24499', - 'Saint-Sauveur (33250)' => '33471', - 'Saint-Sauveur-d\'Aunis (17540)' => '17396', - 'Saint-Sauveur-de-Meilhan (47180)' => '47277', - 'Saint-Sauveur-de-Puynormand (33660)' => '33472', - 'Saint-Sauveur-Lalande (24700)' => '24500', - 'Saint-Savin (33920)' => '33473', - 'Saint-Savin (86310)' => '86246', - 'Saint-Savinien (17350)' => '17397', - 'Saint-Saviol (86400)' => '86247', - 'Saint-Sébastien (23160)' => '23239', - 'Saint-Secondin (86350)' => '86248', - 'Saint-Selve (33650)' => '33474', - 'Saint-Sernin (47120)' => '47278', - 'Saint-Setiers (19290)' => '19241', - 'Saint-Seurin-de-Bourg (33710)' => '33475', - 'Saint-Seurin-de-Cadourne (33180)' => '33476', - 'Saint-Seurin-de-Cursac (33390)' => '33477', - 'Saint-Seurin-de-Palenne (17800)' => '17398', - 'Saint-Seurin-de-Prats (24230)' => '24501', - 'Saint-Seurin-sur-l\'Isle (33660)' => '33478', - 'Saint-Sève (33190)' => '33479', - 'Saint-Sever (40500)' => '40282', - 'Saint-Sever-de-Saintonge (17800)' => '17400', - 'Saint-Séverin (16390)' => '16350', - 'Saint-Séverin-d\'Estissac (24190)' => '24502', - 'Saint-Séverin-sur-Boutonne (17330)' => '17401', - 'Saint-Sigismond-de-Clermont (17240)' => '17402', - 'Saint-Silvain-Bas-le-Roc (23600)' => '23240', - 'Saint-Silvain-Bellegarde (23190)' => '23241', - 'Saint-Silvain-Montaigut (23320)' => '23242', - 'Saint-Silvain-sous-Toulx (23140)' => '23243', - 'Saint-Simeux (16120)' => '16351', - 'Saint-Simon (16120)' => '16352', - 'Saint-Simon-de-Bordes (17500)' => '17403', - 'Saint-Simon-de-Pellouaille (17260)' => '17404', - 'Saint-Sixte (47220)' => '47279', - 'Saint-Solve (19130)' => '19242', - 'Saint-Sorlin-de-Conac (17150)' => '17405', - 'Saint-Sornin (16220)' => '16353', - 'Saint-Sornin (17600)' => '17406', - 'Saint-Sornin-la-Marche (87210)' => '87179', - 'Saint-Sornin-Lavolps (19230)' => '19243', - 'Saint-Sornin-Leulac (87290)' => '87180', - 'Saint-Sulpice-d\'Arnoult (17250)' => '17408', - 'Saint-Sulpice-d\'Excideuil (24800)' => '24505', - 'Saint-Sulpice-de-Cognac (16370)' => '16355', - 'Saint-Sulpice-de-Faleyrens (33330)' => '33480', - 'Saint-Sulpice-de-Guilleragues (33580)' => '33481', - 'Saint-Sulpice-de-Mareuil (24340)' => '24503', - 'Saint-Sulpice-de-Pommiers (33540)' => '33482', - 'Saint-Sulpice-de-Roumagnac (24600)' => '24504', - 'Saint-Sulpice-de-Royan (17200)' => '17409', - 'Saint-Sulpice-de-Ruffec (16460)' => '16356', - 'Saint-Sulpice-et-Cameyrac (33450)' => '33483', - 'Saint-Sulpice-Laurière (87370)' => '87181', - 'Saint-Sulpice-le-Dunois (23800)' => '23244', - 'Saint-Sulpice-le-Guérétois (23000)' => '23245', - 'Saint-Sulpice-les-Bois (19250)' => '19244', - 'Saint-Sulpice-les-Champs (23480)' => '23246', - 'Saint-Sulpice-les-Feuilles (87160)' => '87182', - 'Saint-Sylvain (19380)' => '19245', - 'Saint-Sylvestre (87240)' => '87183', - 'Saint-Sylvestre-sur-Lot (47140)' => '47280', - 'Saint-Symphorien (33113)' => '33484', - 'Saint-Symphorien (79270)' => '79298', - 'Saint-Symphorien-sur-Couze (87140)' => '87184', - 'Saint-Thomas-de-Conac (17150)' => '17410', - 'Saint-Trojan (33710)' => '33486', - 'Saint-Trojan-les-Bains (17370)' => '17411', - 'Saint-Urcisse (47270)' => '47281', - 'Saint-Vaize (17100)' => '17412', - 'Saint-Vallier (16480)' => '16357', - 'Saint-Varent (79330)' => '79299', - 'Saint-Vaury (23320)' => '23247', - 'Saint-Viance (19240)' => '19246', - 'Saint-Victor (24350)' => '24508', - 'Saint-Victor-en-Marche (23000)' => '23248', - 'Saint-Victour (19200)' => '19247', - 'Saint-Victurnien (87420)' => '87185', - 'Saint-Vincent (64800)' => '64498', - 'Saint-Vincent-de-Connezac (24190)' => '24509', - 'Saint-Vincent-de-Cosse (24220)' => '24510', - 'Saint-Vincent-de-Lamontjoie (47310)' => '47282', - 'Saint-Vincent-de-Paul (33440)' => '33487', - 'Saint-Vincent-de-Paul (40990)' => '40283', - 'Saint-Vincent-de-Pertignas (33420)' => '33488', - 'Saint-Vincent-de-Tyrosse (40230)' => '40284', - 'Saint-Vincent-Jalmoutiers (24410)' => '24511', - 'Saint-Vincent-la-Châtre (79500)' => '79301', - 'Saint-Vincent-le-Paluel (24200)' => '24512', - 'Saint-Vincent-sur-l\'Isle (24420)' => '24513', - 'Saint-Vite (47500)' => '47283', - 'Saint-Vitte-sur-Briance (87380)' => '87186', - 'Saint-Vivien (17220)' => '17413', - 'Saint-Vivien (24230)' => '24514', - 'Saint-Vivien-de-Blaye (33920)' => '33489', - 'Saint-Vivien-de-Médoc (33590)' => '33490', - 'Saint-Vivien-de-Monségur (33580)' => '33491', - 'Saint-Xandre (17138)' => '17414', - 'Saint-Yaguen (40400)' => '40285', - 'Saint-Ybard (19140)' => '19248', - 'Saint-Yrieix-la-Montagne (23460)' => '23249', - 'Saint-Yrieix-la-Perche (87500)' => '87187', - 'Saint-Yrieix-le-Déjalat (19300)' => '19249', - 'Saint-Yrieix-les-Bois (23150)' => '23250', - 'Saint-Yrieix-sous-Aixe (87700)' => '87188', - 'Saint-Yrieix-sur-Charente (16710)' => '16358', - 'Saint-Yzan-de-Soudiac (33920)' => '33492', - 'Saint-Yzans-de-Médoc (33340)' => '33493', - 'Sainte-Alvère-Saint-Laurent Les Bâtons (24510)' => '24362', - 'Sainte-Anne-Saint-Priest (87120)' => '87134', - 'Sainte-Bazeille (47180)' => '47233', - 'Sainte-Blandine (79370)' => '79240', - 'Sainte-Colombe (16230)' => '16309', - 'Sainte-Colombe (17210)' => '17319', - 'Sainte-Colombe (33350)' => '33390', - 'Sainte-Colombe (40700)' => '40252', - 'Sainte-Colombe-de-Duras (47120)' => '47236', - 'Sainte-Colombe-de-Villeneuve (47300)' => '47237', - 'Sainte-Colombe-en-Bruilhois (47310)' => '47238', - 'Sainte-Colome (64260)' => '64473', - 'Sainte-Croix (24440)' => '24393', - 'Sainte-Croix-de-Mareuil (24340)' => '24394', - 'Sainte-Croix-du-Mont (33410)' => '33392', - 'Sainte-Eanne (79800)' => '79246', - 'Sainte-Engrâce (64560)' => '64475', - 'Sainte-Eulalie (33560)' => '33397', - 'Sainte-Eulalie-d\'Ans (24640)' => '24401', - 'Sainte-Eulalie-d\'Eymet (24500)' => '24402', - 'Sainte-Eulalie-en-Born (40200)' => '40257', - 'Sainte-Féréole (19270)' => '19202', - 'Sainte-Feyre (23000)' => '23193', - 'Sainte-Feyre-la-Montagne (23500)' => '23194', - 'Sainte-Florence (33350)' => '33401', - 'Sainte-Fortunade (19490)' => '19203', - 'Sainte-Foy (40190)' => '40258', - 'Sainte-Foy-de-Belvès (24170)' => '24406', - 'Sainte-Foy-de-Longas (24510)' => '24407', - 'Sainte-Foy-la-Grande (33220)' => '33402', - 'Sainte-Foy-la-Longue (33490)' => '33403', - 'Sainte-Gemme (17250)' => '17330', - 'Sainte-Gemme (33580)' => '33404', - 'Sainte-Gemme (79330)' => '79250', - 'Sainte-Gemme-Martaillac (47250)' => '47244', - 'Sainte-Hélène (33480)' => '33417', - 'Sainte-Innocence (24500)' => '24423', - 'Sainte-Lheurine (17520)' => '17355', - 'Sainte-Livrade-sur-Lot (47110)' => '47252', - 'Sainte-Marie-de-Chignac (24330)' => '24447', - 'Sainte-Marie-de-Gosse (40390)' => '40271', - 'Sainte-Marie-de-Ré (17740)' => '17360', - 'Sainte-Marie-de-Vaux (87420)' => '87162', - 'Sainte-Marie-Lapanouze (19160)' => '19219', - 'Sainte-Marthe (47430)' => '47253', - 'Sainte-Maure-de-Peyriac (47170)' => '47258', - 'Sainte-Même (17770)' => '17374', - 'Sainte-Mondane (24370)' => '24470', - 'Sainte-Nathalène (24200)' => '24471', - 'Sainte-Néomaye (79260)' => '79283', - 'Sainte-Orse (24210)' => '24473', - 'Sainte-Ouenne (79220)' => '79284', - 'Sainte-Radegonde (17250)' => '17389', - 'Sainte-Radegonde (24560)' => '24492', - 'Sainte-Radegonde (33350)' => '33468', - 'Sainte-Radegonde (79100)' => '79292', - 'Sainte-Radégonde (86300)' => '86239', - 'Sainte-Ramée (17240)' => '17390', - 'Sainte-Sévère (16200)' => '16349', - 'Sainte-Soline (79120)' => '79297', - 'Sainte-Souline (16480)' => '16354', - 'Sainte-Soulle (17220)' => '17407', - 'Sainte-Terre (33350)' => '33485', - 'Sainte-Trie (24160)' => '24507', - 'Sainte-Verge (79100)' => '79300', - 'Saintes (17100)' => '17415', - 'Saires (86420)' => '86249', - 'Saivres (79400)' => '79302', - 'Saix (86120)' => '86250', - 'Salagnac (24160)' => '24515', - 'Salaunes (33160)' => '33494', - 'Saleignes (17510)' => '17416', - 'Salies-de-Béarn (64270)' => '64499', - 'Salignac-de-Mirambeau (17130)' => '17417', - 'Salignac-Eyvigues (24590)' => '24516', - 'Salignac-sur-Charente (17800)' => '17418', - 'Salleboeuf (33370)' => '33496', - 'Salles (33770)' => '33498', - 'Salles (47150)' => '47284', - 'Salles (79800)' => '79303', - 'Salles-d\'Angles (16130)' => '16359', - 'Salles-de-Barbezieux (16300)' => '16360', - 'Salles-de-Belvès (24170)' => '24517', - 'Salles-de-Villefagnan (16700)' => '16361', - 'Salles-Lavalette (16190)' => '16362', - 'Salles-Mongiscard (64300)' => '64500', - 'Salles-sur-Mer (17220)' => '17420', - 'Sallespisse (64300)' => '64501', - 'Salon (24380)' => '24518', - 'Salon-la-Tour (19510)' => '19250', - 'Samadet (40320)' => '40286', - 'Samazan (47250)' => '47285', - 'Sames (64520)' => '64502', - 'Sammarçolles (86200)' => '86252', - 'Samonac (33710)' => '33500', - 'Samsons-Lion (64350)' => '64503', - 'Sanguinet (40460)' => '40287', - 'Sannat (23110)' => '23167', - 'Sansais (79270)' => '79304', - 'Sanxay (86600)' => '86253', - 'Sarbazan (40120)' => '40288', - 'Sardent (23250)' => '23168', - 'Sare (64310)' => '64504', - 'Sarlande (24270)' => '24519', - 'Sarlat-la-Canéda (24200)' => '24520', - 'Sarliac-sur-l\'Isle (24420)' => '24521', - 'Sarpourenx (64300)' => '64505', - 'Sarran (19800)' => '19251', - 'Sarrance (64490)' => '64506', - 'Sarrazac (24800)' => '24522', - 'Sarraziet (40500)' => '40289', - 'Sarron (40800)' => '40290', - 'Sarroux (19110)' => '19252', - 'Saubion (40230)' => '40291', - 'Saubole (64420)' => '64507', - 'Saubrigues (40230)' => '40292', - 'Saubusse (40180)' => '40293', - 'Saucats (33650)' => '33501', - 'Saucède (64400)' => '64508', - 'Saugnac-et-Cambran (40180)' => '40294', - 'Saugnacq-et-Muret (40410)' => '40295', - 'Saugon (33920)' => '33502', - 'Sauguis-Saint-Étienne (64470)' => '64509', - 'Saujon (17600)' => '17421', - 'Saulgé (86500)' => '86254', - 'Saulgond (16420)' => '16363', - 'Sault-de-Navailles (64300)' => '64510', - 'Sauméjan (47420)' => '47286', - 'Saumont (47600)' => '47287', - 'Saumos (33680)' => '33503', - 'Saurais (79200)' => '79306', - 'Saussignac (24240)' => '24523', - 'Sauternes (33210)' => '33504', - 'Sauvagnac (16310)' => '16364', - 'Sauvagnas (47340)' => '47288', - 'Sauvagnon (64230)' => '64511', - 'Sauvelade (64150)' => '64512', - 'Sauveterre-de-Béarn (64390)' => '64513', - 'Sauveterre-de-Guyenne (33540)' => '33506', - 'Sauveterre-la-Lémance (47500)' => '47292', - 'Sauveterre-Saint-Denis (47220)' => '47293', - 'Sauviac (33430)' => '33507', - 'Sauviat-sur-Vige (87400)' => '87190', - 'Sauvignac (16480)' => '16365', - 'Sauzé-Vaussais (79190)' => '79307', - 'Savennes (23000)' => '23170', - 'Savignac (33124)' => '33508', - 'Savignac-de-Duras (47120)' => '47294', - 'Savignac-de-l\'Isle (33910)' => '33509', - 'Savignac-de-Miremont (24260)' => '24524', - 'Savignac-de-Nontron (24300)' => '24525', - 'Savignac-Lédrier (24270)' => '24526', - 'Savignac-les-Églises (24420)' => '24527', - 'Savignac-sur-Leyze (47150)' => '47295', - 'Savigné (86400)' => '86255', - 'Savigny-Lévescault (86800)' => '86256', - 'Savigny-sous-Faye (86140)' => '86257', - 'Sceau-Saint-Angel (24300)' => '24528', - 'Sciecq (79000)' => '79308', - 'Scillé (79240)' => '79309', - 'Scorbé-Clairvaux (86140)' => '86258', - 'Séby (64410)' => '64514', - 'Secondigné-sur-Belle (79170)' => '79310', - 'Secondigny (79130)' => '79311', - 'Sedze-Maubecq (64160)' => '64515', - 'Sedzère (64160)' => '64516', - 'Ségalas (47410)' => '47296', - 'Segonzac (16130)' => '16366', - 'Segonzac (19310)' => '19253', - 'Segonzac (24600)' => '24529', - 'Ségur-le-Château (19230)' => '19254', - 'Seigné (17510)' => '17422', - 'Seignosse (40510)' => '40296', - 'Seilhac (19700)' => '19255', - 'Séligné (79170)' => '79312', - 'Sembas (47360)' => '47297', - 'Séméacq-Blachon (64350)' => '64517', - 'Semens (33490)' => '33510', - 'Semillac (17150)' => '17423', - 'Semoussac (17150)' => '17424', - 'Semussac (17120)' => '17425', - 'Sencenac-Puy-de-Fourches (24310)' => '24530', - 'Sendets (33690)' => '33511', - 'Sendets (64320)' => '64518', - 'Sénestis (47430)' => '47298', - 'Senillé-Saint-Sauveur (86100)' => '86245', - 'Sepvret (79120)' => '79313', - 'Sérandon (19160)' => '19256', - 'Séreilhac (87620)' => '87191', - 'Sergeac (24290)' => '24531', - 'Sérignac-Péboudou (47410)' => '47299', - 'Sérignac-sur-Garonne (47310)' => '47300', - 'Sérigny (86230)' => '86260', - 'Sérilhac (19190)' => '19257', - 'Sermur (23700)' => '23171', - 'Séron (65320)' => '65422', - 'Serres-Castet (64121)' => '64519', - 'Serres-et-Montguyard (24500)' => '24532', - 'Serres-Gaston (40700)' => '40298', - 'Serres-Morlaàs (64160)' => '64520', - 'Serres-Sainte-Marie (64170)' => '64521', - 'Serreslous-et-Arribans (40700)' => '40299', - 'Sers (16410)' => '16368', - 'Servanches (24410)' => '24533', - 'Servières-le-Château (19220)' => '19258', - 'Sévignacq (64160)' => '64523', - 'Sévignacq-Meyracq (64260)' => '64522', - 'Sèvres-Anxaumont (86800)' => '86261', - 'Sexcles (19430)' => '19259', - 'Seyches (47350)' => '47301', - 'Seyresse (40180)' => '40300', - 'Siecq (17490)' => '17427', - 'Siest (40180)' => '40301', - 'Sigalens (33690)' => '33512', - 'Sigogne (16200)' => '16369', - 'Sigoulès (24240)' => '24534', - 'Sillars (86320)' => '86262', - 'Sillas (33690)' => '33513', - 'Simacourbe (64350)' => '64524', - 'Simeyrols (24370)' => '24535', - 'Sindères (40110)' => '40302', - 'Singleyrac (24500)' => '24536', - 'Sioniac (19120)' => '19260', - 'Siorac-de-Ribérac (24600)' => '24537', - 'Siorac-en-Périgord (24170)' => '24538', - 'Sireuil (16440)' => '16370', - 'Siros (64230)' => '64525', - 'Smarves (86240)' => '86263', - 'Solférino (40210)' => '40303', - 'Solignac (87110)' => '87192', - 'Sommières-du-Clain (86160)' => '86264', - 'Sompt (79110)' => '79314', - 'Sonnac (17160)' => '17428', - 'Soorts-Hossegor (40150)' => '40304', - 'Sorbets (40320)' => '40305', - 'Sorde-l\'Abbaye (40300)' => '40306', - 'Sore (40430)' => '40307', - 'Sorges et Ligueux en Périgord (24420)' => '24540', - 'Sornac (19290)' => '19261', - 'Sort-en-Chalosse (40180)' => '40308', - 'Sos (47170)' => '47302', - 'Sossais (86230)' => '86265', - 'Soubise (17780)' => '17429', - 'Soubran (17150)' => '17430', - 'Soubrebost (23250)' => '23173', - 'Soudaine-Lavinadière (19370)' => '19262', - 'Soudan (79800)' => '79316', - 'Soudat (24360)' => '24541', - 'Soudeilles (19300)' => '19263', - 'Souffrignac (16380)' => '16372', - 'Soulac-sur-Mer (33780)' => '33514', - 'Soulaures (24540)' => '24542', - 'Soulignac (33760)' => '33515', - 'Soulignonne (17250)' => '17431', - 'Soumans (23600)' => '23174', - 'Soumensac (47120)' => '47303', - 'Souméras (17130)' => '17432', - 'Soumoulou (64420)' => '64526', - 'Souprosse (40250)' => '40309', - 'Souraïde (64250)' => '64527', - 'Soursac (19550)' => '19264', - 'Sourzac (24400)' => '24543', - 'Sous-Parsat (23150)' => '23175', - 'Sousmoulins (17130)' => '17433', - 'Soussac (33790)' => '33516', - 'Soussans (33460)' => '33517', - 'Soustons (40140)' => '40310', - 'Soutiers (79310)' => '79318', - 'Souvigné (16240)' => '16373', - 'Souvigné (79800)' => '79319', - 'Soyaux (16800)' => '16374', - 'Suaux (16260)' => '16375', - 'Suhescun (64780)' => '64528', - 'Surdoux (87130)' => '87193', - 'Surgères (17700)' => '17434', - 'Surin (79220)' => '79320', - 'Surin (86250)' => '86266', - 'Suris (16270)' => '16376', - 'Sus (64190)' => '64529', - 'Susmiou (64190)' => '64530', - 'Sussac (87130)' => '87194', - 'Tabaille-Usquain (64190)' => '64531', - 'Tabanac (33550)' => '33518', - 'Tadousse-Ussau (64330)' => '64532', - 'Taillant (17350)' => '17435', - 'Taillebourg (17350)' => '17436', - 'Taillebourg (47200)' => '47304', - 'Taillecavat (33580)' => '33520', - 'Taizé (79100)' => '79321', - 'Taizé-Aizie (16700)' => '16378', - 'Talais (33590)' => '33521', - 'Talence (33400)' => '33522', - 'Taller (40260)' => '40311', - 'Talmont-sur-Gironde (17120)' => '17437', - 'Tamniès (24620)' => '24544', - 'Tanzac (17260)' => '17438', - 'Taponnat-Fleurignac (16110)' => '16379', - 'Tardes (23170)' => '23251', - 'Tardets-Sorholus (64470)' => '64533', - 'Targon (33760)' => '33523', - 'Tarnac (19170)' => '19265', - 'Tarnès (33240)' => '33524', - 'Tarnos (40220)' => '40312', - 'Taron-Sadirac-Viellenave (64330)' => '64534', - 'Tarsacq (64360)' => '64535', - 'Tartas (40400)' => '40313', - 'Taugon (17170)' => '17439', - 'Tauriac (33710)' => '33525', - 'Tayac (33570)' => '33526', - 'Tayrac (47270)' => '47305', - 'Teillots (24390)' => '24545', - 'Temple-Laguyon (24390)' => '24546', - 'Tercé (86800)' => '86268', - 'Tercillat (23350)' => '23252', - 'Tercis-les-Bains (40180)' => '40314', - 'Ternant (17400)' => '17440', - 'Ternay (86120)' => '86269', - 'Terrasson-Lavilledieu (24120)' => '24547', - 'Tersannes (87360)' => '87195', - 'Tesson (17460)' => '17441', - 'Tessonnière (79600)' => '79325', - 'Téthieu (40990)' => '40315', - 'Teuillac (33710)' => '33530', - 'Teyjat (24300)' => '24548', - 'Thaims (17120)' => '17442', - 'Thairé (17290)' => '17443', - 'Thalamy (19200)' => '19266', - 'Thauron (23250)' => '23253', - 'Theil-Rabier (16240)' => '16381', - 'Thénac (17460)' => '17444', - 'Thénac (24240)' => '24549', - 'Thénezay (79390)' => '79326', - 'Thenon (24210)' => '24550', - 'Thézac (17600)' => '17445', - 'Thézac (47370)' => '47307', - 'Thèze (64450)' => '64536', - 'Thiat (87320)' => '87196', - 'Thiviers (24800)' => '24551', - 'Thollet (86290)' => '86270', - 'Thonac (24290)' => '24552', - 'Thorigné (79370)' => '79327', - 'Thorigny-sur-le-Mignon (79360)' => '79328', - 'Thors (17160)' => '17446', - 'Thouars (79100)' => '79329', - 'Thouars-sur-Garonne (47230)' => '47308', - 'Thouron (87140)' => '87197', - 'Thurageau (86110)' => '86271', - 'Thuré (86540)' => '86272', - 'Tilh (40360)' => '40316', - 'Tillou (79110)' => '79330', - 'Tizac-de-Curton (33420)' => '33531', - 'Tizac-de-Lapouyade (33620)' => '33532', - 'Tocane-Saint-Apre (24350)' => '24553', - 'Tombeboeuf (47380)' => '47309', - 'Tonnay-Boutonne (17380)' => '17448', - 'Tonnay-Charente (17430)' => '17449', - 'Tonneins (47400)' => '47310', - 'Torsac (16410)' => '16382', - 'Torxé (17380)' => '17450', - 'Tosse (40230)' => '40317', - 'Toulenne (33210)' => '33533', - 'Toulouzette (40250)' => '40318', - 'Toulx-Sainte-Croix (23600)' => '23254', - 'Tourliac (47210)' => '47311', - 'Tournon-d\'Agenais (47370)' => '47312', - 'Tourriers (16560)' => '16383', - 'Tourtenay (79100)' => '79331', - 'Tourtoirac (24390)' => '24555', - 'Tourtrès (47380)' => '47313', - 'Touvérac (16360)' => '16384', - 'Touvre (16600)' => '16385', - 'Touzac (16120)' => '16386', - 'Toy-Viam (19170)' => '19268', - 'Trayes (79240)' => '79332', - 'Treignac (19260)' => '19269', - 'Trélissac (24750)' => '24557', - 'Trémolat (24510)' => '24558', - 'Trémons (47140)' => '47314', - 'Trensacq (40630)' => '40319', - 'Trentels (47140)' => '47315', - 'Tresses (33370)' => '33535', - 'Triac-Lautrait (16200)' => '16387', - 'Trizay (17250)' => '17453', - 'Troche (19230)' => '19270', - 'Trois-Fonds (23230)' => '23255', - 'Trois-Palis (16730)' => '16388', - 'Trois-Villes (64470)' => '64537', - 'Tudeils (19120)' => '19271', - 'Tugéras-Saint-Maurice (17130)' => '17454', - 'Tulle (19000)' => '19272', - 'Turenne (19500)' => '19273', - 'Turgon (16350)' => '16389', - 'Tursac (24620)' => '24559', - 'Tusson (16140)' => '16390', - 'Tuzie (16700)' => '16391', - 'Uchacq-et-Parentis (40090)' => '40320', - 'Uhart-Cize (64220)' => '64538', - 'Uhart-Mixe (64120)' => '64539', - 'Urcuit (64990)' => '64540', - 'Urdès (64370)' => '64541', - 'Urdos (64490)' => '64542', - 'Urepel (64430)' => '64543', - 'Urgons (40320)' => '40321', - 'Urost (64160)' => '64544', - 'Urrugne (64122)' => '64545', - 'Urt (64240)' => '64546', - 'Urval (24480)' => '24560', - 'Ussac (19270)' => '19274', - 'Usseau (79210)' => '79334', - 'Usseau (86230)' => '86275', - 'Ussel (19200)' => '19275', - 'Usson-du-Poitou (86350)' => '86276', - 'Ustaritz (64480)' => '64547', - 'Uza (40170)' => '40322', - 'Uzan (64370)' => '64548', - 'Uzein (64230)' => '64549', - 'Uzerche (19140)' => '19276', - 'Uzeste (33730)' => '33537', - 'Uzos (64110)' => '64550', - 'Val d\'Issoire (87330)' => '87097', - 'Val de Virvée (33240)' => '33018', - 'Val des Vignes (16250)' => '16175', - 'Valdivienne (86300)' => '86233', - 'Valence (16460)' => '16392', - 'Valeuil (24310)' => '24561', - 'Valeyrac (33340)' => '33538', - 'Valiergues (19200)' => '19277', - 'Vallans (79270)' => '79335', - 'Vallereuil (24190)' => '24562', - 'Vallière (23120)' => '23257', - 'Valojoulx (24290)' => '24563', - 'Vançais (79120)' => '79336', - 'Vandré (17700)' => '17457', - 'Vanxains (24600)' => '24564', - 'Vanzac (17500)' => '17458', - 'Vanzay (79120)' => '79338', - 'Varaignes (24360)' => '24565', - 'Varaize (17400)' => '17459', - 'Vareilles (23300)' => '23258', - 'Varennes (24150)' => '24566', - 'Varennes (86110)' => '86277', - 'Varès (47400)' => '47316', - 'Varetz (19240)' => '19278', - 'Vars (16330)' => '16393', - 'Vars-sur-Roseix (19130)' => '19279', - 'Varzay (17460)' => '17460', - 'Vasles (79340)' => '79339', - 'Vaulry (87140)' => '87198', - 'Vaunac (24800)' => '24567', - 'Vausseroux (79420)' => '79340', - 'Vautebis (79420)' => '79341', - 'Vaux (86700)' => '86278', - 'Vaux-Lavalette (16320)' => '16394', - 'Vaux-Rouillac (16170)' => '16395', - 'Vaux-sur-Mer (17640)' => '17461', - 'Vaux-sur-Vienne (86220)' => '86279', - 'Vayres (33870)' => '33539', - 'Vayres (87600)' => '87199', - 'Végennes (19120)' => '19280', - 'Veix (19260)' => '19281', - 'Vélines (24230)' => '24568', - 'Vellèches (86230)' => '86280', - 'Vendays-Montalivet (33930)' => '33540', - 'Vendeuvre-du-Poitou (86380)' => '86281', - 'Vendoire (24320)' => '24569', - 'Vénérand (17100)' => '17462', - 'Vensac (33590)' => '33541', - 'Ventouse (16460)' => '16396', - 'Vérac (33240)' => '33542', - 'Verdelais (33490)' => '33543', - 'Verdets (64400)' => '64551', - 'Verdille (16140)' => '16397', - 'Verdon (24520)' => '24570', - 'Vergeroux (17300)' => '17463', - 'Vergné (17330)' => '17464', - 'Vergt (24380)' => '24571', - 'Vergt-de-Biron (24540)' => '24572', - 'Vérines (17540)' => '17466', - 'Verneiges (23170)' => '23259', - 'Verneuil (16310)' => '16398', - 'Verneuil-Moustiers (87360)' => '87200', - 'Verneuil-sur-Vienne (87430)' => '87201', - 'Vernon (86340)' => '86284', - 'Vernoux-en-Gâtine (79240)' => '79342', - 'Vernoux-sur-Boutonne (79170)' => '79343', - 'Verrières (16130)' => '16399', - 'Verrières (86410)' => '86285', - 'Verrue (86420)' => '86286', - 'Verruyes (79310)' => '79345', - 'Vert (40420)' => '40323', - 'Verteillac (24320)' => '24573', - 'Verteuil-d\'Agenais (47260)' => '47317', - 'Verteuil-sur-Charente (16510)' => '16400', - 'Vertheuil (33180)' => '33545', - 'Vervant (16330)' => '16401', - 'Vervant (17400)' => '17467', - 'Veyrac (87520)' => '87202', - 'Veyrières (19200)' => '19283', - 'Veyrignac (24370)' => '24574', - 'Veyrines-de-Domme (24250)' => '24575', - 'Veyrines-de-Vergt (24380)' => '24576', - 'Vézac (24220)' => '24577', - 'Vézières (86120)' => '86287', - 'Vialer (64330)' => '64552', - 'Viam (19170)' => '19284', - 'Vianne (47230)' => '47318', - 'Vibrac (16120)' => '16402', - 'Vibrac (17130)' => '17468', - 'Vicq-d\'Auribat (40380)' => '40324', - 'Vicq-sur-Breuilh (87260)' => '87203', - 'Vicq-sur-Gartempe (86260)' => '86288', - 'Vidaillat (23250)' => '23260', - 'Videix (87600)' => '87204', - 'Vielle-Saint-Girons (40560)' => '40326', - 'Vielle-Soubiran (40240)' => '40327', - 'Vielle-Tursan (40320)' => '40325', - 'Viellenave-d\'Arthez (64170)' => '64554', - 'Viellenave-de-Navarrenx (64190)' => '64555', - 'Vielleségure (64150)' => '64556', - 'Viennay (79200)' => '79347', - 'Viersat (23170)' => '23261', - 'Vieux-Boucau-les-Bains (40480)' => '40328', - 'Vieux-Mareuil (24340)' => '24579', - 'Vieux-Ruffec (16350)' => '16404', - 'Vigeois (19410)' => '19285', - 'Vigeville (23140)' => '23262', - 'Vignes (64410)' => '64557', - 'Vignolles (16300)' => '16405', - 'Vignols (19130)' => '19286', - 'Vignonet (33330)' => '33546', - 'Vilhonneur (16220)' => '16406', - 'Villac (24120)' => '24580', - 'Villamblard (24140)' => '24581', - 'Villandraut (33730)' => '33547', - 'Villard (23800)' => '23263', - 'Villars (24530)' => '24582', - 'Villars-en-Pons (17260)' => '17469', - 'Villars-les-Bois (17770)' => '17470', - 'Villebois-Lavalette (16320)' => '16408', - 'Villebramar (47380)' => '47319', - 'Villedoux (17230)' => '17472', - 'Villefagnan (16240)' => '16409', - 'Villefavard (87190)' => '87206', - 'Villefollet (79170)' => '79348', - 'Villefranche-de-Lonchat (24610)' => '24584', - 'Villefranche-du-Périgord (24550)' => '24585', - 'Villefranche-du-Queyran (47160)' => '47320', - 'Villefranque (64990)' => '64558', - 'Villegats (16700)' => '16410', - 'Villegouge (33141)' => '33548', - 'Villejésus (16140)' => '16411', - 'Villejoubert (16560)' => '16412', - 'Villemain (79110)' => '79349', - 'Villemorin (17470)' => '17473', - 'Villemort (86310)' => '86291', - 'Villenave (40110)' => '40330', - 'Villenave-d\'Ornon (33140)' => '33550', - 'Villenave-de-Rions (33550)' => '33549', - 'Villenave-près-Béarn (65500)' => '65476', - 'Villeneuve (33710)' => '33551', - 'Villeneuve-de-Duras (47120)' => '47321', - 'Villeneuve-de-Marsan (40190)' => '40331', - 'Villeneuve-la-Comtesse (17330)' => '17474', - 'Villeneuve-sur-Lot (47300)' => '47323', - 'Villeréal (47210)' => '47324', - 'Villeton (47400)' => '47325', - 'Villetoureix (24600)' => '24586', - 'Villexavier (17500)' => '17476', - 'Villiers (86190)' => '86292', - 'Villiers-Couture (17510)' => '17477', - 'Villiers-en-Bois (79360)' => '79350', - 'Villiers-en-Plaine (79160)' => '79351', - 'Villiers-le-Roux (16240)' => '16413', - 'Villiers-sur-Chizé (79170)' => '79352', - 'Villognon (16230)' => '16414', - 'Vinax (17510)' => '17478', - 'Vindelle (16430)' => '16415', - 'Viodos-Abense-de-Bas (64130)' => '64559', - 'Virazeil (47200)' => '47326', - 'Virelade (33720)' => '33552', - 'Virollet (17260)' => '17479', - 'Virsac (33240)' => '33553', - 'Virson (17290)' => '17480', - 'Vitrac (24200)' => '24587', - 'Vitrac-Saint-Vincent (16310)' => '16416', - 'Vitrac-sur-Montane (19800)' => '19287', - 'Viven (64450)' => '64560', - 'Viville (16120)' => '16417', - 'Vivonne (86370)' => '86293', - 'Voeuil-et-Giget (16400)' => '16418', - 'Voissay (17400)' => '17481', - 'Vouharte (16330)' => '16419', - 'Vouhé (17700)' => '17482', - 'Vouhé (79310)' => '79354', - 'Vouillé (79230)' => '79355', - 'Vouillé (86190)' => '86294', - 'Voulême (86400)' => '86295', - 'Voulgézac (16250)' => '16420', - 'Voulmentin (79150)' => '79242', - 'Voulon (86700)' => '86296', - 'Vouneuil-sous-Biard (86580)' => '86297', - 'Vouneuil-sur-Vienne (86210)' => '86298', - 'Voutezac (19130)' => '19288', - 'Vouthon (16220)' => '16421', - 'Vouzailles (86170)' => '86299', - 'Vouzan (16410)' => '16422', - 'Xaintrailles (47230)' => '47327', - 'Xaintray (79220)' => '79357', - 'Xambes (16330)' => '16423', - 'Ychoux (40160)' => '40332', - 'Ygos-Saint-Saturnin (40110)' => '40333', - 'Yssandon (19310)' => '19289', - 'Yversay (86170)' => '86300', - 'Yves (17340)' => '17483', - 'Yviers (16210)' => '16424', - 'Yvrac (33370)' => '33554', - 'Yvrac-et-Malleyrand (16110)' => '16425', - 'Yzosse (40180)' => '40334' - ); + const CITIES = [ + 'Aast (64460)' => '64001', + 'Abère (64160)' => '64002', + 'Abidos (64150)' => '64003', + 'Abitain (64390)' => '64004', + 'Abjat-sur-Bandiat (24300)' => '24001', + 'Abos (64360)' => '64005', + 'Abzac (16500)' => '16001', + 'Abzac (33230)' => '33001', + 'Accous (64490)' => '64006', + 'Adilly (79200)' => '79002', + 'Adriers (86430)' => '86001', + 'Affieux (19260)' => '19001', + 'Agen (47000)' => '47001', + 'Agmé (47350)' => '47002', + 'Agnac (47800)' => '47003', + 'Agnos (64400)' => '64007', + 'Agonac (24460)' => '24002', + 'Agris (16110)' => '16003', + 'Agudelle (17500)' => '17002', + 'Ahaxe-Alciette-Bascassan (64220)' => '64008', + 'Ahetze (64210)' => '64009', + 'Ahun (23150)' => '23001', + 'Aïcirits-Camou-Suhast (64120)' => '64010', + 'Aiffres (79230)' => '79003', + 'Aignes-et-Puypéroux (16190)' => '16004', + 'Aigonnay (79370)' => '79004', + 'Aigre (16140)' => '16005', + 'Aigrefeuille-d\'Aunis (17290)' => '17003', + 'Aiguillon (47190)' => '47004', + 'Aillas (33124)' => '33002', + 'Aincille (64220)' => '64011', + 'Ainharp (64130)' => '64012', + 'Ainhice-Mongelos (64220)' => '64013', + 'Ainhoa (64250)' => '64014', + 'Aire-sur-l\'Adour (40800)' => '40001', + 'Airvault (79600)' => '79005', + 'Aix (19200)' => '19002', + 'Aixe-sur-Vienne (87700)' => '87001', + 'Ajain (23380)' => '23002', + 'Ajat (24210)' => '24004', + 'Albignac (19190)' => '19003', + 'Albussac (19380)' => '19004', + 'Alçay-Alçabéhéty-Sunharette (64470)' => '64015', + 'Aldudes (64430)' => '64016', + 'Allas-Bocage (17150)' => '17005', + 'Allas-Champagne (17500)' => '17006', + 'Allas-les-Mines (24220)' => '24006', + 'Allassac (19240)' => '19005', + 'Allemans (24600)' => '24007', + 'Allemans-du-Dropt (47800)' => '47005', + 'Alles-sur-Dordogne (24480)' => '24005', + 'Alleyrat (19200)' => '19006', + 'Alleyrat (23200)' => '23003', + 'Allez-et-Cazeneuve (47110)' => '47006', + 'Allonne (79130)' => '79007', + 'Allons (47420)' => '47007', + 'Alloue (16490)' => '16007', + 'Alos-Sibas-Abense (64470)' => '64017', + 'Altillac (19120)' => '19007', + 'Amailloux (79350)' => '79008', + 'Ambarès-et-Lagrave (33440)' => '33003', + 'Ambazac (87240)' => '87002', + 'Ambérac (16140)' => '16008', + 'Ambernac (16490)' => '16009', + 'Amberre (86110)' => '86002', + 'Ambès (33810)' => '33004', + 'Ambleville (16300)' => '16010', + 'Ambrugeat (19250)' => '19008', + 'Ambrus (47160)' => '47008', + 'Amendeuix-Oneix (64120)' => '64018', + 'Amorots-Succos (64120)' => '64019', + 'Amou (40330)' => '40002', + 'Amuré (79210)' => '79009', + 'Anais (16560)' => '16011', + 'Anais (17540)' => '17007', + 'Ance (64570)' => '64020', + 'Anché (86700)' => '86003', + 'Andernos-les-Bains (33510)' => '33005', + 'Andilly (17230)' => '17008', + 'Andiran (47170)' => '47009', + 'Andoins (64420)' => '64021', + 'Andrein (64390)' => '64022', + 'Angaïs (64510)' => '64023', + 'Angeac-Champagne (16130)' => '16012', + 'Angeac-Charente (16120)' => '16013', + 'Angeduc (16300)' => '16014', + 'Anglade (33390)' => '33006', + 'Angles-sur-l\'Anglin (86260)' => '86004', + 'Anglet (64600)' => '64024', + 'Angliers (17540)' => '17009', + 'Angliers (86330)' => '86005', + 'Angoisse (24270)' => '24008', + 'Angoulême (16000)' => '16015', + 'Angoulins (17690)' => '17010', + 'Angoumé (40990)' => '40003', + 'Angous (64190)' => '64025', + 'Angresse (40150)' => '40004', + 'Anhaux (64220)' => '64026', + 'Anlhiac (24160)' => '24009', + 'Annepont (17350)' => '17011', + 'Annesse-et-Beaulieu (24430)' => '24010', + 'Annezay (17380)' => '17012', + 'Anos (64160)' => '64027', + 'Anoye (64350)' => '64028', + 'Ansac-sur-Vienne (16500)' => '16016', + 'Antagnac (47700)' => '47010', + 'Antezant-la-Chapelle (17400)' => '17013', + 'Anthé (47370)' => '47011', + 'Antigny (86310)' => '86006', + 'Antonne-et-Trigonant (24420)' => '24011', + 'Antran (86100)' => '86007', + 'Anville (16170)' => '16017', + 'Anzême (23000)' => '23004', + 'Anzex (47700)' => '47012', + 'Aramits (64570)' => '64029', + 'Arancou (64270)' => '64031', + 'Araujuzon (64190)' => '64032', + 'Araux (64190)' => '64033', + 'Arbanats (33640)' => '33007', + 'Arbérats-Sillègue (64120)' => '64034', + 'Arbis (33760)' => '33008', + 'Arbonne (64210)' => '64035', + 'Arboucave (40320)' => '40005', + 'Arbouet-Sussaute (64120)' => '64036', + 'Arbus (64230)' => '64037', + 'Arcachon (33120)' => '33009', + 'Arçais (79210)' => '79010', + 'Arcangues (64200)' => '64038', + 'Arçay (86200)' => '86008', + 'Arces (17120)' => '17015', + 'Archiac (17520)' => '17016', + 'Archignac (24590)' => '24012', + 'Archigny (86210)' => '86009', + 'Archingeay (17380)' => '17017', + 'Arcins (33460)' => '33010', + 'Ardilleux (79110)' => '79011', + 'Ardillières (17290)' => '17018', + 'Ardin (79160)' => '79012', + 'Aren (64400)' => '64039', + 'Arengosse (40110)' => '40006', + 'Arès (33740)' => '33011', + 'Aressy (64320)' => '64041', + 'Arette (64570)' => '64040', + 'Arfeuille-Châtain (23700)' => '23005', + 'Argagnon (64300)' => '64042', + 'Argelos (40700)' => '40007', + 'Argelos (64450)' => '64043', + 'Argelouse (40430)' => '40008', + 'Argentat (19400)' => '19010', + 'Argenton (47250)' => '47013', + 'Argenton-l\'Église (79290)' => '79014', + 'Argentonnay (79150)' => '79013', + 'Arget (64410)' => '64044', + 'Arhansus (64120)' => '64045', + 'Arjuzanx (40110)' => '40009', + 'Armendarits (64640)' => '64046', + 'Armillac (47800)' => '47014', + 'Arnac-la-Poste (87160)' => '87003', + 'Arnac-Pompadour (19230)' => '19011', + 'Arnéguy (64220)' => '64047', + 'Arnos (64370)' => '64048', + 'Aroue-Ithorots-Olhaïby (64120)' => '64049', + 'Arrast-Larrebieu (64130)' => '64050', + 'Arraute-Charritte (64120)' => '64051', + 'Arrènes (23210)' => '23006', + 'Arricau-Bordes (64350)' => '64052', + 'Arrien (64420)' => '64053', + 'Arros-de-Nay (64800)' => '64054', + 'Arrosès (64350)' => '64056', + 'Ars (16130)' => '16018', + 'Ars (23480)' => '23007', + 'Ars-en-Ré (17590)' => '17019', + 'Arsac (33460)' => '33012', + 'Arsague (40330)' => '40011', + 'Artassenx (40090)' => '40012', + 'Arthenac (17520)' => '17020', + 'Arthez-d\'Armagnac (40190)' => '40013', + 'Arthez-d\'Asson (64800)' => '64058', + 'Arthez-de-Béarn (64370)' => '64057', + 'Artigueloutan (64420)' => '64059', + 'Artiguelouve (64230)' => '64060', + 'Artigues-près-Bordeaux (33370)' => '33013', + 'Artix (64170)' => '64061', + 'Arudy (64260)' => '64062', + 'Arue (40120)' => '40014', + 'Arvert (17530)' => '17021', + 'Arveyres (33500)' => '33015', + 'Arx (40310)' => '40015', + 'Arzacq-Arraziguet (64410)' => '64063', + 'Asasp-Arros (64660)' => '64064', + 'Ascain (64310)' => '64065', + 'Ascarat (64220)' => '64066', + 'Aslonnes (86340)' => '86010', + 'Asnières-en-Poitou (79170)' => '79015', + 'Asnières-la-Giraud (17400)' => '17022', + 'Asnières-sur-Blour (86430)' => '86011', + 'Asnières-sur-Nouère (16290)' => '16019', + 'Asnois (86250)' => '86012', + 'Asques (33240)' => '33016', + 'Assais-les-Jumeaux (79600)' => '79016', + 'Assat (64510)' => '64067', + 'Asson (64800)' => '64068', + 'Astaffort (47220)' => '47015', + 'Astaillac (19120)' => '19012', + 'Aste-Béon (64260)' => '64069', + 'Astis (64450)' => '64070', + 'Athos-Aspis (64390)' => '64071', + 'Aubagnan (40700)' => '40016', + 'Aubas (24290)' => '24014', + 'Aubazines (19190)' => '19013', + 'Aubertin (64290)' => '64072', + 'Aubeterre-sur-Dronne (16390)' => '16020', + 'Aubiac (33430)' => '33017', + 'Aubiac (47310)' => '47016', + 'Aubigné (79110)' => '79018', + 'Aubigny (79390)' => '79019', + 'Aubin (64230)' => '64073', + 'Aubous (64330)' => '64074', + 'Aubusson (23200)' => '23008', + 'Audaux (64190)' => '64075', + 'Audenge (33980)' => '33019', + 'Audignon (40500)' => '40017', + 'Audon (40400)' => '40018', + 'Audrix (24260)' => '24015', + 'Auga (64450)' => '64077', + 'Auge (23170)' => '23009', + 'Augé (79400)' => '79020', + 'Auge-Saint-Médard (16170)' => '16339', + 'Augères (23210)' => '23010', + 'Augignac (24300)' => '24016', + 'Augne (87120)' => '87004', + 'Aujac (17770)' => '17023', + 'Aulnay (17470)' => '17024', + 'Aulnay (86330)' => '86013', + 'Aulon (23210)' => '23011', + 'Aumagne (17770)' => '17025', + 'Aunac (16460)' => '16023', + 'Auradou (47140)' => '47017', + 'Aureil (87220)' => '87005', + 'Aureilhan (40200)' => '40019', + 'Auriac (19220)' => '19014', + 'Auriac (64450)' => '64078', + 'Auriac-du-Périgord (24290)' => '24018', + 'Auriac-sur-Dropt (47120)' => '47018', + 'Auriat (23400)' => '23012', + 'Aurice (40500)' => '40020', + 'Auriolles (33790)' => '33020', + 'Aurions-Idernes (64350)' => '64079', + 'Auros (33124)' => '33021', + 'Aussac-Vadalle (16560)' => '16024', + 'Aussevielle (64230)' => '64080', + 'Aussurucq (64130)' => '64081', + 'Auterrive (64270)' => '64082', + 'Autevielle-Saint-Martin-Bideren (64390)' => '64083', + 'Authon-Ébéon (17770)' => '17026', + 'Auzances (23700)' => '23013', + 'Availles-en-Châtellerault (86530)' => '86014', + 'Availles-Limouzine (86460)' => '86015', + 'Availles-Thouarsais (79600)' => '79022', + 'Avanton (86170)' => '86016', + 'Avensan (33480)' => '33022', + 'Avon (79800)' => '79023', + 'Avy (17800)' => '17027', + 'Aydie (64330)' => '64084', + 'Aydius (64490)' => '64085', + 'Ayen (19310)' => '19015', + 'Ayguemorte-les-Graves (33640)' => '33023', + 'Ayherre (64240)' => '64086', + 'Ayron (86190)' => '86017', + 'Aytré (17440)' => '17028', + 'Azat-Châtenet (23210)' => '23014', + 'Azat-le-Ris (87360)' => '87006', + 'Azay-le-Brûlé (79400)' => '79024', + 'Azay-sur-Thouet (79130)' => '79025', + 'Azerables (23160)' => '23015', + 'Azerat (24210)' => '24019', + 'Azur (40140)' => '40021', + 'Badefols-d\'Ans (24390)' => '24021', + 'Badefols-sur-Dordogne (24150)' => '24022', + 'Bagas (33190)' => '33024', + 'Bagnizeau (17160)' => '17029', + 'Bahus-Soubiran (40320)' => '40022', + 'Baigneaux (33760)' => '33025', + 'Baignes-Sainte-Radegonde (16360)' => '16025', + 'Baigts (40380)' => '40023', + 'Baigts-de-Béarn (64300)' => '64087', + 'Bajamont (47480)' => '47019', + 'Balansun (64300)' => '64088', + 'Balanzac (17600)' => '17030', + 'Baleix (64460)' => '64089', + 'Baleyssagues (47120)' => '47020', + 'Baliracq-Maumusson (64330)' => '64090', + 'Baliros (64510)' => '64091', + 'Balizac (33730)' => '33026', + 'Ballans (17160)' => '17031', + 'Balledent (87290)' => '87007', + 'Ballon (17290)' => '17032', + 'Balzac (16430)' => '16026', + 'Banca (64430)' => '64092', + 'Baneuil (24150)' => '24023', + 'Banize (23120)' => '23016', + 'Banos (40500)' => '40024', + 'Bar (19800)' => '19016', + 'Barbaste (47230)' => '47021', + 'Barbezières (16140)' => '16027', + 'Barbezieux-Saint-Hilaire (16300)' => '16028', + 'Barcus (64130)' => '64093', + 'Bardenac (16210)' => '16029', + 'Bardos (64520)' => '64094', + 'Bardou (24560)' => '24024', + 'Barie (33190)' => '33027', + 'Barinque (64160)' => '64095', + 'Baron (33750)' => '33028', + 'Barraute-Camu (64390)' => '64096', + 'Barret (16300)' => '16030', + 'Barro (16700)' => '16031', + 'Bars (24210)' => '24025', + 'Barsac (33720)' => '33030', + 'Barzan (17120)' => '17034', + 'Barzun (64530)' => '64097', + 'Bas-Mauco (40500)' => '40026', + 'Bascons (40090)' => '40025', + 'Bassac (16120)' => '16032', + 'Bassanne (33190)' => '33031', + 'Bassens (33530)' => '33032', + 'Bassercles (40700)' => '40027', + 'Basses (86200)' => '86018', + 'Bassignac-le-Bas (19430)' => '19017', + 'Bassignac-le-Haut (19220)' => '19018', + 'Bassillac (24330)' => '24026', + 'Bassillon-Vauzé (64350)' => '64098', + 'Bassussarry (64200)' => '64100', + 'Bastanès (64190)' => '64099', + 'Bastennes (40360)' => '40028', + 'Basville (23260)' => '23017', + 'Bats (40320)' => '40029', + 'Baudignan (40310)' => '40030', + 'Baudreix (64800)' => '64101', + 'Baurech (33880)' => '33033', + 'Bayac (24150)' => '24027', + 'Bayas (33230)' => '33034', + 'Bayers (16460)' => '16033', + 'Bayon-sur-Gironde (33710)' => '33035', + 'Bayonne (64100)' => '64102', + 'Bazac (16210)' => '16034', + 'Bazas (33430)' => '33036', + 'Bazauges (17490)' => '17035', + 'Bazelat (23160)' => '23018', + 'Bazens (47130)' => '47022', + 'Beaugas (47290)' => '47023', + 'Beaugeay (17620)' => '17036', + 'Beaulieu-sous-Parthenay (79420)' => '79029', + 'Beaulieu-sur-Dordogne (19120)' => '19019', + 'Beaulieu-sur-Sonnette (16450)' => '16035', + 'Beaumont (19390)' => '19020', + 'Beaumont (86490)' => '86019', + 'Beaumont-du-Lac (87120)' => '87009', + 'Beaumontois en Périgord (24440)' => '24028', + 'Beaupouyet (24400)' => '24029', + 'Beaupuy (47200)' => '47024', + 'Beauregard-de-Terrasson (24120)' => '24030', + 'Beauregard-et-Bassac (24140)' => '24031', + 'Beauronne (24400)' => '24032', + 'Beaussac (24340)' => '24033', + 'Beaussais-Vitré (79370)' => '79030', + 'Beautiran (33640)' => '33037', + 'Beauvais-sur-Matha (17490)' => '17037', + 'Beauville (47470)' => '47025', + 'Beauvoir-sur-Niort (79360)' => '79031', + 'Beauziac (47700)' => '47026', + 'Béceleuf (79160)' => '79032', + 'Bécheresse (16250)' => '16036', + 'Bédeille (64460)' => '64103', + 'Bedenac (17210)' => '17038', + 'Bedous (64490)' => '64104', + 'Bégaar (40400)' => '40031', + 'Bégadan (33340)' => '33038', + 'Bègles (33130)' => '33039', + 'Béguey (33410)' => '33040', + 'Béguios (64120)' => '64105', + 'Béhasque-Lapiste (64120)' => '64106', + 'Béhorléguy (64220)' => '64107', + 'Beissat (23260)' => '23019', + 'Beleymas (24140)' => '24034', + 'Belhade (40410)' => '40032', + 'Belin-Béliet (33830)' => '33042', + 'Bélis (40120)' => '40033', + 'Bellac (87300)' => '87011', + 'Bellebat (33760)' => '33043', + 'Bellechassagne (19290)' => '19021', + 'Bellefond (33760)' => '33044', + 'Bellefonds (86210)' => '86020', + 'Bellegarde-en-Marche (23190)' => '23020', + 'Belleville (79360)' => '79033', + 'Bellocq (64270)' => '64108', + 'Bellon (16210)' => '16037', + 'Belluire (17800)' => '17039', + 'Bélus (40300)' => '40034', + 'Belvès-de-Castillon (33350)' => '33045', + 'Benassay (86470)' => '86021', + 'Benayes (19510)' => '19022', + 'Bénéjacq (64800)' => '64109', + 'Bénesse-lès-Dax (40180)' => '40035', + 'Bénesse-Maremne (40230)' => '40036', + 'Benest (16350)' => '16038', + 'Bénévent-l\'Abbaye (23210)' => '23021', + 'Benon (17170)' => '17041', + 'Benquet (40280)' => '40037', + 'Bentayou-Sérée (64460)' => '64111', + 'Béost (64440)' => '64110', + 'Berbiguières (24220)' => '24036', + 'Bercloux (17770)' => '17042', + 'Bérenx (64300)' => '64112', + 'Bergerac (24100)' => '24037', + 'Bergouey (40250)' => '40038', + 'Bergouey-Viellenave (64270)' => '64113', + 'Bernac (16700)' => '16039', + 'Bernadets (64160)' => '64114', + 'Bernay-Saint-Martin (17330)' => '17043', + 'Berneuil (16480)' => '16040', + 'Berneuil (17460)' => '17044', + 'Berneuil (87300)' => '87012', + 'Bernos-Beaulac (33430)' => '33046', + 'Berrie (86120)' => '86022', + 'Berrogain-Laruns (64130)' => '64115', + 'Bersac-sur-Rivalier (87370)' => '87013', + 'Berson (33390)' => '33047', + 'Berthegon (86420)' => '86023', + 'Berthez (33124)' => '33048', + 'Bertric-Burée (24320)' => '24038', + 'Béruges (86190)' => '86024', + 'Bescat (64260)' => '64116', + 'Bésingrand (64150)' => '64117', + 'Bessac (16250)' => '16041', + 'Bessé (16140)' => '16042', + 'Besse (24550)' => '24039', + 'Bessines (79000)' => '79034', + 'Bessines-sur-Gartempe (87250)' => '87014', + 'Betbezer-d\'Armagnac (40240)' => '40039', + 'Bétête (23270)' => '23022', + 'Béthines (86310)' => '86025', + 'Bétracq (64350)' => '64118', + 'Beurlay (17250)' => '17045', + 'Beuste (64800)' => '64119', + 'Beuxes (86120)' => '86026', + 'Beychac-et-Caillau (33750)' => '33049', + 'Beylongue (40370)' => '40040', + 'Beynac (87700)' => '87015', + 'Beynac-et-Cazenac (24220)' => '24040', + 'Beynat (19190)' => '19023', + 'Beyrie-en-Béarn (64230)' => '64121', + 'Beyrie-sur-Joyeuse (64120)' => '64120', + 'Beyries (40700)' => '40041', + 'Beyssac (19230)' => '19024', + 'Beyssenac (19230)' => '19025', + 'Bézenac (24220)' => '24041', + 'Biard (86580)' => '86027', + 'Biarritz (64200)' => '64122', + 'Biarrotte (40390)' => '40042', + 'Bias (40170)' => '40043', + 'Bias (47300)' => '47027', + 'Biaudos (40390)' => '40044', + 'Bidache (64520)' => '64123', + 'Bidarray (64780)' => '64124', + 'Bidart (64210)' => '64125', + 'Bidos (64400)' => '64126', + 'Bielle (64260)' => '64127', + 'Bieujac (33210)' => '33050', + 'Biganos (33380)' => '33051', + 'Bignay (17400)' => '17046', + 'Bignoux (86800)' => '86028', + 'Bilhac (19120)' => '19026', + 'Bilhères (64260)' => '64128', + 'Billère (64140)' => '64129', + 'Bioussac (16700)' => '16044', + 'Birac (16120)' => '16045', + 'Birac (33430)' => '33053', + 'Birac-sur-Trec (47200)' => '47028', + 'Biras (24310)' => '24042', + 'Biriatou (64700)' => '64130', + 'Biron (17800)' => '17047', + 'Biron (24540)' => '24043', + 'Biron (64300)' => '64131', + 'Biscarrosse (40600)' => '40046', + 'Bizanos (64320)' => '64132', + 'Blaignac (33190)' => '33054', + 'Blaignan (33340)' => '33055', + 'Blanquefort (33290)' => '33056', + 'Blanquefort-sur-Briolance (47500)' => '47029', + 'Blanzac (87300)' => '87017', + 'Blanzac-lès-Matha (17160)' => '17048', + 'Blanzac-Porcheresse (16250)' => '16046', + 'Blanzaguet-Saint-Cybard (16320)' => '16047', + 'Blanzay (86400)' => '86029', + 'Blanzay-sur-Boutonne (17470)' => '17049', + 'Blasimon (33540)' => '33057', + 'Blaslay (86170)' => '86030', + 'Blaudeix (23140)' => '23023', + 'Blaye (33390)' => '33058', + 'Blaymont (47470)' => '47030', + 'Blésignac (33670)' => '33059', + 'Blessac (23200)' => '23024', + 'Blis-et-Born (24330)' => '24044', + 'Blond (87300)' => '87018', + 'Boé (47550)' => '47031', + 'Boeil-Bezing (64510)' => '64133', + 'Bois (17240)' => '17050', + 'Boisbreteau (16480)' => '16048', + 'Boismé (79300)' => '79038', + 'Boisné-La Tude (16320)' => '16082', + 'Boisredon (17150)' => '17052', + 'Boisse (24560)' => '24045', + 'Boisserolles (79360)' => '79039', + 'Boisseuil (87220)' => '87019', + 'Boisseuilh (24390)' => '24046', + 'Bommes (33210)' => '33060', + 'Bon-Encontre (47240)' => '47032', + 'Bonloc (64240)' => '64134', + 'Bonnac-la-Côte (87270)' => '87020', + 'Bonnat (23220)' => '23025', + 'Bonnefond (19170)' => '19027', + 'Bonnegarde (40330)' => '40047', + 'Bonnes (16390)' => '16049', + 'Bonnes (86300)' => '86031', + 'Bonnetan (33370)' => '33061', + 'Bonneuil (16120)' => '16050', + 'Bonneuil-Matours (86210)' => '86032', + 'Bonneville (16170)' => '16051', + 'Bonneville-et-Saint-Avit-de-Fumadières (24230)' => '24048', + 'Bonnut (64300)' => '64135', + 'Bonzac (33910)' => '33062', + 'Boos (40370)' => '40048', + 'Borce (64490)' => '64136', + 'Bord-Saint-Georges (23230)' => '23026', + 'Bordeaux (33000)' => '33063', + 'Bordères (64800)' => '64137', + 'Bordères-et-Lamensans (40270)' => '40049', + 'Bordes (64510)' => '64138', + 'Bords (17430)' => '17053', + 'Boresse-et-Martron (17270)' => '17054', + 'Borrèze (24590)' => '24050', + 'Bors (Canton de Baignes-Sainte-Radegonde) (16360)' => '16053', + 'Bors (Canton de Montmoreau-Saint-Cybard) (16190)' => '16052', + 'Bort-les-Orgues (19110)' => '19028', + 'Boscamnant (17360)' => '17055', + 'Bosdarros (64290)' => '64139', + 'Bosmie-l\'Aiguille (87110)' => '87021', + 'Bosmoreau-les-Mines (23400)' => '23027', + 'Bosroger (23200)' => '23028', + 'Bosset (24130)' => '24051', + 'Bossugan (33350)' => '33064', + 'Bostens (40090)' => '40050', + 'Boucau (64340)' => '64140', + 'Boudy-de-Beauregard (47290)' => '47033', + 'Boueilh-Boueilho-Lasque (64330)' => '64141', + 'Bouëx (16410)' => '16055', + 'Bougarber (64230)' => '64142', + 'Bouglon (47250)' => '47034', + 'Bougneau (17800)' => '17056', + 'Bougon (79800)' => '79042', + 'Bougue (40090)' => '40051', + 'Bouhet (17540)' => '17057', + 'Bouillac (24480)' => '24052', + 'Bouillé-Loretz (79290)' => '79043', + 'Bouillé-Saint-Paul (79290)' => '79044', + 'Bouillon (64410)' => '64143', + 'Bouin (79110)' => '79045', + 'Boulazac Isle Manoire (24750)' => '24053', + 'Bouliac (33270)' => '33065', + 'Boumourt (64370)' => '64144', + 'Bouniagues (24560)' => '24054', + 'Bourcefranc-le-Chapus (17560)' => '17058', + 'Bourdalat (40190)' => '40052', + 'Bourdeilles (24310)' => '24055', + 'Bourdelles (33190)' => '33066', + 'Bourdettes (64800)' => '64145', + 'Bouresse (86410)' => '86034', + 'Bourg (33710)' => '33067', + 'Bourg-Archambault (86390)' => '86035', + 'Bourg-Charente (16200)' => '16056', + 'Bourg-des-Maisons (24320)' => '24057', + 'Bourg-du-Bost (24600)' => '24058', + 'Bourganeuf (23400)' => '23030', + 'Bourgnac (24400)' => '24059', + 'Bourgneuf (17220)' => '17059', + 'Bourgougnague (47410)' => '47035', + 'Bourideys (33113)' => '33068', + 'Bourlens (47370)' => '47036', + 'Bournand (86120)' => '86036', + 'Bournel (47210)' => '47037', + 'Bourniquel (24150)' => '24060', + 'Bournos (64450)' => '64146', + 'Bourran (47320)' => '47038', + 'Bourriot-Bergonce (40120)' => '40053', + 'Bourrou (24110)' => '24061', + 'Boussac (23600)' => '23031', + 'Boussac-Bourg (23600)' => '23032', + 'Boussais (79600)' => '79047', + 'Boussès (47420)' => '47039', + 'Bouteilles-Saint-Sébastien (24320)' => '24062', + 'Boutenac-Touvent (17120)' => '17060', + 'Bouteville (16120)' => '16057', + 'Boutiers-Saint-Trojan (16100)' => '16058', + 'Bouzic (24250)' => '24063', + 'Brach (33480)' => '33070', + 'Bran (17210)' => '17061', + 'Branceilles (19500)' => '19029', + 'Branne (33420)' => '33071', + 'Brannens (33124)' => '33072', + 'Brantôme en Périgord (24310)' => '24064', + 'Brassempouy (40330)' => '40054', + 'Braud-et-Saint-Louis (33820)' => '33073', + 'Brax (47310)' => '47040', + 'Bresdon (17490)' => '17062', + 'Bressuire (79300)' => '79049', + 'Bretagne-de-Marsan (40280)' => '40055', + 'Bretignolles (79140)' => '79050', + 'Brettes (16240)' => '16059', + 'Breuil-la-Réorte (17700)' => '17063', + 'Breuil-Magné (17870)' => '17065', + 'Breuilaufa (87300)' => '87022', + 'Breuilh (24380)' => '24065', + 'Breuillet (17920)' => '17064', + 'Bréville (16370)' => '16060', + 'Brie (16590)' => '16061', + 'Brie (79100)' => '79054', + 'Brie-sous-Archiac (17520)' => '17066', + 'Brie-sous-Barbezieux (16300)' => '16062', + 'Brie-sous-Chalais (16210)' => '16063', + 'Brie-sous-Matha (17160)' => '17067', + 'Brie-sous-Mortagne (17120)' => '17068', + 'Brieuil-sur-Chizé (79170)' => '79055', + 'Brignac-la-Plaine (19310)' => '19030', + 'Brigueil-le-Chantre (86290)' => '86037', + 'Brigueuil (16420)' => '16064', + 'Brillac (16500)' => '16065', + 'Brion (86160)' => '86038', + 'Brion-près-Thouet (79290)' => '79056', + 'Brioux-sur-Boutonne (79170)' => '79057', + 'Briscous (64240)' => '64147', + 'Brive-la-Gaillarde (19100)' => '19031', + 'Brives-sur-Charente (17800)' => '17069', + 'Brivezac (19120)' => '19032', + 'Brizambourg (17770)' => '17070', + 'Brocas (40420)' => '40056', + 'Brossac (16480)' => '16066', + 'Brouchaud (24210)' => '24066', + 'Brouqueyran (33124)' => '33074', + 'Brousse (23700)' => '23034', + 'Bruch (47130)' => '47041', + 'Bruges (33520)' => '33075', + 'Bruges-Capbis-Mifaget (64800)' => '64148', + 'Brugnac (47260)' => '47042', + 'Brûlain (79230)' => '79058', + 'Brux (86510)' => '86039', + 'Buanes (40320)' => '40057', + 'Budelière (23170)' => '23035', + 'Budos (33720)' => '33076', + 'Bugeat (19170)' => '19033', + 'Bugnein (64190)' => '64149', + 'Bujaleuf (87460)' => '87024', + 'Bunus (64120)' => '64150', + 'Bunzac (16110)' => '16067', + 'Burgaronne (64390)' => '64151', + 'Burgnac (87800)' => '87025', + 'Burie (17770)' => '17072', + 'Buros (64160)' => '64152', + 'Burosse-Mendousse (64330)' => '64153', + 'Bussac (24350)' => '24069', + 'Bussac-Forêt (17210)' => '17074', + 'Bussac-sur-Charente (17100)' => '17073', + 'Busserolles (24360)' => '24070', + 'Bussière-Badil (24360)' => '24071', + 'Bussière-Dunoise (23320)' => '23036', + 'Bussière-Galant (87230)' => '87027', + 'Bussière-Nouvelle (23700)' => '23037', + 'Bussière-Poitevine (87320)' => '87028', + 'Bussière-Saint-Georges (23600)' => '23038', + 'Bussunarits-Sarrasquette (64220)' => '64154', + 'Bustince-Iriberry (64220)' => '64155', + 'Buxerolles (86180)' => '86041', + 'Buxeuil (37160)' => '86042', + 'Buzet-sur-Baïse (47160)' => '47043', + 'Buziet (64680)' => '64156', + 'Buzy (64260)' => '64157', + 'Cabanac-et-Villagrains (33650)' => '33077', + 'Cabara (33420)' => '33078', + 'Cabariot (17430)' => '17075', + 'Cabidos (64410)' => '64158', + 'Cachen (40120)' => '40058', + 'Cadarsac (33750)' => '33079', + 'Cadaujac (33140)' => '33080', + 'Cadillac (33410)' => '33081', + 'Cadillac-en-Fronsadais (33240)' => '33082', + 'Cadillon (64330)' => '64159', + 'Cagnotte (40300)' => '40059', + 'Cahuzac (47330)' => '47044', + 'Calès (24150)' => '24073', + 'Calignac (47600)' => '47045', + 'Callen (40430)' => '40060', + 'Calonges (47430)' => '47046', + 'Calviac-en-Périgord (24370)' => '24074', + 'Camarsac (33750)' => '33083', + 'Cambes (33880)' => '33084', + 'Cambes (47350)' => '47047', + 'Camblanes-et-Meynac (33360)' => '33085', + 'Cambo-les-Bains (64250)' => '64160', + 'Came (64520)' => '64161', + 'Camiac-et-Saint-Denis (33420)' => '33086', + 'Camiran (33190)' => '33087', + 'Camou-Cihigue (64470)' => '64162', + 'Campagnac-lès-Quercy (24550)' => '24075', + 'Campagne (24260)' => '24076', + 'Campagne (40090)' => '40061', + 'Campet-et-Lamolère (40090)' => '40062', + 'Camps-Saint-Mathurin-Léobazel (19430)' => '19034', + 'Camps-sur-l\'Isle (33660)' => '33088', + 'Campsegret (24140)' => '24077', + 'Campugnan (33390)' => '33089', + 'Cancon (47290)' => '47048', + 'Candresse (40180)' => '40063', + 'Canéjan (33610)' => '33090', + 'Canenx-et-Réaut (40090)' => '40064', + 'Cantenac (33460)' => '33091', + 'Cantillac (24530)' => '24079', + 'Cantois (33760)' => '33092', + 'Capbreton (40130)' => '40065', + 'Capdrot (24540)' => '24080', + 'Capian (33550)' => '33093', + 'Caplong (33220)' => '33094', + 'Captieux (33840)' => '33095', + 'Carbon-Blanc (33560)' => '33096', + 'Carcans (33121)' => '33097', + 'Carcarès-Sainte-Croix (40400)' => '40066', + 'Carcen-Ponson (40400)' => '40067', + 'Cardan (33410)' => '33098', + 'Cardesse (64360)' => '64165', + 'Carignan-de-Bordeaux (33360)' => '33099', + 'Carlux (24370)' => '24081', + 'Caro (64220)' => '64166', + 'Carrère (64160)' => '64167', + 'Carresse-Cassaber (64270)' => '64168', + 'Cars (33390)' => '33100', + 'Carsac-Aillac (24200)' => '24082', + 'Carsac-de-Gurson (24610)' => '24083', + 'Cartelègue (33390)' => '33101', + 'Carves (24170)' => '24084', + 'Cassen (40380)' => '40068', + 'Casseneuil (47440)' => '47049', + 'Casseuil (33190)' => '33102', + 'Cassignas (47340)' => '47050', + 'Castagnède (64270)' => '64170', + 'Castaignos-Souslens (40700)' => '40069', + 'Castandet (40270)' => '40070', + 'Casteide-Cami (64170)' => '64171', + 'Casteide-Candau (64370)' => '64172', + 'Casteide-Doat (64460)' => '64173', + 'Castel-Sarrazin (40330)' => '40074', + 'Castelculier (47240)' => '47051', + 'Casteljaloux (47700)' => '47052', + 'Castella (47340)' => '47053', + 'Castelmoron-d\'Albret (33540)' => '33103', + 'Castelmoron-sur-Lot (47260)' => '47054', + 'Castelnau-Chalosse (40360)' => '40071', + 'Castelnau-de-Médoc (33480)' => '33104', + 'Castelnau-sur-Gupie (47180)' => '47056', + 'Castelnau-Tursan (40320)' => '40072', + 'Castelnaud-de-Gratecambe (47290)' => '47055', + 'Castelnaud-la-Chapelle (24250)' => '24086', + 'Castelner (40700)' => '40073', + 'Castels (24220)' => '24087', + 'Castelviel (33540)' => '33105', + 'Castéra-Loubix (64460)' => '64174', + 'Castet (64260)' => '64175', + 'Castetbon (64190)' => '64176', + 'Castétis (64300)' => '64177', + 'Castetnau-Camblong (64190)' => '64178', + 'Castetner (64300)' => '64179', + 'Castetpugon (64330)' => '64180', + 'Castets (40260)' => '40075', + 'Castets-en-Dorthe (33210)' => '33106', + 'Castillon (Canton d\'Arthez-de-Béarn) (64370)' => '64181', + 'Castillon (Canton de Lembeye) (64350)' => '64182', + 'Castillon-de-Castets (33210)' => '33107', + 'Castillon-la-Bataille (33350)' => '33108', + 'Castillonnès (47330)' => '47057', + 'Castres-Gironde (33640)' => '33109', + 'Caubeyres (47160)' => '47058', + 'Caubios-Loos (64230)' => '64183', + 'Caubon-Saint-Sauveur (47120)' => '47059', + 'Caudecoste (47220)' => '47060', + 'Caudrot (33490)' => '33111', + 'Caumont (33540)' => '33112', + 'Caumont-sur-Garonne (47430)' => '47061', + 'Cauna (40500)' => '40076', + 'Caunay (79190)' => '79060', + 'Cauneille (40300)' => '40077', + 'Caupenne (40250)' => '40078', + 'Cause-de-Clérans (24150)' => '24088', + 'Cauvignac (33690)' => '33113', + 'Cauzac (47470)' => '47062', + 'Cavarc (47330)' => '47063', + 'Cavignac (33620)' => '33114', + 'Cazalis (33113)' => '33115', + 'Cazalis (40700)' => '40079', + 'Cazats (33430)' => '33116', + 'Cazaugitat (33790)' => '33117', + 'Cazères-sur-l\'Adour (40270)' => '40080', + 'Cazideroque (47370)' => '47064', + 'Cazoulès (24370)' => '24089', + 'Ceaux-en-Couhé (86700)' => '86043', + 'Ceaux-en-Loudun (86200)' => '86044', + 'Celle-Lévescault (86600)' => '86045', + 'Cellefrouin (16260)' => '16068', + 'Celles (17520)' => '17076', + 'Celles (24600)' => '24090', + 'Celles-sur-Belle (79370)' => '79061', + 'Cellettes (16230)' => '16069', + 'Cénac (33360)' => '33118', + 'Cénac-et-Saint-Julien (24250)' => '24091', + 'Cendrieux (24380)' => '24092', + 'Cenon (33150)' => '33119', + 'Cenon-sur-Vienne (86530)' => '86046', + 'Cercles (24320)' => '24093', + 'Cercoux (17270)' => '17077', + 'Cère (40090)' => '40081', + 'Cerizay (79140)' => '79062', + 'Cernay (86140)' => '86047', + 'Cérons (33720)' => '33120', + 'Cersay (79290)' => '79063', + 'Cescau (64170)' => '64184', + 'Cessac (33760)' => '33121', + 'Cestas (33610)' => '33122', + 'Cette-Eygun (64490)' => '64185', + 'Ceyroux (23210)' => '23042', + 'Cézac (33620)' => '33123', + 'Chabanais (16150)' => '16070', + 'Chabournay (86380)' => '86048', + 'Chabrac (16150)' => '16071', + 'Chabrignac (19350)' => '19035', + 'Chadenac (17800)' => '17078', + 'Chadurie (16250)' => '16072', + 'Chail (79500)' => '79064', + 'Chaillac-sur-Vienne (87200)' => '87030', + 'Chaillevette (17890)' => '17079', + 'Chalagnac (24380)' => '24094', + 'Chalais (16210)' => '16073', + 'Chalais (24800)' => '24095', + 'Chalais (86200)' => '86049', + 'Chalandray (86190)' => '86050', + 'Challignac (16300)' => '16074', + 'Châlus (87230)' => '87032', + 'Chamadelle (33230)' => '33124', + 'Chamberaud (23480)' => '23043', + 'Chamberet (19370)' => '19036', + 'Chambon (17290)' => '17080', + 'Chambon-Sainte-Croix (23220)' => '23044', + 'Chambon-sur-Voueize (23170)' => '23045', + 'Chambonchard (23110)' => '23046', + 'Chamborand (23240)' => '23047', + 'Chamboret (87140)' => '87033', + 'Chamboulive (19450)' => '19037', + 'Chameyrat (19330)' => '19038', + 'Chamouillac (17130)' => '17081', + 'Champagnac (17500)' => '17082', + 'Champagnac-de-Belair (24530)' => '24096', + 'Champagnac-la-Noaille (19320)' => '19039', + 'Champagnac-la-Prune (19320)' => '19040', + 'Champagnac-la-Rivière (87150)' => '87034', + 'Champagnat (23190)' => '23048', + 'Champagne (17620)' => '17083', + 'Champagne-et-Fontaine (24320)' => '24097', + 'Champagné-le-Sec (86510)' => '86051', + 'Champagne-Mouton (16350)' => '16076', + 'Champagné-Saint-Hilaire (86160)' => '86052', + 'Champagne-Vigny (16250)' => '16075', + 'Champagnolles (17240)' => '17084', + 'Champcevinel (24750)' => '24098', + 'Champdeniers-Saint-Denis (79220)' => '79066', + 'Champdolent (17430)' => '17085', + 'Champeaux-et-la-Chapelle-Pommier (24340)' => '24099', + 'Champigny-le-Sec (86170)' => '86053', + 'Champmillon (16290)' => '16077', + 'Champnétery (87400)' => '87035', + 'Champniers (16430)' => '16078', + 'Champniers (86400)' => '86054', + 'Champniers-et-Reilhac (24360)' => '24100', + 'Champs-Romain (24470)' => '24101', + 'Champsac (87230)' => '87036', + 'Champsanglard (23220)' => '23049', + 'Chanac-les-Mines (19150)' => '19041', + 'Chancelade (24650)' => '24102', + 'Chaniers (17610)' => '17086', + 'Chantecorps (79340)' => '79068', + 'Chanteix (19330)' => '19042', + 'Chanteloup (79320)' => '79069', + 'Chantemerle-sur-la-Soie (17380)' => '17087', + 'Chantérac (24190)' => '24104', + 'Chantillac (16360)' => '16079', + 'Chapdeuil (24320)' => '24105', + 'Chapelle-Spinasse (19300)' => '19046', + 'Chapelle-Viviers (86300)' => '86059', + 'Chaptelat (87270)' => '87038', + 'Chard (23700)' => '23053', + 'Charmé (16140)' => '16083', + 'Charrais (86170)' => '86060', + 'Charras (16380)' => '16084', + 'Charre (64190)' => '64186', + 'Charritte-de-Bas (64130)' => '64187', + 'Charron (17230)' => '17091', + 'Charron (23700)' => '23054', + 'Charroux (86250)' => '86061', + 'Chartrier-Ferrière (19600)' => '19047', + 'Chartuzac (17130)' => '17092', + 'Chassaignes (24600)' => '24114', + 'Chasseneuil-du-Poitou (86360)' => '86062', + 'Chasseneuil-sur-Bonnieure (16260)' => '16085', + 'Chassenon (16150)' => '16086', + 'Chassiecq (16350)' => '16087', + 'Chassors (16200)' => '16088', + 'Chasteaux (19600)' => '19049', + 'Chatain (86250)' => '86063', + 'Château-Chervix (87380)' => '87039', + 'Château-Garnier (86350)' => '86064', + 'Château-l\'Évêque (24460)' => '24115', + 'Château-Larcher (86370)' => '86065', + 'Châteaubernard (16100)' => '16089', + 'Châteauneuf-la-Forêt (87130)' => '87040', + 'Châteauneuf-sur-Charente (16120)' => '16090', + 'Châteauponsac (87290)' => '87041', + 'Châtelaillon-Plage (17340)' => '17094', + 'Châtelard (23700)' => '23055', + 'Châtellerault (86100)' => '86066', + 'Châtelus-le-Marcheix (23430)' => '23056', + 'Châtelus-Malvaleix (23270)' => '23057', + 'Chatenet (17210)' => '17095', + 'Châtignac (16480)' => '16091', + 'Châtillon (86700)' => '86067', + 'Châtillon-sur-Thouet (79200)' => '79080', + 'Châtres (24120)' => '24116', + 'Chauffour-sur-Vell (19500)' => '19050', + 'Chaumeil (19390)' => '19051', + 'Chaunac (17130)' => '17096', + 'Chaunay (86510)' => '86068', + 'Chauray (79180)' => '79081', + 'Chauvigny (86300)' => '86070', + 'Chavagnac (24120)' => '24117', + 'Chavanac (19290)' => '19052', + 'Chavanat (23250)' => '23060', + 'Chaveroche (19200)' => '19053', + 'Chazelles (16380)' => '16093', + 'Chef-Boutonne (79110)' => '79083', + 'Cheissoux (87460)' => '87043', + 'Chenac-Saint-Seurin-d\'Uzet (17120)' => '17098', + 'Chenailler-Mascheix (19120)' => '19054', + 'Chenay (79120)' => '79084', + 'Cheneché (86380)' => '86071', + 'Chénérailles (23130)' => '23061', + 'Chenevelles (86450)' => '86072', + 'Chéniers (23220)' => '23062', + 'Chenommet (16460)' => '16094', + 'Chenon (16460)' => '16095', + 'Chepniers (17210)' => '17099', + 'Chérac (17610)' => '17100', + 'Chéraute (64130)' => '64188', + 'Cherbonnières (17470)' => '17101', + 'Chérigné (79170)' => '79085', + 'Chermignac (17460)' => '17102', + 'Chéronnac (87600)' => '87044', + 'Cherval (24320)' => '24119', + 'Cherveix-Cubas (24390)' => '24120', + 'Cherves (86170)' => '86073', + 'Cherves-Châtelars (16310)' => '16096', + 'Cherves-Richemont (16370)' => '16097', + 'Chervettes (17380)' => '17103', + 'Cherveux (79410)' => '79086', + 'Chevanceaux (17210)' => '17104', + 'Chey (79120)' => '79087', + 'Chiché (79350)' => '79088', + 'Chillac (16480)' => '16099', + 'Chirac (16150)' => '16100', + 'Chirac-Bellevue (19160)' => '19055', + 'Chiré-en-Montreuil (86190)' => '86074', + 'Chives (17510)' => '17105', + 'Chizé (79170)' => '79090', + 'Chouppes (86110)' => '86075', + 'Chourgnac (24640)' => '24121', + 'Ciboure (64500)' => '64189', + 'Cierzac (17520)' => '17106', + 'Cieux (87520)' => '87045', + 'Ciré-d\'Aunis (17290)' => '17107', + 'Cirières (79140)' => '79091', + 'Cissac-Médoc (33250)' => '33125', + 'Cissé (86170)' => '86076', + 'Civaux (86320)' => '86077', + 'Civrac-de-Blaye (33920)' => '33126', + 'Civrac-en-Médoc (33340)' => '33128', + 'Civrac-sur-Dordogne (33350)' => '33127', + 'Civray (86400)' => '86078', + 'Cladech (24170)' => '24122', + 'Clairac (47320)' => '47065', + 'Clairavaux (23500)' => '23063', + 'Claix (16440)' => '16101', + 'Clam (17500)' => '17108', + 'Claracq (64330)' => '64190', + 'Classun (40320)' => '40082', + 'Clavé (79420)' => '79092', + 'Clavette (17220)' => '17109', + 'Clèdes (40320)' => '40083', + 'Clérac (17270)' => '17110', + 'Clergoux (19320)' => '19056', + 'Clermont (40180)' => '40084', + 'Clermont-d\'Excideuil (24160)' => '24124', + 'Clermont-de-Beauregard (24140)' => '24123', + 'Clermont-Dessous (47130)' => '47066', + 'Clermont-Soubiran (47270)' => '47067', + 'Clessé (79350)' => '79094', + 'Cleyrac (33540)' => '33129', + 'Clion (17240)' => '17111', + 'Cloué (86600)' => '86080', + 'Clugnat (23270)' => '23064', + 'Clussais-la-Pommeraie (79190)' => '79095', + 'Coarraze (64800)' => '64191', + 'Cocumont (47250)' => '47068', + 'Cognac (16100)' => '16102', + 'Cognac-la-Forêt (87310)' => '87046', + 'Coimères (33210)' => '33130', + 'Coirac (33540)' => '33131', + 'Coivert (17330)' => '17114', + 'Colayrac-Saint-Cirq (47450)' => '47069', + 'Collonges-la-Rouge (19500)' => '19057', + 'Colombier (24560)' => '24126', + 'Colombiers (17460)' => '17115', + 'Colombiers (86490)' => '86081', + 'Colondannes (23800)' => '23065', + 'Coly (24120)' => '24127', + 'Comberanche-et-Épeluche (24600)' => '24128', + 'Combiers (16320)' => '16103', + 'Combrand (79140)' => '79096', + 'Combressol (19250)' => '19058', + 'Commensacq (40210)' => '40085', + 'Compreignac (87140)' => '87047', + 'Comps (33710)' => '33132', + 'Concèze (19350)' => '19059', + 'Conchez-de-Béarn (64330)' => '64192', + 'Condac (16700)' => '16104', + 'Condat-sur-Ganaveix (19140)' => '19060', + 'Condat-sur-Trincou (24530)' => '24129', + 'Condat-sur-Vézère (24570)' => '24130', + 'Condat-sur-Vienne (87920)' => '87048', + 'Condéon (16360)' => '16105', + 'Condezaygues (47500)' => '47070', + 'Confolens (16500)' => '16106', + 'Confolent-Port-Dieu (19200)' => '19167', + 'Conne-de-Labarde (24560)' => '24132', + 'Connezac (24300)' => '24131', + 'Consac (17150)' => '17116', + 'Contré (17470)' => '17117', + 'Corbère-Abères (64350)' => '64193', + 'Corgnac-sur-l\'Isle (24800)' => '24134', + 'Corignac (17130)' => '17118', + 'Corme-Écluse (17600)' => '17119', + 'Corme-Royal (17600)' => '17120', + 'Cornil (19150)' => '19061', + 'Cornille (24750)' => '24135', + 'Corrèze (19800)' => '19062', + 'Coslédaà-Lube-Boast (64160)' => '64194', + 'Cosnac (19360)' => '19063', + 'Coubeyrac (33890)' => '33133', + 'Coubjours (24390)' => '24136', + 'Coublucq (64410)' => '64195', + 'Coudures (40500)' => '40086', + 'Couffy-sur-Sarsonne (19340)' => '19064', + 'Couhé (86700)' => '86082', + 'Coulaures (24420)' => '24137', + 'Coulgens (16560)' => '16107', + 'Coulombiers (86600)' => '86083', + 'Coulon (79510)' => '79100', + 'Coulonges (16330)' => '16108', + 'Coulonges (17800)' => '17122', + 'Coulonges (86290)' => '86084', + 'Coulonges-sur-l\'Autize (79160)' => '79101', + 'Coulonges-Thouarsais (79330)' => '79102', + 'Coulounieix-Chamiers (24660)' => '24138', + 'Coulx (47260)' => '47071', + 'Couquèques (33340)' => '33134', + 'Courant (17330)' => '17124', + 'Courbiac (47370)' => '47072', + 'Courbillac (16200)' => '16109', + 'Courcelles (17400)' => '17125', + 'Courcerac (17160)' => '17126', + 'Courcôme (16240)' => '16110', + 'Courçon (17170)' => '17127', + 'Courcoury (17100)' => '17128', + 'Courgeac (16190)' => '16111', + 'Courlac (16210)' => '16112', + 'Courlay (79440)' => '79103', + 'Courpiac (33760)' => '33135', + 'Courpignac (17130)' => '17129', + 'Cours (47360)' => '47073', + 'Cours (79220)' => '79104', + 'Cours-de-Monségur (33580)' => '33136', + 'Cours-de-Pile (24520)' => '24140', + 'Cours-les-Bains (33690)' => '33137', + 'Coursac (24430)' => '24139', + 'Courteix (19340)' => '19065', + 'Coussac-Bonneval (87500)' => '87049', + 'Coussay (86110)' => '86085', + 'Coussay-les-Bois (86270)' => '86086', + 'Couthures-sur-Garonne (47180)' => '47074', + 'Coutières (79340)' => '79105', + 'Coutras (33230)' => '33138', + 'Couture (16460)' => '16114', + 'Couture-d\'Argenson (79110)' => '79106', + 'Coutures (24320)' => '24141', + 'Coutures (33580)' => '33139', + 'Coux (17130)' => '17130', + 'Coux et Bigaroque-Mouzens (24220)' => '24142', + 'Couze-et-Saint-Front (24150)' => '24143', + 'Couzeix (87270)' => '87050', + 'Cozes (17120)' => '17131', + 'Cramchaban (17170)' => '17132', + 'Craon (86110)' => '86087', + 'Cravans (17260)' => '17133', + 'Crazannes (17350)' => '17134', + 'Créon (33670)' => '33140', + 'Créon-d\'Armagnac (40240)' => '40087', + 'Cressac-Saint-Genis (16250)' => '16115', + 'Cressat (23140)' => '23068', + 'Cressé (17160)' => '17135', + 'Creyssac (24350)' => '24144', + 'Creysse (24100)' => '24145', + 'Creyssensac-et-Pissot (24380)' => '24146', + 'Crézières (79110)' => '79107', + 'Criteuil-la-Magdeleine (16300)' => '16116', + 'Crocq (23260)' => '23069', + 'Croignon (33750)' => '33141', + 'Croix-Chapeau (17220)' => '17136', + 'Cromac (87160)' => '87053', + 'Crouseilles (64350)' => '64196', + 'Croutelle (86240)' => '86088', + 'Crozant (23160)' => '23070', + 'Croze (23500)' => '23071', + 'Cubjac (24640)' => '24147', + 'Cublac (19520)' => '19066', + 'Cubnezais (33620)' => '33142', + 'Cubzac-les-Ponts (33240)' => '33143', + 'Cudos (33430)' => '33144', + 'Cuhon (86110)' => '86089', + 'Cunèges (24240)' => '24148', + 'Cuq (47220)' => '47076', + 'Cuqueron (64360)' => '64197', + 'Curac (16210)' => '16117', + 'Curçay-sur-Dive (86120)' => '86090', + 'Curemonte (19500)' => '19067', + 'Cursan (33670)' => '33145', + 'Curzay-sur-Vonne (86600)' => '86091', + 'Cussac (87150)' => '87054', + 'Cussac-Fort-Médoc (33460)' => '33146', + 'Cuzorn (47500)' => '47077', + 'Daglan (24250)' => '24150', + 'Daignac (33420)' => '33147', + 'Damazan (47160)' => '47078', + 'Dampierre-sur-Boutonne (17470)' => '17138', + 'Dampniat (19360)' => '19068', + 'Dangé-Saint-Romain (86220)' => '86092', + 'Darazac (19220)' => '19069', + 'Dardenac (33420)' => '33148', + 'Darnac (87320)' => '87055', + 'Darnets (19300)' => '19070', + 'Daubèze (33540)' => '33149', + 'Dausse (47140)' => '47079', + 'Davignac (19250)' => '19071', + 'Dax (40100)' => '40088', + 'Denguin (64230)' => '64198', + 'Dercé (86420)' => '86093', + 'Deviat (16190)' => '16118', + 'Dévillac (47210)' => '47080', + 'Dienné (86410)' => '86094', + 'Dieulivol (33580)' => '33150', + 'Dignac (16410)' => '16119', + 'Dinsac (87210)' => '87056', + 'Dirac (16410)' => '16120', + 'Dissay (86130)' => '86095', + 'Diusse (64330)' => '64199', + 'Doazit (40700)' => '40089', + 'Doazon (64370)' => '64200', + 'Doeuil-sur-le-Mignon (17330)' => '17139', + 'Dognen (64190)' => '64201', + 'Doissat (24170)' => '24151', + 'Dolmayrac (47110)' => '47081', + 'Dolus-d\'Oléron (17550)' => '17140', + 'Domeyrot (23140)' => '23072', + 'Domezain-Berraute (64120)' => '64202', + 'Domme (24250)' => '24152', + 'Dompierre-les-Églises (87190)' => '87057', + 'Dompierre-sur-Charente (17610)' => '17141', + 'Dompierre-sur-Mer (17139)' => '17142', + 'Domps (87120)' => '87058', + 'Dondas (47470)' => '47082', + 'Donnezac (33860)' => '33151', + 'Dontreix (23700)' => '23073', + 'Donzac (33410)' => '33152', + 'Donzacq (40360)' => '40090', + 'Donzenac (19270)' => '19072', + 'Douchapt (24350)' => '24154', + 'Doudrac (47210)' => '47083', + 'Doulezon (33350)' => '33153', + 'Doumy (64450)' => '64203', + 'Dournazac (87230)' => '87060', + 'Doussay (86140)' => '86096', + 'Douville (24140)' => '24155', + 'Doux (79390)' => '79108', + 'Douzains (47330)' => '47084', + 'Douzat (16290)' => '16121', + 'Douzillac (24190)' => '24157', + 'Droux (87190)' => '87061', + 'Duhort-Bachen (40800)' => '40091', + 'Dumes (40500)' => '40092', + 'Dun-le-Palestel (23800)' => '23075', + 'Durance (47420)' => '47085', + 'Duras (47120)' => '47086', + 'Dussac (24270)' => '24158', + 'Eaux-Bonnes (64440)' => '64204', + 'Ébréon (16140)' => '16122', + 'Échallat (16170)' => '16123', + 'Échebrune (17800)' => '17145', + 'Échillais (17620)' => '17146', + 'Échiré (79410)' => '79109', + 'Échourgnac (24410)' => '24159', + 'Écoyeux (17770)' => '17147', + 'Écuras (16220)' => '16124', + 'Écurat (17810)' => '17148', + 'Édon (16320)' => '16125', + 'Égletons (19300)' => '19073', + 'Église-Neuve-d\'Issac (24400)' => '24161', + 'Église-Neuve-de-Vergt (24380)' => '24160', + 'Empuré (16240)' => '16127', + 'Engayrac (47470)' => '47087', + 'Ensigné (79170)' => '79111', + 'Épannes (79270)' => '79112', + 'Épargnes (17120)' => '17152', + 'Épenède (16490)' => '16128', + 'Éraville (16120)' => '16129', + 'Escalans (40310)' => '40093', + 'Escassefort (47350)' => '47088', + 'Escaudes (33840)' => '33155', + 'Escaunets (65500)' => '65160', + 'Esclottes (47120)' => '47089', + 'Escoire (24420)' => '24162', + 'Escos (64270)' => '64205', + 'Escot (64490)' => '64206', + 'Escou (64870)' => '64207', + 'Escoubès (64160)' => '64208', + 'Escource (40210)' => '40094', + 'Escoussans (33760)' => '33156', + 'Escout (64870)' => '64209', + 'Escurès (64350)' => '64210', + 'Eslourenties-Daban (64420)' => '64211', + 'Esnandes (17137)' => '17153', + 'Espagnac (19150)' => '19075', + 'Espartignac (19140)' => '19076', + 'Espéchède (64160)' => '64212', + 'Espelette (64250)' => '64213', + 'Espès-Undurein (64130)' => '64214', + 'Espiens (47600)' => '47090', + 'Espiet (33420)' => '33157', + 'Espiute (64390)' => '64215', + 'Espoey (64420)' => '64216', + 'Esquiule (64400)' => '64217', + 'Esse (16500)' => '16131', + 'Essouvert (17400)' => '17277', + 'Estérençuby (64220)' => '64218', + 'Estialescq (64290)' => '64219', + 'Estibeaux (40290)' => '40095', + 'Estigarde (40240)' => '40096', + 'Estillac (47310)' => '47091', + 'Estivals (19600)' => '19077', + 'Estivaux (19410)' => '19078', + 'Estos (64400)' => '64220', + 'Étagnac (16150)' => '16132', + 'Étaules (17750)' => '17155', + 'Étauliers (33820)' => '33159', + 'Etcharry (64120)' => '64221', + 'Etchebar (64470)' => '64222', + 'Étouars (24360)' => '24163', + 'Étriac (16250)' => '16133', + 'Etsaut (64490)' => '64223', + 'Eugénie-les-Bains (40320)' => '40097', + 'Évaux-les-Bains (23110)' => '23076', + 'Excideuil (24160)' => '24164', + 'Exideuil (16150)' => '16134', + 'Exireuil (79400)' => '79114', + 'Exoudun (79800)' => '79115', + 'Expiremont (17130)' => '17156', + 'Eybouleuf (87400)' => '87062', + 'Eyburie (19140)' => '19079', + 'Eygurande (19340)' => '19080', + 'Eygurande-et-Gardedeuil (24700)' => '24165', + 'Eyjeaux (87220)' => '87063', + 'Eyliac (24330)' => '24166', + 'Eymet (24500)' => '24167', + 'Eymouthiers (16220)' => '16135', + 'Eymoutiers (87120)' => '87064', + 'Eynesse (33220)' => '33160', + 'Eyrans (33390)' => '33161', + 'Eyrein (19800)' => '19081', + 'Eyres-Moncube (40500)' => '40098', + 'Eysines (33320)' => '33162', + 'Eysus (64400)' => '64224', + 'Eyvirat (24460)' => '24170', + 'Eyzerac (24800)' => '24171', + 'Faleyras (33760)' => '33163', + 'Fals (47220)' => '47092', + 'Fanlac (24290)' => '24174', + 'Fargues (33210)' => '33164', + 'Fargues (40500)' => '40099', + 'Fargues-Saint-Hilaire (33370)' => '33165', + 'Fargues-sur-Ourbise (47700)' => '47093', + 'Fauguerolles (47400)' => '47094', + 'Fauillet (47400)' => '47095', + 'Faurilles (24560)' => '24176', + 'Faux (24560)' => '24177', + 'Faux-la-Montagne (23340)' => '23077', + 'Faux-Mazuras (23400)' => '23078', + 'Favars (19330)' => '19082', + 'Faye-l\'Abbesse (79350)' => '79116', + 'Faye-sur-Ardin (79160)' => '79117', + 'Féas (64570)' => '64225', + 'Felletin (23500)' => '23079', + 'Fénery (79450)' => '79118', + 'Féniers (23100)' => '23080', + 'Fenioux (17350)' => '17157', + 'Fenioux (79160)' => '79119', + 'Ferrensac (47330)' => '47096', + 'Ferrières (17170)' => '17158', + 'Festalemps (24410)' => '24178', + 'Feugarolles (47230)' => '47097', + 'Feuillade (16380)' => '16137', + 'Feyt (19340)' => '19083', + 'Feytiat (87220)' => '87065', + 'Fichous-Riumayou (64410)' => '64226', + 'Fieux (47600)' => '47098', + 'Firbeix (24450)' => '24180', + 'Flaugeac (24240)' => '24181', + 'Flaujagues (33350)' => '33168', + 'Flavignac (87230)' => '87066', + 'Flayat (23260)' => '23081', + 'Fléac (16730)' => '16138', + 'Fléac-sur-Seugne (17800)' => '17159', + 'Fleix (86300)' => '86098', + 'Fleurac (16200)' => '16139', + 'Fleurac (24580)' => '24183', + 'Fleurat (23320)' => '23082', + 'Fleuré (86340)' => '86099', + 'Floirac (17120)' => '17160', + 'Floirac (33270)' => '33167', + 'Florimont-Gaumier (24250)' => '24184', + 'Floudès (33190)' => '33169', + 'Folles (87250)' => '87067', + 'Fomperron (79340)' => '79121', + 'Fongrave (47260)' => '47099', + 'Fonroque (24500)' => '24186', + 'Fontaine-Chalendray (17510)' => '17162', + 'Fontaine-le-Comte (86240)' => '86100', + 'Fontaines-d\'Ozillac (17500)' => '17163', + 'Fontanières (23110)' => '23083', + 'Fontclaireau (16230)' => '16140', + 'Fontcouverte (17100)' => '17164', + 'Fontenet (17400)' => '17165', + 'Fontenille (16230)' => '16141', + 'Fontenille-Saint-Martin-d\'Entraigues (79110)' => '79122', + 'Fontet (33190)' => '33170', + 'Forges (17290)' => '17166', + 'Forgès (19380)' => '19084', + 'Fors (79230)' => '79125', + 'Fossemagne (24210)' => '24188', + 'Fossès-et-Baleyssac (33190)' => '33171', + 'Fougueyrolles (33220)' => '24189', + 'Foulayronnes (47510)' => '47100', + 'Fouleix (24380)' => '24190', + 'Fouquebrune (16410)' => '16143', + 'Fouqueure (16140)' => '16144', + 'Fouras (17450)' => '17168', + 'Fourques-sur-Garonne (47200)' => '47101', + 'Fours (33390)' => '33172', + 'Foussignac (16200)' => '16145', + 'Fraisse (24130)' => '24191', + 'Francescas (47600)' => '47102', + 'François (79260)' => '79128', + 'Francs (33570)' => '33173', + 'Fransèches (23480)' => '23086', + 'Fréchou (47600)' => '47103', + 'Frégimont (47360)' => '47104', + 'Frespech (47140)' => '47105', + 'Fresselines (23450)' => '23087', + 'Fressines (79370)' => '79129', + 'Fromental (87250)' => '87068', + 'Fronsac (33126)' => '33174', + 'Frontenac (33760)' => '33175', + 'Frontenay-Rohan-Rohan (79270)' => '79130', + 'Frozes (86190)' => '86102', + 'Fumel (47500)' => '47106', + 'Gaas (40350)' => '40101', + 'Gabarnac (33410)' => '33176', + 'Gabarret (40310)' => '40102', + 'Gabaston (64160)' => '64227', + 'Gabat (64120)' => '64228', + 'Gabillou (24210)' => '24192', + 'Gageac-et-Rouillac (24240)' => '24193', + 'Gaillan-en-Médoc (33340)' => '33177', + 'Gaillères (40090)' => '40103', + 'Gajac (33430)' => '33178', + 'Gajoubert (87330)' => '87069', + 'Galapian (47190)' => '47107', + 'Galgon (33133)' => '33179', + 'Gamarde-les-Bains (40380)' => '40104', + 'Gamarthe (64220)' => '64229', + 'Gan (64290)' => '64230', + 'Gans (33430)' => '33180', + 'Garat (16410)' => '16146', + 'Gardegan-et-Tourtirac (33350)' => '33181', + 'Gardères (65320)' => '65185', + 'Gardes-le-Pontaroux (16320)' => '16147', + 'Gardonne (24680)' => '24194', + 'Garein (40420)' => '40105', + 'Garindein (64130)' => '64231', + 'Garlède-Mondebat (64450)' => '64232', + 'Garlin (64330)' => '64233', + 'Garos (64410)' => '64234', + 'Garrey (40180)' => '40106', + 'Garris (64120)' => '64235', + 'Garrosse (40110)' => '40107', + 'Gartempe (23320)' => '23088', + 'Gastes (40160)' => '40108', + 'Gaugeac (24540)' => '24195', + 'Gaujac (47200)' => '47108', + 'Gaujacq (40330)' => '40109', + 'Gauriac (33710)' => '33182', + 'Gauriaguet (33240)' => '33183', + 'Gavaudun (47150)' => '47109', + 'Gayon (64350)' => '64236', + 'Geaune (40320)' => '40110', + 'Geay (17250)' => '17171', + 'Geay (79330)' => '79131', + 'Gelos (64110)' => '64237', + 'Geloux (40090)' => '40111', + 'Gémozac (17260)' => '17172', + 'Genac-Bignac (16170)' => '16148', + 'Gençay (86160)' => '86103', + 'Générac (33920)' => '33184', + 'Génis (24160)' => '24196', + 'Génissac (33420)' => '33185', + 'Genneton (79150)' => '79132', + 'Genouillac (16270)' => '16149', + 'Genouillac (23350)' => '23089', + 'Genouillé (17430)' => '17174', + 'Genouillé (86250)' => '86104', + 'Gensac (33890)' => '33186', + 'Gensac-la-Pallue (16130)' => '16150', + 'Genté (16130)' => '16151', + 'Gentioux-Pigerolles (23340)' => '23090', + 'Ger (64530)' => '64238', + 'Gerderest (64160)' => '64239', + 'Gère-Bélesten (64260)' => '64240', + 'Germignac (17520)' => '17175', + 'Germond-Rouvre (79220)' => '79133', + 'Géronce (64400)' => '64241', + 'Gestas (64190)' => '64242', + 'Géus-d\'Arzacq (64370)' => '64243', + 'Geüs-d\'Oloron (64400)' => '64244', + 'Gibourne (17160)' => '17176', + 'Gibret (40380)' => '40112', + 'Gimel-les-Cascades (19800)' => '19085', + 'Gimeux (16130)' => '16152', + 'Ginestet (24130)' => '24197', + 'Gioux (23500)' => '23091', + 'Gironde-sur-Dropt (33190)' => '33187', + 'Giscos (33840)' => '33188', + 'Givrezac (17260)' => '17178', + 'Gizay (86340)' => '86105', + 'Glandon (87500)' => '87071', + 'Glanges (87380)' => '87072', + 'Glénay (79330)' => '79134', + 'Glénic (23380)' => '23092', + 'Glénouze (86200)' => '86106', + 'Goès (64400)' => '64245', + 'Gomer (64420)' => '64246', + 'Gond-Pontouvre (16160)' => '16154', + 'Gondeville (16200)' => '16153', + 'Gontaud-de-Nogaret (47400)' => '47110', + 'Goos (40180)' => '40113', + 'Gornac (33540)' => '33189', + 'Gorre (87310)' => '87073', + 'Gotein-Libarrenx (64130)' => '64247', + 'Goualade (33840)' => '33190', + 'Gouex (86320)' => '86107', + 'Goulles (19430)' => '19086', + 'Gourbera (40990)' => '40114', + 'Gourdon-Murat (19170)' => '19087', + 'Gourgé (79200)' => '79135', + 'Gournay-Loizé (79110)' => '79136', + 'Gours (33660)' => '33191', + 'Gourville (16170)' => '16156', + 'Gourvillette (17490)' => '17180', + 'Gousse (40465)' => '40115', + 'Gout-Rossignol (24320)' => '24199', + 'Gouts (40400)' => '40116', + 'Gouzon (23230)' => '23093', + 'Gradignan (33170)' => '33192', + 'Grand-Brassac (24350)' => '24200', + 'Grandjean (17350)' => '17181', + 'Grandsaigne (19300)' => '19088', + 'Granges-d\'Ans (24390)' => '24202', + 'Granges-sur-Lot (47260)' => '47111', + 'Granzay-Gript (79360)' => '79137', + 'Grassac (16380)' => '16158', + 'Grateloup-Saint-Gayrand (47400)' => '47112', + 'Graves-Saint-Amant (16120)' => '16297', + 'Grayan-et-l\'Hôpital (33590)' => '33193', + 'Grayssas (47270)' => '47113', + 'Grenade-sur-l\'Adour (40270)' => '40117', + 'Grézac (17120)' => '17183', + 'Grèzes (24120)' => '24204', + 'Grézet-Cavagnan (47250)' => '47114', + 'Grézillac (33420)' => '33194', + 'Grignols (24110)' => '24205', + 'Grignols (33690)' => '33195', + 'Grives (24170)' => '24206', + 'Groléjac (24250)' => '24207', + 'Gros-Chastang (19320)' => '19089', + 'Grun-Bordas (24380)' => '24208', + 'Guéret (23000)' => '23096', + 'Guérin (47250)' => '47115', + 'Guesnes (86420)' => '86109', + 'Guéthary (64210)' => '64249', + 'Guiche (64520)' => '64250', + 'Guillac (33420)' => '33196', + 'Guillos (33720)' => '33197', + 'Guimps (16300)' => '16160', + 'Guinarthe-Parenties (64390)' => '64251', + 'Guitinières (17500)' => '17187', + 'Guîtres (33230)' => '33198', + 'Guizengeard (16480)' => '16161', + 'Gujan-Mestras (33470)' => '33199', + 'Gumond (19320)' => '19090', + 'Gurat (16320)' => '16162', + 'Gurmençon (64400)' => '64252', + 'Gurs (64190)' => '64253', + 'Habas (40290)' => '40118', + 'Hagetaubin (64370)' => '64254', + 'Hagetmau (40700)' => '40119', + 'Haimps (17160)' => '17188', + 'Haims (86310)' => '86110', + 'Halsou (64480)' => '64255', + 'Hanc (79110)' => '79140', + 'Hasparren (64240)' => '64256', + 'Hastingues (40300)' => '40120', + 'Hauriet (40250)' => '40121', + 'Haut-de-Bosdarros (64800)' => '64257', + 'Haut-Mauco (40280)' => '40122', + 'Hautefage (19400)' => '19091', + 'Hautefage-la-Tour (47340)' => '47117', + 'Hautefaye (24300)' => '24209', + 'Hautefort (24390)' => '24210', + 'Hautesvignes (47400)' => '47118', + 'Haux (33550)' => '33201', + 'Haux (64470)' => '64258', + 'Hélette (64640)' => '64259', + 'Hendaye (64700)' => '64260', + 'Herm (40990)' => '40123', + 'Herré (40310)' => '40124', + 'Herrère (64680)' => '64261', + 'Heugas (40180)' => '40125', + 'Hiers-Brouage (17320)' => '17189', + 'Hiersac (16290)' => '16163', + 'Hiesse (16490)' => '16164', + 'Higuères-Souye (64160)' => '64262', + 'Hinx (40180)' => '40126', + 'Hontanx (40190)' => '40127', + 'Horsarrieu (40700)' => '40128', + 'Hosta (64120)' => '64265', + 'Hostens (33125)' => '33202', + 'Houeillès (47420)' => '47119', + 'Houlette (16200)' => '16165', + 'Hours (64420)' => '64266', + 'Hourtin (33990)' => '33203', + 'Hure (33190)' => '33204', + 'Ibarrolle (64120)' => '64267', + 'Idaux-Mendy (64130)' => '64268', + 'Idron (64320)' => '64269', + 'Igon (64800)' => '64270', + 'Iholdy (64640)' => '64271', + 'Île-d\'Aix (17123)' => '17004', + 'Ilharre (64120)' => '64272', + 'Illats (33720)' => '33205', + 'Ingrandes (86220)' => '86111', + 'Irais (79600)' => '79141', + 'Irissarry (64780)' => '64273', + 'Irouléguy (64220)' => '64274', + 'Isle (87170)' => '87075', + 'Isle-Saint-Georges (33640)' => '33206', + 'Ispoure (64220)' => '64275', + 'Issac (24400)' => '24211', + 'Issigeac (24560)' => '24212', + 'Issor (64570)' => '64276', + 'Issoudun-Létrieix (23130)' => '23097', + 'Isturits (64240)' => '64277', + 'Iteuil (86240)' => '86113', + 'Itxassou (64250)' => '64279', + 'Izeste (64260)' => '64280', + 'Izon (33450)' => '33207', + 'Jabreilles-les-Bordes (87370)' => '87076', + 'Jalesches (23270)' => '23098', + 'Janailhac (87800)' => '87077', + 'Janaillat (23250)' => '23099', + 'Jardres (86800)' => '86114', + 'Jarnac (16200)' => '16167', + 'Jarnac-Champagne (17520)' => '17192', + 'Jarnages (23140)' => '23100', + 'Jasses (64190)' => '64281', + 'Jatxou (64480)' => '64282', + 'Jau-Dignac-et-Loirac (33590)' => '33208', + 'Jauldes (16560)' => '16168', + 'Jaunay-Clan (86130)' => '86115', + 'Jaure (24140)' => '24213', + 'Javerdat (87520)' => '87078', + 'Javerlhac-et-la-Chapelle-Saint-Robert (24300)' => '24214', + 'Javrezac (16100)' => '16169', + 'Jaxu (64220)' => '64283', + 'Jayac (24590)' => '24215', + 'Jazeneuil (86600)' => '86116', + 'Jazennes (17260)' => '17196', + 'Jonzac (17500)' => '17197', + 'Josse (40230)' => '40129', + 'Jouac (87890)' => '87080', + 'Jouhet (86500)' => '86117', + 'Jouillat (23220)' => '23101', + 'Jourgnac (87800)' => '87081', + 'Journet (86290)' => '86118', + 'Journiac (24260)' => '24217', + 'Joussé (86350)' => '86119', + 'Jugazan (33420)' => '33209', + 'Jugeals-Nazareth (19500)' => '19093', + 'Juicq (17770)' => '17198', + 'Juignac (16190)' => '16170', + 'Juillac (19350)' => '19094', + 'Juillac (33890)' => '33210', + 'Juillac-le-Coq (16130)' => '16171', + 'Juillé (16230)' => '16173', + 'Juillé (79170)' => '79142', + 'Julienne (16200)' => '16174', + 'Jumilhac-le-Grand (24630)' => '24218', + 'Jurançon (64110)' => '64284', + 'Juscorps (79230)' => '79144', + 'Jusix (47180)' => '47120', + 'Jussas (17130)' => '17199', + 'Juxue (64120)' => '64285', + 'L\'Absie (79240)' => '79001', + 'L\'Église-aux-Bois (19170)' => '19074', + 'L\'Éguille (17600)' => '17151', + 'L\'Hôpital-d\'Orion (64270)' => '64263', + 'L\'Hôpital-Saint-Blaise (64130)' => '64264', + 'L\'Houmeau (17137)' => '17190', + 'L\'Isle-d\'Espagnac (16340)' => '16166', + 'L\'Isle-Jourdain (86150)' => '86112', + 'La Bachellerie (24210)' => '24020', + 'La Barde (17360)' => '17033', + 'La Bastide-Clairence (64240)' => '64289', + 'La Bataille (79110)' => '79027', + 'La Bazeuge (87210)' => '87008', + 'La Boissière-d\'Ans (24640)' => '24047', + 'La Boissière-en-Gâtine (79310)' => '79040', + 'La Brède (33650)' => '33213', + 'La Brée-les-Bains (17840)' => '17486', + 'La Brionne (23000)' => '23033', + 'La Brousse (17160)' => '17071', + 'La Bussière (86310)' => '86040', + 'La Cassagne (24120)' => '24085', + 'La Celle-Dunoise (23800)' => '23039', + 'La Celle-sous-Gouzon (23230)' => '23040', + 'La Cellette (23350)' => '23041', + 'La Chapelle (16140)' => '16081', + 'La Chapelle-Aubareil (24290)' => '24106', + 'La Chapelle-aux-Brocs (19360)' => '19043', + 'La Chapelle-aux-Saints (19120)' => '19044', + 'La Chapelle-Baloue (23160)' => '23050', + 'La Chapelle-Bâton (79220)' => '79070', + 'La Chapelle-Bâton (86250)' => '86055', + 'La Chapelle-Bertrand (79200)' => '79071', + 'La Chapelle-des-Pots (17100)' => '17089', + 'La Chapelle-Faucher (24530)' => '24107', + 'La Chapelle-Gonaguet (24350)' => '24108', + 'La Chapelle-Grésignac (24320)' => '24109', + 'La Chapelle-Montabourlet (24320)' => '24110', + 'La Chapelle-Montbrandeix (87440)' => '87037', + 'La Chapelle-Montmoreau (24300)' => '24111', + 'La Chapelle-Montreuil (86470)' => '86056', + 'La Chapelle-Moulière (86210)' => '86058', + 'La Chapelle-Pouilloux (79190)' => '79074', + 'La Chapelle-Saint-Étienne (79240)' => '79075', + 'La Chapelle-Saint-Géraud (19430)' => '19045', + 'La Chapelle-Saint-Jean (24390)' => '24113', + 'La Chapelle-Saint-Laurent (79430)' => '79076', + 'La Chapelle-Saint-Martial (23250)' => '23051', + 'La Chapelle-Taillefert (23000)' => '23052', + 'La Chapelle-Thireuil (79160)' => '79077', + 'La Chaussade (23200)' => '23059', + 'La Chaussée (86330)' => '86069', + 'La Chèvrerie (16240)' => '16098', + 'La Clisse (17600)' => '17112', + 'La Clotte (17360)' => '17113', + 'La Coquille (24450)' => '24133', + 'La Couarde (79800)' => '79098', + 'La Couarde-sur-Mer (17670)' => '17121', + 'La Couronne (16400)' => '16113', + 'La Courtine (23100)' => '23067', + 'La Crèche (79260)' => '79048', + 'La Croisille-sur-Briance (87130)' => '87051', + 'La Croix-Blanche (47340)' => '47075', + 'La Croix-Comtesse (17330)' => '17137', + 'La Croix-sur-Gartempe (87210)' => '87052', + 'La Dornac (24120)' => '24153', + 'La Douze (24330)' => '24156', + 'La Faye (16700)' => '16136', + 'La Ferrière-Airoux (86160)' => '86097', + 'La Ferrière-en-Parthenay (79390)' => '79120', + 'La Feuillade (24120)' => '24179', + 'La Flotte (17630)' => '17161', + 'La Force (24130)' => '24222', + 'La Forêt-de-Tessé (16240)' => '16142', + 'La Forêt-du-Temple (23360)' => '23084', + 'La Forêt-sur-Sèvre (79380)' => '79123', + 'La Foye-Monjault (79360)' => '79127', + 'La Frédière (17770)' => '17169', + 'La Genétouze (17360)' => '17173', + 'La Geneytouse (87400)' => '87070', + 'La Gonterie-Boulouneix (24310)' => '24198', + 'La Grève-sur-Mignon (17170)' => '17182', + 'La Grimaudière (86330)' => '86108', + 'La Gripperie-Saint-Symphorien (17620)' => '17184', + 'La Jard (17460)' => '17191', + 'La Jarne (17220)' => '17193', + 'La Jarrie (17220)' => '17194', + 'La Jarrie-Audouin (17330)' => '17195', + 'La Jemaye (24410)' => '24216', + 'La Jonchère-Saint-Maurice (87340)' => '87079', + 'La Laigne (17170)' => '17201', + 'La Lande-de-Fronsac (33240)' => '33219', + 'La Magdeleine (16240)' => '16197', + 'La Mazière-aux-Bons-Hommes (23260)' => '23129', + 'La Meyze (87800)' => '87096', + 'La Mothe-Saint-Héray (79800)' => '79184', + 'La Nouaille (23500)' => '23144', + 'La Péruse (16270)' => '16259', + 'La Petite-Boissière (79700)' => '79207', + 'La Peyratte (79200)' => '79208', + 'La Porcherie (87380)' => '87120', + 'La Pouge (23250)' => '23157', + 'La Puye (86260)' => '86202', + 'La Réole (33190)' => '33352', + 'La Réunion (47700)' => '47222', + 'La Rivière (33126)' => '33356', + 'La Roche-Canillac (19320)' => '19174', + 'La Roche-Chalais (24490)' => '24354', + 'La Roche-l\'Abeille (87800)' => '87127', + 'La Roche-Posay (86270)' => '86207', + 'La Roche-Rigault (86200)' => '86079', + 'La Rochebeaucourt-et-Argentine (24340)' => '24353', + 'La Rochefoucauld (16110)' => '16281', + 'La Rochelle (17000)' => '17300', + 'La Rochénard (79270)' => '79229', + 'La Rochette (16110)' => '16282', + 'La Ronde (17170)' => '17303', + 'La Roque-Gageac (24250)' => '24355', + 'La Roquille (33220)' => '33360', + 'La Saunière (23000)' => '23169', + 'La Sauve (33670)' => '33505', + 'La Sauvetat-de-Savères (47270)' => '47289', + 'La Sauvetat-du-Dropt (47800)' => '47290', + 'La Sauvetat-sur-Lède (47150)' => '47291', + 'La Serre-Bussière-Vieille (23190)' => '23172', + 'La Souterraine (23300)' => '23176', + 'La Tâche (16260)' => '16377', + 'La Teste-de-Buch (33260)' => '33529', + 'La Tour-Blanche (24320)' => '24554', + 'La Tremblade (17390)' => '17452', + 'La Trimouille (86290)' => '86273', + 'La Vallée (17250)' => '17455', + 'La Vergne (17400)' => '17465', + 'La Villedieu (17470)' => '17471', + 'La Villedieu (23340)' => '23264', + 'La Villedieu-du-Clain (86340)' => '86290', + 'La Villeneuve (23260)' => '23265', + 'La Villetelle (23260)' => '23266', + 'Laà-Mondrans (64300)' => '64286', + 'Laàs (64390)' => '64287', + 'Labarde (33460)' => '33211', + 'Labastide-Castel-Amouroux (47250)' => '47121', + 'Labastide-Cézéracq (64170)' => '64288', + 'Labastide-Chalosse (40700)' => '40130', + 'Labastide-d\'Armagnac (40240)' => '40131', + 'Labastide-Monréjeau (64170)' => '64290', + 'Labastide-Villefranche (64270)' => '64291', + 'Labatmale (64530)' => '64292', + 'Labatut (40300)' => '40132', + 'Labatut (64460)' => '64293', + 'Labenne (40530)' => '40133', + 'Labescau (33690)' => '33212', + 'Labets-Biscay (64120)' => '64294', + 'Labeyrie (64300)' => '64295', + 'Labouheyre (40210)' => '40134', + 'Labretonie (47350)' => '47122', + 'Labrit (40420)' => '40135', + 'Lacadée (64300)' => '64296', + 'Lacajunte (40320)' => '40136', + 'Lacanau (33680)' => '33214', + 'Lacapelle-Biron (47150)' => '47123', + 'Lacarre (64220)' => '64297', + 'Lacarry-Arhan-Charritte-de-Haut (64470)' => '64298', + 'Lacaussade (47150)' => '47124', + 'Lacelle (19170)' => '19095', + 'Lacépède (47360)' => '47125', + 'Lachaise (16300)' => '16176', + 'Lachapelle (47350)' => '47126', + 'Lacommande (64360)' => '64299', + 'Lacq (64170)' => '64300', + 'Lacquy (40120)' => '40137', + 'Lacrabe (40700)' => '40138', + 'Lacropte (24380)' => '24220', + 'Ladapeyre (23270)' => '23102', + 'Ladaux (33760)' => '33215', + 'Ladignac-le-Long (87500)' => '87082', + 'Ladignac-sur-Rondelles (19150)' => '19096', + 'Ladiville (16120)' => '16177', + 'Lados (33124)' => '33216', + 'Lafage-sur-Sombre (19320)' => '19097', + 'Lafat (23800)' => '23103', + 'Lafitte-sur-Lot (47320)' => '47127', + 'Lafox (47240)' => '47128', + 'Lagarde-Enval (19150)' => '19098', + 'Lagarde-sur-le-Né (16300)' => '16178', + 'Lagarrigue (47190)' => '47129', + 'Lageon (79200)' => '79145', + 'Lagleygeolle (19500)' => '19099', + 'Laglorieuse (40090)' => '40139', + 'Lagor (64150)' => '64301', + 'Lagorce (33230)' => '33218', + 'Lagord (17140)' => '17200', + 'Lagos (64800)' => '64302', + 'Lagrange (40240)' => '40140', + 'Lagraulière (19700)' => '19100', + 'Lagruère (47400)' => '47130', + 'Laguenne (19150)' => '19101', + 'Laguinge-Restoue (64470)' => '64303', + 'Lagupie (47180)' => '47131', + 'Lahonce (64990)' => '64304', + 'Lahontan (64270)' => '64305', + 'Lahosse (40250)' => '40141', + 'Lahourcade (64150)' => '64306', + 'Lalande-de-Pomerol (33500)' => '33222', + 'Lalandusse (47330)' => '47132', + 'Lalinde (24150)' => '24223', + 'Lalongue (64350)' => '64307', + 'Lalonquette (64450)' => '64308', + 'Laluque (40465)' => '40142', + 'Lamarque (33460)' => '33220', + 'Lamayou (64460)' => '64309', + 'Lamazière-Basse (19160)' => '19102', + 'Lamazière-Haute (19340)' => '19103', + 'Lamongerie (19510)' => '19104', + 'Lamontjoie (47310)' => '47133', + 'Lamonzie-Montastruc (24520)' => '24224', + 'Lamonzie-Saint-Martin (24680)' => '24225', + 'Lamothe (40250)' => '40143', + 'Lamothe-Landerron (33190)' => '33221', + 'Lamothe-Montravel (24230)' => '24226', + 'Landerrouat (33790)' => '33223', + 'Landerrouet-sur-Ségur (33540)' => '33224', + 'Landes (17380)' => '17202', + 'Landiras (33720)' => '33225', + 'Landrais (17290)' => '17203', + 'Langoiran (33550)' => '33226', + 'Langon (33210)' => '33227', + 'Lanne-en-Barétous (64570)' => '64310', + 'Lannecaube (64350)' => '64311', + 'Lanneplaà (64300)' => '64312', + 'Lannes (47170)' => '47134', + 'Lanouaille (24270)' => '24227', + 'Lanquais (24150)' => '24228', + 'Lansac (33710)' => '33228', + 'Lantabat (64640)' => '64313', + 'Lanteuil (19190)' => '19105', + 'Lanton (33138)' => '33229', + 'Laparade (47260)' => '47135', + 'Laperche (47800)' => '47136', + 'Lapleau (19550)' => '19106', + 'Laplume (47310)' => '47137', + 'Lapouyade (33620)' => '33230', + 'Laprade (16390)' => '16180', + 'Larbey (40250)' => '40144', + 'Larceveau-Arros-Cibits (64120)' => '64314', + 'Larche (19600)' => '19107', + 'Largeasse (79240)' => '79147', + 'Laroche-près-Feyt (19340)' => '19108', + 'Laroin (64110)' => '64315', + 'Laroque (33410)' => '33231', + 'Laroque-Timbaut (47340)' => '47138', + 'Larrau (64560)' => '64316', + 'Larressore (64480)' => '64317', + 'Larreule (64410)' => '64318', + 'Larribar-Sorhapuru (64120)' => '64319', + 'Larrivière-Saint-Savin (40270)' => '40145', + 'Lartigue (33840)' => '33232', + 'Laruns (64440)' => '64320', + 'Laruscade (33620)' => '33233', + 'Larzac (24170)' => '24230', + 'Lascaux (19130)' => '19109', + 'Lasclaveries (64450)' => '64321', + 'Lasse (64220)' => '64322', + 'Lasserre (47600)' => '47139', + 'Lasserre (64350)' => '64323', + 'Lasseube (64290)' => '64324', + 'Lasseubetat (64290)' => '64325', + 'Lathus-Saint-Rémy (86390)' => '86120', + 'Latillé (86190)' => '86121', + 'Latresne (33360)' => '33234', + 'Latrille (40800)' => '40146', + 'Latronche (19160)' => '19110', + 'Laugnac (47360)' => '47140', + 'Laurède (40250)' => '40147', + 'Lauret (40320)' => '40148', + 'Laurière (87370)' => '87083', + 'Laussou (47150)' => '47141', + 'Lauthiers (86300)' => '86122', + 'Lauzun (47410)' => '47142', + 'Laval-sur-Luzège (19550)' => '19111', + 'Lavalade (24540)' => '24231', + 'Lavardac (47230)' => '47143', + 'Lavaufranche (23600)' => '23104', + 'Lavaur (24550)' => '24232', + 'Lavausseau (86470)' => '86123', + 'Lavaveix-les-Mines (23150)' => '23105', + 'Lavazan (33690)' => '33235', + 'Lavergne (47800)' => '47144', + 'Laveyssière (24130)' => '24233', + 'Lavignac (87230)' => '87084', + 'Lavoux (86800)' => '86124', + 'Lay-Lamidou (64190)' => '64326', + 'Layrac (47390)' => '47145', + 'Le Barp (33114)' => '33029', + 'Le Beugnon (79130)' => '79035', + 'Le Bois-Plage-en-Ré (17580)' => '17051', + 'Le Bouchage (16350)' => '16054', + 'Le Bourdeix (24300)' => '24056', + 'Le Bourdet (79210)' => '79046', + 'Le Bourg-d\'Hem (23220)' => '23029', + 'Le Bouscat (33110)' => '33069', + 'Le Breuil-Bernard (79320)' => '79051', + 'Le Bugue (24260)' => '24067', + 'Le Buis (87140)' => '87023', + 'Le Buisson-de-Cadouin (24480)' => '24068', + 'Le Busseau (79240)' => '79059', + 'Le Chalard (87500)' => '87031', + 'Le Change (24640)' => '24103', + 'Le Chastang (19190)' => '19048', + 'Le Château-d\'Oléron (17480)' => '17093', + 'Le Châtenet-en-Dognon (87400)' => '87042', + 'Le Chauchet (23130)' => '23058', + 'Le Chay (17600)' => '17097', + 'Le Chillou (79600)' => '79089', + 'Le Compas (23700)' => '23066', + 'Le Donzeil (23480)' => '23074', + 'Le Dorat (87210)' => '87059', + 'Le Douhet (17100)' => '17143', + 'Le Fieu (33230)' => '33166', + 'Le Fleix (24130)' => '24182', + 'Le Fouilloux (17270)' => '17167', + 'Le Frêche (40190)' => '40100', + 'Le Gicq (17160)' => '17177', + 'Le Grand-Bourg (23240)' => '23095', + 'Le Grand-Madieu (16450)' => '16157', + 'Le Grand-Village-Plage (17370)' => '17485', + 'Le Gua (17600)' => '17185', + 'Le Gué-d\'Alleré (17540)' => '17186', + 'Le Haillan (33185)' => '33200', + 'Le Jardin (19300)' => '19092', + 'Le Lardin-Saint-Lazare (24570)' => '24229', + 'Le Leuy (40250)' => '40153', + 'Le Lindois (16310)' => '16188', + 'Le Lonzac (19470)' => '19118', + 'Le Mas-d\'Agenais (47430)' => '47159', + 'Le Mas-d\'Artige (23100)' => '23125', + 'Le Monteil-au-Vicomte (23460)' => '23134', + 'Le Mung (17350)' => '17252', + 'Le Nizan (33430)' => '33305', + 'Le Palais-sur-Vienne (87410)' => '87113', + 'Le Passage (47520)' => '47201', + 'Le Pescher (19190)' => '19163', + 'Le Pian-Médoc (33290)' => '33322', + 'Le Pian-sur-Garonne (33490)' => '33323', + 'Le Pin (17210)' => '17276', + 'Le Pin (79140)' => '79210', + 'Le Pizou (24700)' => '24329', + 'Le Porge (33680)' => '33333', + 'Le Pout (33670)' => '33335', + 'Le Puy (33580)' => '33345', + 'Le Retail (79130)' => '79226', + 'Le Rochereau (86170)' => '86208', + 'Le Sen (40420)' => '40297', + 'Le Seure (17770)' => '17426', + 'Le Taillan-Médoc (33320)' => '33519', + 'Le Tallud (79200)' => '79322', + 'Le Tâtre (16360)' => '16380', + 'Le Teich (33470)' => '33527', + 'Le Temple (33680)' => '33528', + 'Le Temple-sur-Lot (47110)' => '47306', + 'Le Thou (17290)' => '17447', + 'Le Tourne (33550)' => '33534', + 'Le Tuzan (33125)' => '33536', + 'Le Vanneau-Irleau (79270)' => '79337', + 'Le Verdon-sur-Mer (33123)' => '33544', + 'Le Vert (79170)' => '79346', + 'Le Vieux-Cérier (16350)' => '16403', + 'Le Vigeant (86150)' => '86289', + 'Le Vigen (87110)' => '87205', + 'Le Vignau (40270)' => '40329', + 'Lecumberry (64220)' => '64327', + 'Lédat (47300)' => '47146', + 'Ledeuix (64400)' => '64328', + 'Lée (64320)' => '64329', + 'Lées-Athas (64490)' => '64330', + 'Lège-Cap-Ferret (33950)' => '33236', + 'Léguillac-de-Cercles (24340)' => '24235', + 'Léguillac-de-l\'Auche (24110)' => '24236', + 'Leigné-les-Bois (86450)' => '86125', + 'Leigné-sur-Usseau (86230)' => '86127', + 'Leignes-sur-Fontaine (86300)' => '86126', + 'Lembeye (64350)' => '64331', + 'Lembras (24100)' => '24237', + 'Lème (64450)' => '64332', + 'Lempzours (24800)' => '24238', + 'Lencloître (86140)' => '86128', + 'Lencouacq (40120)' => '40149', + 'Léogeats (33210)' => '33237', + 'Léognan (33850)' => '33238', + 'Léon (40550)' => '40150', + 'Léoville (17500)' => '17204', + 'Lépaud (23170)' => '23106', + 'Lépinas (23150)' => '23107', + 'Léren (64270)' => '64334', + 'Lerm-et-Musset (33840)' => '33239', + 'Les Adjots (16700)' => '16002', + 'Les Alleuds (79190)' => '79006', + 'Les Angles-sur-Corrèze (19000)' => '19009', + 'Les Artigues-de-Lussac (33570)' => '33014', + 'Les Billanges (87340)' => '87016', + 'Les Billaux (33500)' => '33052', + 'Les Cars (87230)' => '87029', + 'Les Éduts (17510)' => '17149', + 'Les Églises-d\'Argenteuil (17400)' => '17150', + 'Les Églisottes-et-Chalaures (33230)' => '33154', + 'Les Essards (16210)' => '16130', + 'Les Essards (17250)' => '17154', + 'Les Esseintes (33190)' => '33158', + 'Les Eyzies-de-Tayac-Sireuil (24620)' => '24172', + 'Les Farges (24290)' => '24175', + 'Les Forges (79340)' => '79124', + 'Les Fosses (79360)' => '79126', + 'Les Gonds (17100)' => '17179', + 'Les Gours (16140)' => '16155', + 'Les Grands-Chézeaux (87160)' => '87074', + 'Les Graulges (24340)' => '24203', + 'Les Groseillers (79220)' => '79139', + 'Les Lèches (24400)' => '24234', + 'Les Lèves-et-Thoumeyragues (33220)' => '33242', + 'Les Mars (23700)' => '23123', + 'Les Mathes (17570)' => '17225', + 'Les Métairies (16200)' => '16220', + 'Les Nouillers (17380)' => '17266', + 'Les Ormes (86220)' => '86183', + 'Les Peintures (33230)' => '33315', + 'Les Pins (16260)' => '16261', + 'Les Portes-en-Ré (17880)' => '17286', + 'Les Salles-de-Castillon (33350)' => '33499', + 'Les Salles-Lavauguyon (87440)' => '87189', + 'Les Touches-de-Périgny (17160)' => '17451', + 'Les Trois-Moutiers (86120)' => '86274', + 'Lescar (64230)' => '64335', + 'Lescun (64490)' => '64336', + 'Lesgor (40400)' => '40151', + 'Lésignac-Durand (16310)' => '16183', + 'Lésigny (86270)' => '86129', + 'Lesparre-Médoc (33340)' => '33240', + 'Lesperon (40260)' => '40152', + 'Lespielle (64350)' => '64337', + 'Lespourcy (64160)' => '64338', + 'Lessac (16500)' => '16181', + 'Lestards (19170)' => '19112', + 'Lestelle-Bétharram (64800)' => '64339', + 'Lesterps (16420)' => '16182', + 'Lestiac-sur-Garonne (33550)' => '33241', + 'Leugny (86220)' => '86130', + 'Lévignac-de-Guyenne (47120)' => '47147', + 'Lévignacq (40170)' => '40154', + 'Leyrat (23600)' => '23108', + 'Leyritz-Moncassin (47700)' => '47148', + 'Lezay (79120)' => '79148', + 'Lhommaizé (86410)' => '86131', + 'Lhoumois (79390)' => '79149', + 'Libourne (33500)' => '33243', + 'Lichans-Sunhar (64470)' => '64340', + 'Lichères (16460)' => '16184', + 'Lichos (64130)' => '64341', + 'Licq-Athérey (64560)' => '64342', + 'Liginiac (19160)' => '19113', + 'Liglet (86290)' => '86132', + 'Lignan-de-Bazas (33430)' => '33244', + 'Lignan-de-Bordeaux (33360)' => '33245', + 'Lignareix (19200)' => '19114', + 'Ligné (16140)' => '16185', + 'Ligneyrac (19500)' => '19115', + 'Lignières-Sonneville (16130)' => '16186', + 'Ligueux (33220)' => '33246', + 'Ligugé (86240)' => '86133', + 'Limalonges (79190)' => '79150', + 'Limendous (64420)' => '64343', + 'Limeuil (24510)' => '24240', + 'Limeyrat (24210)' => '24241', + 'Limoges (87000)' => '87085', + 'Linard (23220)' => '23109', + 'Linards (87130)' => '87086', + 'Linars (16730)' => '16187', + 'Linazay (86400)' => '86134', + 'Liniers (86800)' => '86135', + 'Linxe (40260)' => '40155', + 'Liorac-sur-Louyre (24520)' => '24242', + 'Liourdres (19120)' => '19116', + 'Lioux-les-Monges (23700)' => '23110', + 'Liposthey (40410)' => '40156', + 'Lisle (24350)' => '24243', + 'Lissac-sur-Couze (19600)' => '19117', + 'Listrac-de-Durèze (33790)' => '33247', + 'Listrac-Médoc (33480)' => '33248', + 'Lit-et-Mixe (40170)' => '40157', + 'Livron (64530)' => '64344', + 'Lizant (86400)' => '86136', + 'Lizières (23240)' => '23111', + 'Lohitzun-Oyhercq (64120)' => '64345', + 'Loire-les-Marais (17870)' => '17205', + 'Loiré-sur-Nie (17470)' => '17206', + 'Loix (17111)' => '17207', + 'Lolme (24540)' => '24244', + 'Lombia (64160)' => '64346', + 'Lonçon (64410)' => '64347', + 'Londigny (16700)' => '16189', + 'Longèves (17230)' => '17208', + 'Longré (16240)' => '16190', + 'Longueville (47200)' => '47150', + 'Lonnes (16230)' => '16191', + 'Lons (64140)' => '64348', + 'Lonzac (17520)' => '17209', + 'Lorignac (17240)' => '17210', + 'Lorigné (79190)' => '79152', + 'Lormont (33310)' => '33249', + 'Losse (40240)' => '40158', + 'Lostanges (19500)' => '19119', + 'Loubejac (24550)' => '24245', + 'Loubens (33190)' => '33250', + 'Loubès-Bernac (47120)' => '47151', + 'Loubieng (64300)' => '64349', + 'Loubigné (79110)' => '79153', + 'Loubillé (79110)' => '79154', + 'Louchats (33125)' => '33251', + 'Loudun (86200)' => '86137', + 'Louer (40380)' => '40159', + 'Lougratte (47290)' => '47152', + 'Louhossoa (64250)' => '64350', + 'Louignac (19310)' => '19120', + 'Louin (79600)' => '79156', + 'Loulay (17330)' => '17211', + 'Loupes (33370)' => '33252', + 'Loupiac (33410)' => '33253', + 'Loupiac-de-la-Réole (33190)' => '33254', + 'Lourdios-Ichère (64570)' => '64351', + 'Lourdoueix-Saint-Pierre (23360)' => '23112', + 'Lourenties (64420)' => '64352', + 'Lourquen (40250)' => '40160', + 'Louvie-Juzon (64260)' => '64353', + 'Louvie-Soubiron (64440)' => '64354', + 'Louvigny (64410)' => '64355', + 'Louzac-Saint-André (16100)' => '16193', + 'Louzignac (17160)' => '17212', + 'Louzy (79100)' => '79157', + 'Lozay (17330)' => '17213', + 'Lubbon (40240)' => '40161', + 'Lubersac (19210)' => '19121', + 'Luc-Armau (64350)' => '64356', + 'Lucarré (64350)' => '64357', + 'Lucbardez-et-Bargues (40090)' => '40162', + 'Lucgarier (64420)' => '64358', + 'Luchapt (86430)' => '86138', + 'Luchat (17600)' => '17214', + 'Luché-sur-Brioux (79170)' => '79158', + 'Luché-Thouarsais (79330)' => '79159', + 'Lucmau (33840)' => '33255', + 'Lucq-de-Béarn (64360)' => '64359', + 'Ludon-Médoc (33290)' => '33256', + 'Lüe (40210)' => '40163', + 'Lugaignac (33420)' => '33257', + 'Lugasson (33760)' => '33258', + 'Luglon (40630)' => '40165', + 'Lugon-et-l\'Île-du-Carnay (33240)' => '33259', + 'Lugos (33830)' => '33260', + 'Lunas (24130)' => '24246', + 'Lupersat (23190)' => '23113', + 'Lupsault (16140)' => '16194', + 'Luquet (65320)' => '65292', + 'Lurbe-Saint-Christau (64660)' => '64360', + 'Lusignac (24320)' => '24247', + 'Lusignan (86600)' => '86139', + 'Lusignan-Petit (47360)' => '47154', + 'Lussac (16450)' => '16195', + 'Lussac (17500)' => '17215', + 'Lussac (33570)' => '33261', + 'Lussac-les-Châteaux (86320)' => '86140', + 'Lussac-les-Églises (87360)' => '87087', + 'Lussagnet (40270)' => '40166', + 'Lussagnet-Lusson (64160)' => '64361', + 'Lussant (17430)' => '17216', + 'Lussas-et-Nontronneau (24300)' => '24248', + 'Lussat (23170)' => '23114', + 'Lusseray (79170)' => '79160', + 'Luxé (16230)' => '16196', + 'Luxe-Sumberraute (64120)' => '64362', + 'Luxey (40430)' => '40167', + 'Luzay (79100)' => '79161', + 'Lys (64260)' => '64363', + 'Macau (33460)' => '33262', + 'Macaye (64240)' => '64364', + 'Macqueville (17490)' => '17217', + 'Madaillan (47360)' => '47155', + 'Madirac (33670)' => '33263', + 'Madranges (19470)' => '19122', + 'Magescq (40140)' => '40168', + 'Magnac-Bourg (87380)' => '87088', + 'Magnac-Laval (87190)' => '87089', + 'Magnac-Lavalette-Villars (16320)' => '16198', + 'Magnac-sur-Touvre (16600)' => '16199', + 'Magnat-l\'Étrange (23260)' => '23115', + 'Magné (79460)' => '79162', + 'Magné (86160)' => '86141', + 'Mailhac-sur-Benaize (87160)' => '87090', + 'Maillas (40120)' => '40169', + 'Maillé (86190)' => '86142', + 'Maillères (40120)' => '40170', + 'Maine-de-Boixe (16230)' => '16200', + 'Mainsat (23700)' => '23116', + 'Mainxe (16200)' => '16202', + 'Mainzac (16380)' => '16203', + 'Mairé (86270)' => '86143', + 'Mairé-Levescault (79190)' => '79163', + 'Maison-Feyne (23800)' => '23117', + 'Maisonnais-sur-Tardoire (87440)' => '87091', + 'Maisonnay (79500)' => '79164', + 'Maisonneuve (86170)' => '86144', + 'Maisonnisses (23150)' => '23118', + 'Maisontiers (79600)' => '79165', + 'Malaussanne (64410)' => '64365', + 'Malaville (16120)' => '16204', + 'Malemort (19360)' => '19123', + 'Malleret (23260)' => '23119', + 'Malleret-Boussac (23600)' => '23120', + 'Malval (23220)' => '23121', + 'Manaurie (24620)' => '24249', + 'Mano (40410)' => '40171', + 'Manot (16500)' => '16205', + 'Mansac (19520)' => '19124', + 'Mansat-la-Courrière (23400)' => '23122', + 'Mansle (16230)' => '16206', + 'Mant (40700)' => '40172', + 'Manzac-sur-Vern (24110)' => '24251', + 'Marans (17230)' => '17218', + 'Maransin (33230)' => '33264', + 'Marc-la-Tour (19150)' => '19127', + 'Marçay (86370)' => '86145', + 'Marcellus (47200)' => '47156', + 'Marcenais (33620)' => '33266', + 'Marcheprime (33380)' => '33555', + 'Marcillac (33860)' => '33267', + 'Marcillac-la-Croisille (19320)' => '19125', + 'Marcillac-la-Croze (19500)' => '19126', + 'Marcillac-Lanville (16140)' => '16207', + 'Marcillac-Saint-Quentin (24200)' => '24252', + 'Marennes (17320)' => '17219', + 'Mareuil (16170)' => '16208', + 'Mareuil (24340)' => '24253', + 'Margaux (33460)' => '33268', + 'Margerides (19200)' => '19128', + 'Margueron (33220)' => '33269', + 'Marignac (17800)' => '17220', + 'Marigny (79360)' => '79166', + 'Marigny-Brizay (86380)' => '86146', + 'Marigny-Chemereau (86370)' => '86147', + 'Marillac-le-Franc (16110)' => '16209', + 'Marimbault (33430)' => '33270', + 'Marions (33690)' => '33271', + 'Marmande (47200)' => '47157', + 'Marmont-Pachas (47220)' => '47158', + 'Marnac (24220)' => '24254', + 'Marnay (86160)' => '86148', + 'Marnes (79600)' => '79167', + 'Marpaps (40330)' => '40173', + 'Marquay (24620)' => '24255', + 'Marsac (16570)' => '16210', + 'Marsac (23210)' => '23124', + 'Marsac-sur-l\'Isle (24430)' => '24256', + 'Marsais (17700)' => '17221', + 'Marsalès (24540)' => '24257', + 'Marsaneix (24750)' => '24258', + 'Marsas (33620)' => '33272', + 'Marsilly (17137)' => '17222', + 'Martaizé (86330)' => '86149', + 'Marthon (16380)' => '16211', + 'Martignas-sur-Jalle (33127)' => '33273', + 'Martillac (33650)' => '33274', + 'Martres (33760)' => '33275', + 'Marval (87440)' => '87092', + 'Masbaraud-Mérignat (23400)' => '23126', + 'Mascaraàs-Haron (64330)' => '64366', + 'Maslacq (64300)' => '64367', + 'Masléon (87130)' => '87093', + 'Masparraute (64120)' => '64368', + 'Maspie-Lalonquère-Juillacq (64350)' => '64369', + 'Masquières (47370)' => '47160', + 'Massac (17490)' => '17223', + 'Massais (79150)' => '79168', + 'Masseilles (33690)' => '33276', + 'Massels (47140)' => '47161', + 'Masseret (19510)' => '19129', + 'Massignac (16310)' => '16212', + 'Massognes (86170)' => '86150', + 'Massoulès (47140)' => '47162', + 'Massugas (33790)' => '33277', + 'Matha (17160)' => '17224', + 'Maucor (64160)' => '64370', + 'Maulay (86200)' => '86151', + 'Mauléon (79700)' => '79079', + 'Mauléon-Licharre (64130)' => '64371', + 'Mauprévoir (86460)' => '86152', + 'Maure (64460)' => '64372', + 'Maurens (24140)' => '24259', + 'Mauriac (33540)' => '33278', + 'Mauries (40320)' => '40174', + 'Maurrin (40270)' => '40175', + 'Maussac (19250)' => '19130', + 'Mautes (23190)' => '23127', + 'Mauvezin-d\'Armagnac (40240)' => '40176', + 'Mauvezin-sur-Gupie (47200)' => '47163', + 'Mauzac-et-Grand-Castang (24150)' => '24260', + 'Mauzé-sur-le-Mignon (79210)' => '79170', + 'Mauzé-Thouarsais (79100)' => '79171', + 'Mauzens-et-Miremont (24260)' => '24261', + 'Mayac (24420)' => '24262', + 'Maylis (40250)' => '40177', + 'Mazeirat (23150)' => '23128', + 'Mazeray (17400)' => '17226', + 'Mazères (33210)' => '33279', + 'Mazères-Lezons (64110)' => '64373', + 'Mazerolles (16310)' => '16213', + 'Mazerolles (17800)' => '17227', + 'Mazerolles (40090)' => '40178', + 'Mazerolles (64230)' => '64374', + 'Mazerolles (86320)' => '86153', + 'Mazeuil (86110)' => '86154', + 'Mazeyrolles (24550)' => '24263', + 'Mazières (16270)' => '16214', + 'Mazières-en-Gâtine (79310)' => '79172', + 'Mazières-Naresse (47210)' => '47164', + 'Mazières-sur-Béronne (79500)' => '79173', + 'Mazion (33390)' => '33280', + 'Méasnes (23360)' => '23130', + 'Médillac (16210)' => '16215', + 'Médis (17600)' => '17228', + 'Mées (40990)' => '40179', + 'Méharin (64120)' => '64375', + 'Meilhac (87800)' => '87094', + 'Meilhan (40400)' => '40180', + 'Meilhan-sur-Garonne (47180)' => '47165', + 'Meilhards (19510)' => '19131', + 'Meillon (64510)' => '64376', + 'Melle (79500)' => '79174', + 'Melleran (79190)' => '79175', + 'Mendionde (64240)' => '64377', + 'Menditte (64130)' => '64378', + 'Mendive (64220)' => '64379', + 'Ménesplet (24700)' => '24264', + 'Ménigoute (79340)' => '79176', + 'Ménoire (19190)' => '19132', + 'Mensignac (24350)' => '24266', + 'Méracq (64410)' => '64380', + 'Mercoeur (19430)' => '19133', + 'Mérignac (16200)' => '16216', + 'Mérignac (17210)' => '17229', + 'Mérignac (33700)' => '33281', + 'Mérignas (33350)' => '33282', + 'Mérinchal (23420)' => '23131', + 'Méritein (64190)' => '64381', + 'Merlines (19340)' => '19134', + 'Merpins (16100)' => '16217', + 'Meschers-sur-Gironde (17132)' => '17230', + 'Mescoules (24240)' => '24267', + 'Mesnac (16370)' => '16218', + 'Mesplède (64370)' => '64382', + 'Messac (17130)' => '17231', + 'Messanges (40660)' => '40181', + 'Messé (79120)' => '79177', + 'Messemé (86200)' => '86156', + 'Mesterrieux (33540)' => '33283', + 'Mestes (19200)' => '19135', + 'Meursac (17120)' => '17232', + 'Meux (17500)' => '17233', + 'Meuzac (87380)' => '87095', + 'Meymac (19250)' => '19136', + 'Meyrals (24220)' => '24268', + 'Meyrignac-l\'Église (19800)' => '19137', + 'Meyssac (19500)' => '19138', + 'Mézin (47170)' => '47167', + 'Mézos (40170)' => '40182', + 'Mialet (24450)' => '24269', + 'Mialos (64410)' => '64383', + 'Mignaloux-Beauvoir (86550)' => '86157', + 'Migné-Auxances (86440)' => '86158', + 'Migré (17330)' => '17234', + 'Migron (17770)' => '17235', + 'Milhac-d\'Auberoche (24330)' => '24270', + 'Milhac-de-Nontron (24470)' => '24271', + 'Millac (86150)' => '86159', + 'Millevaches (19290)' => '19139', + 'Mimbaste (40350)' => '40183', + 'Mimizan (40200)' => '40184', + 'Minzac (24610)' => '24272', + 'Mios (33380)' => '33284', + 'Miossens-Lanusse (64450)' => '64385', + 'Mirambeau (17150)' => '17236', + 'Miramont-de-Guyenne (47800)' => '47168', + 'Miramont-Sensacq (40320)' => '40185', + 'Mirebeau (86110)' => '86160', + 'Mirepeix (64800)' => '64386', + 'Missé (79100)' => '79178', + 'Misson (40290)' => '40186', + 'Moëze (17780)' => '17237', + 'Moirax (47310)' => '47169', + 'Moissannes (87400)' => '87099', + 'Molières (24480)' => '24273', + 'Moliets-et-Maa (40660)' => '40187', + 'Momas (64230)' => '64387', + 'Mombrier (33710)' => '33285', + 'Momuy (40700)' => '40188', + 'Momy (64350)' => '64388', + 'Monassut-Audiracq (64160)' => '64389', + 'Monbahus (47290)' => '47170', + 'Monbalen (47340)' => '47171', + 'Monbazillac (24240)' => '24274', + 'Moncaup (64350)' => '64390', + 'Moncaut (47310)' => '47172', + 'Moncayolle-Larrory-Mendibieu (64130)' => '64391', + 'Monceaux-sur-Dordogne (19400)' => '19140', + 'Moncla (64330)' => '64392', + 'Monclar (47380)' => '47173', + 'Moncontour (86330)' => '86161', + 'Moncoutant (79320)' => '79179', + 'Moncrabeau (47600)' => '47174', + 'Mondion (86230)' => '86162', + 'Monein (64360)' => '64393', + 'Monestier (24240)' => '24276', + 'Monestier-Merlines (19340)' => '19141', + 'Monestier-Port-Dieu (19110)' => '19142', + 'Monfaucon (24130)' => '24277', + 'Monflanquin (47150)' => '47175', + 'Mongaillard (47230)' => '47176', + 'Mongauzy (33190)' => '33287', + 'Monget (40700)' => '40189', + 'Monheurt (47160)' => '47177', + 'Monmadalès (24560)' => '24278', + 'Monmarvès (24560)' => '24279', + 'Monpazier (24540)' => '24280', + 'Monpezat (64350)' => '64394', + 'Monplaisant (24170)' => '24293', + 'Monprimblanc (33410)' => '33288', + 'Mons (16140)' => '16221', + 'Mons (17160)' => '17239', + 'Monsac (24440)' => '24281', + 'Monsaguel (24560)' => '24282', + 'Monsec (24340)' => '24283', + 'Monségur (33580)' => '33289', + 'Monségur (40700)' => '40190', + 'Monségur (47150)' => '47178', + 'Monségur (64460)' => '64395', + 'Monsempron-Libos (47500)' => '47179', + 'Mont (64300)' => '64396', + 'Mont-de-Marsan (40000)' => '40192', + 'Mont-Disse (64330)' => '64401', + 'Montagnac-d\'Auberoche (24210)' => '24284', + 'Montagnac-la-Crempse (24140)' => '24285', + 'Montagnac-sur-Auvignon (47600)' => '47180', + 'Montagnac-sur-Lède (47150)' => '47181', + 'Montagne (33570)' => '33290', + 'Montagoudin (33190)' => '33291', + 'Montagrier (24350)' => '24286', + 'Montagut (64410)' => '64397', + 'Montaignac-Saint-Hippolyte (19300)' => '19143', + 'Montaigut-le-Blanc (23320)' => '23132', + 'Montalembert (79190)' => '79180', + 'Montamisé (86360)' => '86163', + 'Montaner (64460)' => '64398', + 'Montardon (64121)' => '64399', + 'Montastruc (47380)' => '47182', + 'Montauriol (47330)' => '47183', + 'Montaut (24560)' => '24287', + 'Montaut (40500)' => '40191', + 'Montaut (47210)' => '47184', + 'Montaut (64800)' => '64400', + 'Montayral (47500)' => '47185', + 'Montazeau (24230)' => '24288', + 'Montboucher (23400)' => '23133', + 'Montboyer (16620)' => '16222', + 'Montbron (16220)' => '16223', + 'Montcaret (24230)' => '24289', + 'Montégut (40190)' => '40193', + 'Montemboeuf (16310)' => '16225', + 'Montendre (17130)' => '17240', + 'Montesquieu (47130)' => '47186', + 'Monteton (47120)' => '47187', + 'Montferrand-du-Périgord (24440)' => '24290', + 'Montfort (64190)' => '64403', + 'Montfort-en-Chalosse (40380)' => '40194', + 'Montgaillard (40500)' => '40195', + 'Montgibaud (19210)' => '19144', + 'Montguyon (17270)' => '17241', + 'Monthoiron (86210)' => '86164', + 'Montignac (24290)' => '24291', + 'Montignac (33760)' => '33292', + 'Montignac-Charente (16330)' => '16226', + 'Montignac-de-Lauzun (47800)' => '47188', + 'Montignac-le-Coq (16390)' => '16227', + 'Montignac-Toupinerie (47350)' => '47189', + 'Montigné (16170)' => '16228', + 'Montils (17800)' => '17242', + 'Montjean (16240)' => '16229', + 'Montlieu-la-Garde (17210)' => '17243', + 'Montmérac (16300)' => '16224', + 'Montmoreau-Saint-Cybard (16190)' => '16230', + 'Montmorillon (86500)' => '86165', + 'Montory (64470)' => '64404', + 'Montpellier-de-Médillan (17260)' => '17244', + 'Montpeyroux (24610)' => '24292', + 'Montpezat (47360)' => '47190', + 'Montpon-Ménestérol (24700)' => '24294', + 'Montpouillan (47200)' => '47191', + 'Montravers (79140)' => '79183', + 'Montrem (24110)' => '24295', + 'Montreuil-Bonnin (86470)' => '86166', + 'Montrol-Sénard (87330)' => '87100', + 'Montrollet (16420)' => '16231', + 'Montroy (17220)' => '17245', + 'Monts-sur-Guesnes (86420)' => '86167', + 'Montsoué (40500)' => '40196', + 'Montussan (33450)' => '33293', + 'Monviel (47290)' => '47192', + 'Moragne (17430)' => '17246', + 'Morcenx (40110)' => '40197', + 'Morganx (40700)' => '40198', + 'Morizès (33190)' => '33294', + 'Morlaàs (64160)' => '64405', + 'Morlanne (64370)' => '64406', + 'Mornac (16600)' => '16232', + 'Mornac-sur-Seudre (17113)' => '17247', + 'Mortagne-sur-Gironde (17120)' => '17248', + 'Mortemart (87330)' => '87101', + 'Mortiers (17500)' => '17249', + 'Morton (86120)' => '86169', + 'Mortroux (23220)' => '23136', + 'Mosnac (16120)' => '16233', + 'Mosnac (17240)' => '17250', + 'Mougon (79370)' => '79185', + 'Mouguerre (64990)' => '64407', + 'Mouhous (64330)' => '64408', + 'Mouillac (33240)' => '33295', + 'Mouleydier (24520)' => '24296', + 'Moulidars (16290)' => '16234', + 'Mouliets-et-Villemartin (33350)' => '33296', + 'Moulin-Neuf (24700)' => '24297', + 'Moulinet (47290)' => '47193', + 'Moulis-en-Médoc (33480)' => '33297', + 'Moulismes (86500)' => '86170', + 'Moulon (33420)' => '33298', + 'Moumour (64400)' => '64409', + 'Mourens (33410)' => '33299', + 'Mourenx (64150)' => '64410', + 'Mourioux-Vieilleville (23210)' => '23137', + 'Mouscardès (40290)' => '40199', + 'Moussac (86150)' => '86171', + 'Moustey (40410)' => '40200', + 'Moustier (47800)' => '47194', + 'Moustier-Ventadour (19300)' => '19145', + 'Mouterre-Silly (86200)' => '86173', + 'Mouterre-sur-Blourde (86430)' => '86172', + 'Mouthiers-sur-Boëme (16440)' => '16236', + 'Moutier-d\'Ahun (23150)' => '23138', + 'Moutier-Malcard (23220)' => '23139', + 'Moutier-Rozeille (23200)' => '23140', + 'Moutiers-sous-Chantemerle (79320)' => '79188', + 'Mouton (16460)' => '16237', + 'Moutonneau (16460)' => '16238', + 'Mouzon (16310)' => '16239', + 'Mugron (40250)' => '40201', + 'Muron (17430)' => '17253', + 'Musculdy (64130)' => '64411', + 'Mussidan (24400)' => '24299', + 'Nabas (64190)' => '64412', + 'Nabinaud (16390)' => '16240', + 'Nabirat (24250)' => '24300', + 'Nachamps (17380)' => '17254', + 'Nadaillac (24590)' => '24301', + 'Nailhac (24390)' => '24302', + 'Naillat (23800)' => '23141', + 'Naintré (86530)' => '86174', + 'Nalliers (86310)' => '86175', + 'Nanclars (16230)' => '16241', + 'Nancras (17600)' => '17255', + 'Nanteuil (79400)' => '79189', + 'Nanteuil-Auriac-de-Bourzac (24320)' => '24303', + 'Nanteuil-en-Vallée (16700)' => '16242', + 'Nantheuil (24800)' => '24304', + 'Nanthiat (24800)' => '24305', + 'Nantiat (87140)' => '87103', + 'Nantillé (17770)' => '17256', + 'Narcastet (64510)' => '64413', + 'Narp (64190)' => '64414', + 'Narrosse (40180)' => '40202', + 'Nassiet (40330)' => '40203', + 'Nastringues (24230)' => '24306', + 'Naujac-sur-Mer (33990)' => '33300', + 'Naujan-et-Postiac (33420)' => '33301', + 'Naussannes (24440)' => '24307', + 'Navailles-Angos (64450)' => '64415', + 'Navarrenx (64190)' => '64416', + 'Naves (19460)' => '19146', + 'Nay (64800)' => '64417', + 'Néac (33500)' => '33302', + 'Nedde (87120)' => '87104', + 'Négrondes (24460)' => '24308', + 'Néoux (23200)' => '23142', + 'Nérac (47600)' => '47195', + 'Nerbis (40250)' => '40204', + 'Nercillac (16200)' => '16243', + 'Néré (17510)' => '17257', + 'Nérigean (33750)' => '33303', + 'Nérignac (86150)' => '86176', + 'Nersac (16440)' => '16244', + 'Nespouls (19600)' => '19147', + 'Neuffons (33580)' => '33304', + 'Neuillac (17520)' => '17258', + 'Neulles (17500)' => '17259', + 'Neuvic (19160)' => '19148', + 'Neuvic (24190)' => '24309', + 'Neuvic-Entier (87130)' => '87105', + 'Neuvicq (17270)' => '17260', + 'Neuvicq-le-Château (17490)' => '17261', + 'Neuville (19380)' => '19149', + 'Neuville-de-Poitou (86170)' => '86177', + 'Neuvy-Bouin (79130)' => '79190', + 'Nexon (87800)' => '87106', + 'Nicole (47190)' => '47196', + 'Nieuil (16270)' => '16245', + 'Nieuil-l\'Espoir (86340)' => '86178', + 'Nieul (87510)' => '87107', + 'Nieul-le-Virouil (17150)' => '17263', + 'Nieul-lès-Saintes (17810)' => '17262', + 'Nieul-sur-Mer (17137)' => '17264', + 'Nieulle-sur-Seudre (17600)' => '17265', + 'Niort (79000)' => '79191', + 'Noailhac (19500)' => '19150', + 'Noaillac (33190)' => '33306', + 'Noaillan (33730)' => '33307', + 'Noailles (19600)' => '19151', + 'Noguères (64150)' => '64418', + 'Nomdieu (47600)' => '47197', + 'Nonac (16190)' => '16246', + 'Nonards (19120)' => '19152', + 'Nonaville (16120)' => '16247', + 'Nontron (24300)' => '24311', + 'Noth (23300)' => '23143', + 'Notre-Dame-de-Sanilhac (24660)' => '24312', + 'Nouaillé-Maupertuis (86340)' => '86180', + 'Nouhant (23170)' => '23145', + 'Nouic (87330)' => '87108', + 'Nousse (40380)' => '40205', + 'Nousty (64420)' => '64419', + 'Nouzerines (23600)' => '23146', + 'Nouzerolles (23360)' => '23147', + 'Nouziers (23350)' => '23148', + 'Nuaillé-d\'Aunis (17540)' => '17267', + 'Nuaillé-sur-Boutonne (17470)' => '17268', + 'Nueil-les-Aubiers (79250)' => '79195', + 'Nueil-sous-Faye (86200)' => '86181', + 'Objat (19130)' => '19153', + 'Oeyregave (40300)' => '40206', + 'Oeyreluy (40180)' => '40207', + 'Ogenne-Camptort (64190)' => '64420', + 'Ogeu-les-Bains (64680)' => '64421', + 'Oiron (79100)' => '79196', + 'Oloron-Sainte-Marie (64400)' => '64422', + 'Omet (33410)' => '33308', + 'Onard (40380)' => '40208', + 'Ondres (40440)' => '40209', + 'Onesse-Laharie (40110)' => '40210', + 'Oraàs (64390)' => '64423', + 'Oradour (16140)' => '16248', + 'Oradour-Fanais (16500)' => '16249', + 'Oradour-Saint-Genest (87210)' => '87109', + 'Oradour-sur-Glane (87520)' => '87110', + 'Oradour-sur-Vayres (87150)' => '87111', + 'Orches (86230)' => '86182', + 'Ordiarp (64130)' => '64424', + 'Ordonnac (33340)' => '33309', + 'Orègue (64120)' => '64425', + 'Orgedeuil (16220)' => '16250', + 'Orgnac-sur-Vézère (19410)' => '19154', + 'Origne (33113)' => '33310', + 'Orignolles (17210)' => '17269', + 'Orin (64400)' => '64426', + 'Oriolles (16480)' => '16251', + 'Orion (64390)' => '64427', + 'Orist (40300)' => '40211', + 'Orival (16210)' => '16252', + 'Orliac (24170)' => '24313', + 'Orliac-de-Bar (19390)' => '19155', + 'Orliaguet (24370)' => '24314', + 'Oroux (79390)' => '79197', + 'Orriule (64390)' => '64428', + 'Orsanco (64120)' => '64429', + 'Orthevielle (40300)' => '40212', + 'Orthez (64300)' => '64430', + 'Orx (40230)' => '40213', + 'Os-Marsillon (64150)' => '64431', + 'Ossages (40290)' => '40214', + 'Ossas-Suhare (64470)' => '64432', + 'Osse-en-Aspe (64490)' => '64433', + 'Ossenx (64190)' => '64434', + 'Osserain-Rivareyte (64390)' => '64435', + 'Ossès (64780)' => '64436', + 'Ostabat-Asme (64120)' => '64437', + 'Ouillon (64160)' => '64438', + 'Ousse (64320)' => '64439', + 'Ousse-Suzan (40110)' => '40215', + 'Ouzilly (86380)' => '86184', + 'Oyré (86220)' => '86186', + 'Ozenx-Montestrucq (64300)' => '64440', + 'Ozillac (17500)' => '17270', + 'Ozourt (40380)' => '40216', + 'Pageas (87230)' => '87112', + 'Pagolle (64120)' => '64441', + 'Paillé (17470)' => '17271', + 'Paillet (33550)' => '33311', + 'Pailloles (47440)' => '47198', + 'Paizay-le-Chapt (79170)' => '79198', + 'Paizay-le-Sec (86300)' => '86187', + 'Paizay-le-Tort (79500)' => '79199', + 'Paizay-Naudouin-Embourie (16240)' => '16253', + 'Palazinges (19190)' => '19156', + 'Palisse (19160)' => '19157', + 'Palluaud (16390)' => '16254', + 'Pamplie (79220)' => '79200', + 'Pamproux (79800)' => '79201', + 'Panazol (87350)' => '87114', + 'Pandrignes (19150)' => '19158', + 'Parbayse (64360)' => '64442', + 'Parcoul-Chenaud (24410)' => '24316', + 'Pardaillan (47120)' => '47199', + 'Pardies (64150)' => '64443', + 'Pardies-Piétat (64800)' => '64444', + 'Parempuyre (33290)' => '33312', + 'Parentis-en-Born (40160)' => '40217', + 'Parleboscq (40310)' => '40218', + 'Parranquet (47210)' => '47200', + 'Parsac-Rimondeix (23140)' => '23149', + 'Parthenay (79200)' => '79202', + 'Parzac (16450)' => '16255', + 'Pas-de-Jeu (79100)' => '79203', + 'Passirac (16480)' => '16256', + 'Pau (64000)' => '64445', + 'Pauillac (33250)' => '33314', + 'Paulhiac (47150)' => '47202', + 'Paulin (24590)' => '24317', + 'Paunat (24510)' => '24318', + 'Paussac-et-Saint-Vivien (24310)' => '24319', + 'Payré (86700)' => '86188', + 'Payros-Cazautets (40320)' => '40219', + 'Payroux (86350)' => '86189', + 'Pays de Belvès (24170)' => '24035', + 'Payzac (24270)' => '24320', + 'Pazayac (24120)' => '24321', + 'Pécorade (40320)' => '40220', + 'Pellegrue (33790)' => '33316', + 'Penne-d\'Agenais (47140)' => '47203', + 'Pensol (87440)' => '87115', + 'Péré (17700)' => '17272', + 'Péret-Bel-Air (19300)' => '19159', + 'Pérignac (16250)' => '16258', + 'Pérignac (17800)' => '17273', + 'Périgné (79170)' => '79204', + 'Périgny (17180)' => '17274', + 'Périgueux (24000)' => '24322', + 'Périssac (33240)' => '33317', + 'Pérols-sur-Vézère (19170)' => '19160', + 'Perpezac-le-Blanc (19310)' => '19161', + 'Perpezac-le-Noir (19410)' => '19162', + 'Perquie (40190)' => '40221', + 'Pers (79190)' => '79205', + 'Persac (86320)' => '86190', + 'Pessac (33600)' => '33318', + 'Pessac-sur-Dordogne (33890)' => '33319', + 'Pessines (17810)' => '17275', + 'Petit-Bersac (24600)' => '24323', + 'Petit-Palais-et-Cornemps (33570)' => '33320', + 'Peujard (33240)' => '33321', + 'Pey (40300)' => '40222', + 'Peyrabout (23000)' => '23150', + 'Peyrat-de-Bellac (87300)' => '87116', + 'Peyrat-la-Nonière (23130)' => '23151', + 'Peyrat-le-Château (87470)' => '87117', + 'Peyre (40700)' => '40223', + 'Peyrehorade (40300)' => '40224', + 'Peyrelevade (19290)' => '19164', + 'Peyrelongue-Abos (64350)' => '64446', + 'Peyrière (47350)' => '47204', + 'Peyrignac (24210)' => '24324', + 'Peyrilhac (87510)' => '87118', + 'Peyrillac-et-Millac (24370)' => '24325', + 'Peyrissac (19260)' => '19165', + 'Peyzac-le-Moustier (24620)' => '24326', + 'Pezuls (24510)' => '24327', + 'Philondenx (40320)' => '40225', + 'Piégut-Pluviers (24360)' => '24328', + 'Pierre-Buffière (87260)' => '87119', + 'Pierrefitte (19450)' => '19166', + 'Pierrefitte (23130)' => '23152', + 'Pierrefitte (79330)' => '79209', + 'Piets-Plasence-Moustrou (64410)' => '64447', + 'Pillac (16390)' => '16260', + 'Pimbo (40320)' => '40226', + 'Pindères (47700)' => '47205', + 'Pindray (86500)' => '86191', + 'Pinel-Hauterive (47380)' => '47206', + 'Pineuilh (33220)' => '33324', + 'Pionnat (23140)' => '23154', + 'Pioussay (79110)' => '79211', + 'Pisany (17600)' => '17278', + 'Pissos (40410)' => '40227', + 'Plaisance (24560)' => '24168', + 'Plaisance (86500)' => '86192', + 'Plassac (17240)' => '17279', + 'Plassac (33390)' => '33325', + 'Plassac-Rouffiac (16250)' => '16263', + 'Plassay (17250)' => '17280', + 'Plazac (24580)' => '24330', + 'Pleine-Selve (33820)' => '33326', + 'Pleumartin (86450)' => '86193', + 'Pleuville (16490)' => '16264', + 'Pliboux (79190)' => '79212', + 'Podensac (33720)' => '33327', + 'Poey-d\'Oloron (64400)' => '64449', + 'Poey-de-Lescar (64230)' => '64448', + 'Poitiers (86000)' => '86194', + 'Polignac (17210)' => '17281', + 'Pomarez (40360)' => '40228', + 'Pomerol (33500)' => '33328', + 'Pommiers-Moulons (17130)' => '17282', + 'Pompaire (79200)' => '79213', + 'Pompéjac (33730)' => '33329', + 'Pompiey (47230)' => '47207', + 'Pompignac (33370)' => '33330', + 'Pompogne (47420)' => '47208', + 'Pomport (24240)' => '24331', + 'Pomps (64370)' => '64450', + 'Pondaurat (33190)' => '33331', + 'Pons (17800)' => '17283', + 'Ponson-Debat-Pouts (64460)' => '64451', + 'Ponson-Dessus (64460)' => '64452', + 'Pont-du-Casse (47480)' => '47209', + 'Pont-l\'Abbé-d\'Arnoult (17250)' => '17284', + 'Pontacq (64530)' => '64453', + 'Pontarion (23250)' => '23155', + 'Pontcharraud (23260)' => '23156', + 'Pontenx-les-Forges (40200)' => '40229', + 'Ponteyraud (24410)' => '24333', + 'Pontiacq-Viellepinte (64460)' => '64454', + 'Pontonx-sur-l\'Adour (40465)' => '40230', + 'Pontours (24150)' => '24334', + 'Porchères (33660)' => '33332', + 'Port-d\'Envaux (17350)' => '17285', + 'Port-de-Lanne (40300)' => '40231', + 'Port-de-Piles (86220)' => '86195', + 'Port-des-Barques (17730)' => '17484', + 'Port-Sainte-Foy-et-Ponchapt (33220)' => '24335', + 'Port-Sainte-Marie (47130)' => '47210', + 'Portet (64330)' => '64455', + 'Portets (33640)' => '33334', + 'Pouançay (86120)' => '86196', + 'Pouant (86200)' => '86197', + 'Poudenas (47170)' => '47211', + 'Poudenx (40700)' => '40232', + 'Pouffonds (79500)' => '79214', + 'Pougne-Hérisson (79130)' => '79215', + 'Pouillac (17210)' => '17287', + 'Pouillé (86800)' => '86198', + 'Pouillon (40350)' => '40233', + 'Pouliacq (64410)' => '64456', + 'Poullignac (16190)' => '16267', + 'Poursac (16700)' => '16268', + 'Poursay-Garnaud (17400)' => '17288', + 'Poursiugues-Boucoue (64410)' => '64457', + 'Poussanges (23500)' => '23158', + 'Poussignac (47700)' => '47212', + 'Pouydesseaux (40120)' => '40234', + 'Poyanne (40380)' => '40235', + 'Poyartin (40380)' => '40236', + 'Pradines (19170)' => '19168', + 'Prahecq (79230)' => '79216', + 'Prailles (79370)' => '79217', + 'Pranzac (16110)' => '16269', + 'Prats-de-Carlux (24370)' => '24336', + 'Prats-du-Périgord (24550)' => '24337', + 'Prayssas (47360)' => '47213', + 'Préchac (33730)' => '33336', + 'Préchacq-Josbaig (64190)' => '64458', + 'Préchacq-les-Bains (40465)' => '40237', + 'Préchacq-Navarrenx (64190)' => '64459', + 'Précilhon (64400)' => '64460', + 'Préguillac (17460)' => '17289', + 'Preignac (33210)' => '33337', + 'Pressac (86460)' => '86200', + 'Pressignac (16150)' => '16270', + 'Pressignac-Vicq (24150)' => '24338', + 'Pressigny (79390)' => '79218', + 'Preyssac-d\'Excideuil (24160)' => '24339', + 'Priaires (79210)' => '79219', + 'Prignac (17160)' => '17290', + 'Prignac-en-Médoc (33340)' => '33338', + 'Prignac-et-Marcamps (33710)' => '33339', + 'Prigonrieux (24130)' => '24340', + 'Prin-Deyrançon (79210)' => '79220', + 'Prinçay (86420)' => '86201', + 'Prissé-la-Charrière (79360)' => '79078', + 'Proissans (24200)' => '24341', + 'Puch-d\'Agenais (47160)' => '47214', + 'Pugnac (33710)' => '33341', + 'Pugny (79320)' => '79222', + 'Puihardy (79160)' => '79223', + 'Puilboreau (17138)' => '17291', + 'Puisseguin (33570)' => '33342', + 'Pujo-le-Plan (40190)' => '40238', + 'Pujols (33350)' => '33344', + 'Pujols (47300)' => '47215', + 'Pujols-sur-Ciron (33210)' => '33343', + 'Puy-d\'Arnac (19120)' => '19169', + 'Puy-du-Lac (17380)' => '17292', + 'Puy-Malsignat (23130)' => '23159', + 'Puybarban (33190)' => '33346', + 'Puymiclan (47350)' => '47216', + 'Puymirol (47270)' => '47217', + 'Puymoyen (16400)' => '16271', + 'Puynormand (33660)' => '33347', + 'Puyol-Cazalet (40320)' => '40239', + 'Puyoô (64270)' => '64461', + 'Puyravault (17700)' => '17293', + 'Puyréaux (16230)' => '16272', + 'Puyrenier (24340)' => '24344', + 'Puyrolland (17380)' => '17294', + 'Puysserampion (47800)' => '47218', + 'Queaux (86150)' => '86203', + 'Queyrac (33340)' => '33348', + 'Queyssac (24140)' => '24345', + 'Queyssac-les-Vignes (19120)' => '19170', + 'Quinçay (86190)' => '86204', + 'Quinsac (24530)' => '24346', + 'Quinsac (33360)' => '33349', + 'Raix (16240)' => '16273', + 'Ramous (64270)' => '64462', + 'Rampieux (24440)' => '24347', + 'Rancogne (16110)' => '16274', + 'Rancon (87290)' => '87121', + 'Ranton (86200)' => '86205', + 'Ranville-Breuillaud (16140)' => '16275', + 'Raslay (86120)' => '86206', + 'Rauzan (33420)' => '33350', + 'Rayet (47210)' => '47219', + 'Razac-d\'Eymet (24500)' => '24348', + 'Razac-de-Saussignac (24240)' => '24349', + 'Razac-sur-l\'Isle (24430)' => '24350', + 'Razès (87640)' => '87122', + 'Razimet (47160)' => '47220', + 'Réaup-Lisse (47170)' => '47221', + 'Réaux sur Trèfle (17500)' => '17295', + 'Rébénacq (64260)' => '64463', + 'Reffannes (79420)' => '79225', + 'Reignac (16360)' => '16276', + 'Reignac (33860)' => '33351', + 'Rempnat (87120)' => '87123', + 'Renung (40270)' => '40240', + 'Réparsac (16200)' => '16277', + 'Rétaud (17460)' => '17296', + 'Reterre (23110)' => '23160', + 'Retjons (40120)' => '40164', + 'Reygade (19430)' => '19171', + 'Ribagnac (24240)' => '24351', + 'Ribarrouy (64330)' => '64464', + 'Ribérac (24600)' => '24352', + 'Rilhac-Lastours (87800)' => '87124', + 'Rilhac-Rancon (87570)' => '87125', + 'Rilhac-Treignac (19260)' => '19172', + 'Rilhac-Xaintrie (19220)' => '19173', + 'Rimbez-et-Baudiets (40310)' => '40242', + 'Rimons (33580)' => '33353', + 'Riocaud (33220)' => '33354', + 'Rion-des-Landes (40370)' => '40243', + 'Rions (33410)' => '33355', + 'Rioux (17460)' => '17298', + 'Rioux-Martin (16210)' => '16279', + 'Riupeyrous (64160)' => '64465', + 'Rivedoux-Plage (17940)' => '17297', + 'Rivehaute (64190)' => '64466', + 'Rives (47210)' => '47223', + 'Rivière-Saas-et-Gourby (40180)' => '40244', + 'Rivières (16110)' => '16280', + 'Roaillan (33210)' => '33357', + 'Roche-le-Peyroux (19160)' => '19175', + 'Rochechouart (87600)' => '87126', + 'Rochefort (17300)' => '17299', + 'Roches (23270)' => '23162', + 'Roches-Prémarie-Andillé (86340)' => '86209', + 'Roiffé (86120)' => '86210', + 'Rom (79120)' => '79230', + 'Romagne (33760)' => '33358', + 'Romagne (86700)' => '86211', + 'Romans (79260)' => '79231', + 'Romazières (17510)' => '17301', + 'Romegoux (17250)' => '17302', + 'Romestaing (47250)' => '47224', + 'Ronsenac (16320)' => '16283', + 'Rontignon (64110)' => '64467', + 'Roquebrune (33580)' => '33359', + 'Roquefort (40120)' => '40245', + 'Roquefort (47310)' => '47225', + 'Roquiague (64130)' => '64468', + 'Rosiers-d\'Égletons (19300)' => '19176', + 'Rosiers-de-Juillac (19350)' => '19177', + 'Rouffiac (16210)' => '16284', + 'Rouffiac (17800)' => '17304', + 'Rouffignac (17130)' => '17305', + 'Rouffignac-de-Sigoulès (24240)' => '24357', + 'Rouffignac-Saint-Cernin-de-Reilhac (24580)' => '24356', + 'Rougnac (16320)' => '16285', + 'Rougnat (23700)' => '23164', + 'Rouillac (16170)' => '16286', + 'Rouillé (86480)' => '86213', + 'Roullet-Saint-Estèphe (16440)' => '16287', + 'Roumagne (47800)' => '47226', + 'Roumazières-Loubert (16270)' => '16192', + 'Roussac (87140)' => '87128', + 'Roussines (16310)' => '16289', + 'Rouzède (16220)' => '16290', + 'Royan (17200)' => '17306', + 'Royère-de-Vassivière (23460)' => '23165', + 'Royères (87400)' => '87129', + 'Roziers-Saint-Georges (87130)' => '87130', + 'Ruch (33350)' => '33361', + 'Rudeau-Ladosse (24340)' => '24221', + 'Ruelle-sur-Touvre (16600)' => '16291', + 'Ruffec (16700)' => '16292', + 'Ruffiac (47700)' => '47227', + 'Sablonceaux (17600)' => '17307', + 'Sablons (33910)' => '33362', + 'Sabres (40630)' => '40246', + 'Sadillac (24500)' => '24359', + 'Sadirac (33670)' => '33363', + 'Sadroc (19270)' => '19178', + 'Sagelat (24170)' => '24360', + 'Sagnat (23800)' => '23166', + 'Saillac (19500)' => '19179', + 'Saillans (33141)' => '33364', + 'Saillat-sur-Vienne (87720)' => '87131', + 'Saint Aulaye-Puymangou (24410)' => '24376', + 'Saint Maurice Étusson (79150)' => '79280', + 'Saint-Abit (64800)' => '64469', + 'Saint-Adjutory (16310)' => '16293', + 'Saint-Agnant (17620)' => '17308', + 'Saint-Agnant-de-Versillat (23300)' => '23177', + 'Saint-Agnant-près-Crocq (23260)' => '23178', + 'Saint-Agne (24520)' => '24361', + 'Saint-Agnet (40800)' => '40247', + 'Saint-Aignan (33126)' => '33365', + 'Saint-Aigulin (17360)' => '17309', + 'Saint-Alpinien (23200)' => '23179', + 'Saint-Amand (23200)' => '23180', + 'Saint-Amand-de-Coly (24290)' => '24364', + 'Saint-Amand-de-Vergt (24380)' => '24365', + 'Saint-Amand-Jartoudeix (23400)' => '23181', + 'Saint-Amand-le-Petit (87120)' => '87132', + 'Saint-Amand-Magnazeix (87290)' => '87133', + 'Saint-Amand-sur-Sèvre (79700)' => '79235', + 'Saint-Amant-de-Boixe (16330)' => '16295', + 'Saint-Amant-de-Bonnieure (16230)' => '16296', + 'Saint-Amant-de-Montmoreau (16190)' => '16294', + 'Saint-Amant-de-Nouère (16170)' => '16298', + 'Saint-André-d\'Allas (24200)' => '24366', + 'Saint-André-de-Cubzac (33240)' => '33366', + 'Saint-André-de-Double (24190)' => '24367', + 'Saint-André-de-Lidon (17260)' => '17310', + 'Saint-André-de-Seignanx (40390)' => '40248', + 'Saint-André-du-Bois (33490)' => '33367', + 'Saint-André-et-Appelles (33220)' => '33369', + 'Saint-André-sur-Sèvre (79380)' => '79236', + 'Saint-Androny (33390)' => '33370', + 'Saint-Angeau (16230)' => '16300', + 'Saint-Angel (19200)' => '19180', + 'Saint-Antoine-Cumond (24410)' => '24368', + 'Saint-Antoine-d\'Auberoche (24330)' => '24369', + 'Saint-Antoine-de-Breuilh (24230)' => '24370', + 'Saint-Antoine-de-Ficalba (47340)' => '47228', + 'Saint-Antoine-du-Queyret (33790)' => '33372', + 'Saint-Antoine-sur-l\'Isle (33660)' => '33373', + 'Saint-Aquilin (24110)' => '24371', + 'Saint-Armou (64160)' => '64470', + 'Saint-Astier (24110)' => '24372', + 'Saint-Astier (47120)' => '47229', + 'Saint-Aubin (40250)' => '40249', + 'Saint-Aubin (47150)' => '47230', + 'Saint-Aubin-de-Blaye (33820)' => '33374', + 'Saint-Aubin-de-Branne (33420)' => '33375', + 'Saint-Aubin-de-Cadelech (24500)' => '24373', + 'Saint-Aubin-de-Lanquais (24560)' => '24374', + 'Saint-Aubin-de-Médoc (33160)' => '33376', + 'Saint-Aubin-de-Nabirat (24250)' => '24375', + 'Saint-Aubin-du-Plain (79300)' => '79238', + 'Saint-Aubin-le-Cloud (79450)' => '79239', + 'Saint-Augustin (17570)' => '17311', + 'Saint-Augustin (19390)' => '19181', + 'Saint-Aulaire (19130)' => '19182', + 'Saint-Aulais-la-Chapelle (16300)' => '16301', + 'Saint-Auvent (87310)' => '87135', + 'Saint-Avit (16210)' => '16302', + 'Saint-Avit (40090)' => '40250', + 'Saint-Avit (47350)' => '47231', + 'Saint-Avit-de-Soulège (33220)' => '33377', + 'Saint-Avit-de-Tardes (23200)' => '23182', + 'Saint-Avit-de-Vialard (24260)' => '24377', + 'Saint-Avit-le-Pauvre (23480)' => '23183', + 'Saint-Avit-Rivière (24540)' => '24378', + 'Saint-Avit-Saint-Nazaire (33220)' => '33378', + 'Saint-Avit-Sénieur (24440)' => '24379', + 'Saint-Barbant (87330)' => '87136', + 'Saint-Bard (23260)' => '23184', + 'Saint-Barthélemy (40390)' => '40251', + 'Saint-Barthélemy-d\'Agenais (47350)' => '47232', + 'Saint-Barthélemy-de-Bellegarde (24700)' => '24380', + 'Saint-Barthélemy-de-Bussière (24360)' => '24381', + 'Saint-Bazile (87150)' => '87137', + 'Saint-Bazile-de-la-Roche (19320)' => '19183', + 'Saint-Bazile-de-Meyssac (19500)' => '19184', + 'Saint-Benoît (86280)' => '86214', + 'Saint-Boès (64300)' => '64471', + 'Saint-Bonnet (16300)' => '16303', + 'Saint-Bonnet-Avalouze (19150)' => '19185', + 'Saint-Bonnet-Briance (87260)' => '87138', + 'Saint-Bonnet-de-Bellac (87300)' => '87139', + 'Saint-Bonnet-Elvert (19380)' => '19186', + 'Saint-Bonnet-l\'Enfantier (19410)' => '19188', + 'Saint-Bonnet-la-Rivière (19130)' => '19187', + 'Saint-Bonnet-les-Tours-de-Merle (19430)' => '19189', + 'Saint-Bonnet-près-Bort (19200)' => '19190', + 'Saint-Bonnet-sur-Gironde (17150)' => '17312', + 'Saint-Brice (16100)' => '16304', + 'Saint-Brice (33540)' => '33379', + 'Saint-Brice-sur-Vienne (87200)' => '87140', + 'Saint-Bris-des-Bois (17770)' => '17313', + 'Saint-Caprais-de-Blaye (33820)' => '33380', + 'Saint-Caprais-de-Bordeaux (33880)' => '33381', + 'Saint-Caprais-de-Lerm (47270)' => '47234', + 'Saint-Capraise-d\'Eymet (24500)' => '24383', + 'Saint-Capraise-de-Lalinde (24150)' => '24382', + 'Saint-Cassien (24540)' => '24384', + 'Saint-Castin (64160)' => '64472', + 'Saint-Cernin-de-l\'Herm (24550)' => '24386', + 'Saint-Cernin-de-Labarde (24560)' => '24385', + 'Saint-Cernin-de-Larche (19600)' => '19191', + 'Saint-Césaire (17770)' => '17314', + 'Saint-Chabrais (23130)' => '23185', + 'Saint-Chamant (19380)' => '19192', + 'Saint-Chamassy (24260)' => '24388', + 'Saint-Christoly-de-Blaye (33920)' => '33382', + 'Saint-Christoly-Médoc (33340)' => '33383', + 'Saint-Christophe (16420)' => '16306', + 'Saint-Christophe (17220)' => '17315', + 'Saint-Christophe (23000)' => '23186', + 'Saint-Christophe (86230)' => '86217', + 'Saint-Christophe-de-Double (33230)' => '33385', + 'Saint-Christophe-des-Bardes (33330)' => '33384', + 'Saint-Christophe-sur-Roc (79220)' => '79241', + 'Saint-Cibard (33570)' => '33386', + 'Saint-Ciers-Champagne (17520)' => '17316', + 'Saint-Ciers-d\'Abzac (33910)' => '33387', + 'Saint-Ciers-de-Canesse (33710)' => '33388', + 'Saint-Ciers-du-Taillon (17240)' => '17317', + 'Saint-Ciers-sur-Bonnieure (16230)' => '16307', + 'Saint-Ciers-sur-Gironde (33820)' => '33389', + 'Saint-Cirgues-la-Loutre (19220)' => '19193', + 'Saint-Cirq (24260)' => '24389', + 'Saint-Clair (86330)' => '86218', + 'Saint-Claud (16450)' => '16308', + 'Saint-Clément (19700)' => '19194', + 'Saint-Clément-des-Baleines (17590)' => '17318', + 'Saint-Colomb-de-Lauzun (47410)' => '47235', + 'Saint-Côme (33430)' => '33391', + 'Saint-Coutant (16350)' => '16310', + 'Saint-Coutant (79120)' => '79243', + 'Saint-Coutant-le-Grand (17430)' => '17320', + 'Saint-Crépin (17380)' => '17321', + 'Saint-Crépin-d\'Auberoche (24330)' => '24390', + 'Saint-Crépin-de-Richemont (24310)' => '24391', + 'Saint-Crépin-et-Carlucet (24590)' => '24392', + 'Saint-Cricq-Chalosse (40700)' => '40253', + 'Saint-Cricq-du-Gave (40300)' => '40254', + 'Saint-Cricq-Villeneuve (40190)' => '40255', + 'Saint-Cybardeaux (16170)' => '16312', + 'Saint-Cybranet (24250)' => '24395', + 'Saint-Cyprien (19130)' => '19195', + 'Saint-Cyprien (24220)' => '24396', + 'Saint-Cyr (86130)' => '86219', + 'Saint-Cyr (87310)' => '87141', + 'Saint-Cyr-du-Doret (17170)' => '17322', + 'Saint-Cyr-la-Lande (79100)' => '79244', + 'Saint-Cyr-la-Roche (19130)' => '19196', + 'Saint-Cyr-les-Champagnes (24270)' => '24397', + 'Saint-Denis-d\'Oléron (17650)' => '17323', + 'Saint-Denis-de-Pile (33910)' => '33393', + 'Saint-Denis-des-Murs (87400)' => '87142', + 'Saint-Dizant-du-Bois (17150)' => '17324', + 'Saint-Dizant-du-Gua (17240)' => '17325', + 'Saint-Dizier-la-Tour (23130)' => '23187', + 'Saint-Dizier-les-Domaines (23270)' => '23188', + 'Saint-Dizier-Leyrenne (23400)' => '23189', + 'Saint-Domet (23190)' => '23190', + 'Saint-Dos (64270)' => '64474', + 'Saint-Éloi (23000)' => '23191', + 'Saint-Éloy-les-Tuileries (19210)' => '19198', + 'Saint-Émilion (33330)' => '33394', + 'Saint-Esteben (64640)' => '64476', + 'Saint-Estèphe (24360)' => '24398', + 'Saint-Estèphe (33180)' => '33395', + 'Saint-Étienne-aux-Clos (19200)' => '19199', + 'Saint-Étienne-d\'Orthe (40300)' => '40256', + 'Saint-Étienne-de-Baïgorry (64430)' => '64477', + 'Saint-Étienne-de-Fougères (47380)' => '47239', + 'Saint-Étienne-de-Fursac (23290)' => '23192', + 'Saint-Étienne-de-Lisse (33330)' => '33396', + 'Saint-Étienne-de-Puycorbier (24400)' => '24399', + 'Saint-Étienne-de-Villeréal (47210)' => '47240', + 'Saint-Étienne-la-Cigogne (79360)' => '79247', + 'Saint-Étienne-la-Geneste (19160)' => '19200', + 'Saint-Eugène (17520)' => '17326', + 'Saint-Eutrope (16190)' => '16314', + 'Saint-Eutrope-de-Born (47210)' => '47241', + 'Saint-Exupéry (33190)' => '33398', + 'Saint-Exupéry-les-Roches (19200)' => '19201', + 'Saint-Faust (64110)' => '64478', + 'Saint-Félix (16480)' => '16315', + 'Saint-Félix (17330)' => '17327', + 'Saint-Félix-de-Bourdeilles (24340)' => '24403', + 'Saint-Félix-de-Foncaude (33540)' => '33399', + 'Saint-Félix-de-Reillac-et-Mortemart (24260)' => '24404', + 'Saint-Félix-de-Villadeix (24510)' => '24405', + 'Saint-Ferme (33580)' => '33400', + 'Saint-Fiel (23000)' => '23195', + 'Saint-Fort-sur-Gironde (17240)' => '17328', + 'Saint-Fort-sur-le-Né (16130)' => '16316', + 'Saint-Fraigne (16140)' => '16317', + 'Saint-Fréjoux (19200)' => '19204', + 'Saint-Frion (23500)' => '23196', + 'Saint-Front (16460)' => '16318', + 'Saint-Front-d\'Alemps (24460)' => '24408', + 'Saint-Front-de-Pradoux (24400)' => '24409', + 'Saint-Front-la-Rivière (24300)' => '24410', + 'Saint-Front-sur-Lémance (47500)' => '47242', + 'Saint-Front-sur-Nizonne (24300)' => '24411', + 'Saint-Froult (17780)' => '17329', + 'Saint-Gaudent (86400)' => '86220', + 'Saint-Gein (40190)' => '40259', + 'Saint-Gelais (79410)' => '79249', + 'Saint-Génard (79500)' => '79251', + 'Saint-Gence (87510)' => '87143', + 'Saint-Généroux (79600)' => '79252', + 'Saint-Genès-de-Blaye (33390)' => '33405', + 'Saint-Genès-de-Castillon (33350)' => '33406', + 'Saint-Genès-de-Fronsac (33240)' => '33407', + 'Saint-Genès-de-Lombaud (33670)' => '33408', + 'Saint-Genest-d\'Ambière (86140)' => '86221', + 'Saint-Genest-sur-Roselle (87260)' => '87144', + 'Saint-Geniès (24590)' => '24412', + 'Saint-Geniez-ô-Merle (19220)' => '19205', + 'Saint-Genis-d\'Hiersac (16570)' => '16320', + 'Saint-Genis-de-Saintonge (17240)' => '17331', + 'Saint-Genis-du-Bois (33760)' => '33409', + 'Saint-Georges (16700)' => '16321', + 'Saint-Georges (47370)' => '47328', + 'Saint-Georges-Antignac (17240)' => '17332', + 'Saint-Georges-Blancaneix (24130)' => '24413', + 'Saint-Georges-d\'Oléron (17190)' => '17337', + 'Saint-Georges-de-Didonne (17110)' => '17333', + 'Saint-Georges-de-Longuepierre (17470)' => '17334', + 'Saint-Georges-de-Montclard (24140)' => '24414', + 'Saint-Georges-de-Noisné (79400)' => '79253', + 'Saint-Georges-de-Rex (79210)' => '79254', + 'Saint-Georges-des-Agoûts (17150)' => '17335', + 'Saint-Georges-des-Coteaux (17810)' => '17336', + 'Saint-Georges-du-Bois (17700)' => '17338', + 'Saint-Georges-la-Pouge (23250)' => '23197', + 'Saint-Georges-lès-Baillargeaux (86130)' => '86222', + 'Saint-Georges-les-Landes (87160)' => '87145', + 'Saint-Georges-Nigremont (23500)' => '23198', + 'Saint-Geours-d\'Auribat (40380)' => '40260', + 'Saint-Geours-de-Maremne (40230)' => '40261', + 'Saint-Géraud (47120)' => '47245', + 'Saint-Géraud-de-Corps (24700)' => '24415', + 'Saint-Germain (86310)' => '86223', + 'Saint-Germain-Beaupré (23160)' => '23199', + 'Saint-Germain-d\'Esteuil (33340)' => '33412', + 'Saint-Germain-de-Belvès (24170)' => '24416', + 'Saint-Germain-de-Grave (33490)' => '33411', + 'Saint-Germain-de-la-Rivière (33240)' => '33414', + 'Saint-Germain-de-Longue-Chaume (79200)' => '79255', + 'Saint-Germain-de-Lusignan (17500)' => '17339', + 'Saint-Germain-de-Marencennes (17700)' => '17340', + 'Saint-Germain-de-Montbron (16380)' => '16323', + 'Saint-Germain-de-Vibrac (17500)' => '17341', + 'Saint-Germain-des-Prés (24160)' => '24417', + 'Saint-Germain-du-Puch (33750)' => '33413', + 'Saint-Germain-du-Salembre (24190)' => '24418', + 'Saint-Germain-du-Seudre (17240)' => '17342', + 'Saint-Germain-et-Mons (24520)' => '24419', + 'Saint-Germain-Lavolps (19290)' => '19206', + 'Saint-Germain-les-Belles (87380)' => '87146', + 'Saint-Germain-les-Vergnes (19330)' => '19207', + 'Saint-Germier (79340)' => '79256', + 'Saint-Gervais (33240)' => '33415', + 'Saint-Gervais-les-Trois-Clochers (86230)' => '86224', + 'Saint-Géry (24400)' => '24420', + 'Saint-Geyrac (24330)' => '24421', + 'Saint-Gilles-les-Forêts (87130)' => '87147', + 'Saint-Girons-d\'Aiguevives (33920)' => '33416', + 'Saint-Girons-en-Béarn (64300)' => '64479', + 'Saint-Gladie-Arrive-Munein (64390)' => '64480', + 'Saint-Goin (64400)' => '64481', + 'Saint-Gor (40120)' => '40262', + 'Saint-Gourson (16700)' => '16325', + 'Saint-Goussaud (23430)' => '23200', + 'Saint-Grégoire-d\'Ardennes (17240)' => '17343', + 'Saint-Groux (16230)' => '16326', + 'Saint-Hilaire-Bonneval (87260)' => '87148', + 'Saint-Hilaire-d\'Estissac (24140)' => '24422', + 'Saint-Hilaire-de-la-Noaille (33190)' => '33418', + 'Saint-Hilaire-de-Lusignan (47450)' => '47246', + 'Saint-Hilaire-de-Villefranche (17770)' => '17344', + 'Saint-Hilaire-du-Bois (17500)' => '17345', + 'Saint-Hilaire-du-Bois (33540)' => '33419', + 'Saint-Hilaire-Foissac (19550)' => '19208', + 'Saint-Hilaire-la-Palud (79210)' => '79257', + 'Saint-Hilaire-la-Plaine (23150)' => '23201', + 'Saint-Hilaire-la-Treille (87190)' => '87149', + 'Saint-Hilaire-le-Château (23250)' => '23202', + 'Saint-Hilaire-les-Courbes (19170)' => '19209', + 'Saint-Hilaire-les-Places (87800)' => '87150', + 'Saint-Hilaire-Luc (19160)' => '19210', + 'Saint-Hilaire-Peyroux (19560)' => '19211', + 'Saint-Hilaire-Taurieux (19400)' => '19212', + 'Saint-Hippolyte (17430)' => '17346', + 'Saint-Hippolyte (33330)' => '33420', + 'Saint-Jacques-de-Thouars (79100)' => '79258', + 'Saint-Jal (19700)' => '19213', + 'Saint-Jammes (64160)' => '64482', + 'Saint-Jean-d\'Angély (17400)' => '17347', + 'Saint-Jean-d\'Angle (17620)' => '17348', + 'Saint-Jean-d\'Ataux (24190)' => '24424', + 'Saint-Jean-d\'Estissac (24140)' => '24426', + 'Saint-Jean-d\'Eyraud (24140)' => '24427', + 'Saint-Jean-d\'Illac (33127)' => '33422', + 'Saint-Jean-de-Blaignac (33420)' => '33421', + 'Saint-Jean-de-Côle (24800)' => '24425', + 'Saint-Jean-de-Duras (47120)' => '47247', + 'Saint-Jean-de-Lier (40380)' => '40263', + 'Saint-Jean-de-Liversay (17170)' => '17349', + 'Saint-Jean-de-Luz (64500)' => '64483', + 'Saint-Jean-de-Marsacq (40230)' => '40264', + 'Saint-Jean-de-Sauves (86330)' => '86225', + 'Saint-Jean-de-Thouars (79100)' => '79259', + 'Saint-Jean-de-Thurac (47270)' => '47248', + 'Saint-Jean-le-Vieux (64220)' => '64484', + 'Saint-Jean-Ligoure (87260)' => '87151', + 'Saint-Jean-Pied-de-Port (64220)' => '64485', + 'Saint-Jean-Poudge (64330)' => '64486', + 'Saint-Jory-de-Chalais (24800)' => '24428', + 'Saint-Jory-las-Bloux (24160)' => '24429', + 'Saint-Jouin-de-Marnes (79600)' => '79260', + 'Saint-Jouin-de-Milly (79380)' => '79261', + 'Saint-Jouvent (87510)' => '87152', + 'Saint-Julien-aux-Bois (19220)' => '19214', + 'Saint-Julien-Beychevelle (33250)' => '33423', + 'Saint-Julien-d\'Armagnac (40240)' => '40265', + 'Saint-Julien-d\'Eymet (24500)' => '24433', + 'Saint-Julien-de-Crempse (24140)' => '24431', + 'Saint-Julien-de-l\'Escap (17400)' => '17350', + 'Saint-Julien-de-Lampon (24370)' => '24432', + 'Saint-Julien-en-Born (40170)' => '40266', + 'Saint-Julien-l\'Ars (86800)' => '86226', + 'Saint-Julien-la-Genête (23110)' => '23203', + 'Saint-Julien-le-Châtel (23130)' => '23204', + 'Saint-Julien-le-Pèlerin (19430)' => '19215', + 'Saint-Julien-le-Petit (87460)' => '87153', + 'Saint-Julien-le-Vendômois (19210)' => '19216', + 'Saint-Julien-Maumont (19500)' => '19217', + 'Saint-Julien-près-Bort (19110)' => '19218', + 'Saint-Junien (87200)' => '87154', + 'Saint-Junien-la-Bregère (23400)' => '23205', + 'Saint-Junien-les-Combes (87300)' => '87155', + 'Saint-Just (24320)' => '24434', + 'Saint-Just-Ibarre (64120)' => '64487', + 'Saint-Just-le-Martel (87590)' => '87156', + 'Saint-Just-Luzac (17320)' => '17351', + 'Saint-Justin (40240)' => '40267', + 'Saint-Laon (86200)' => '86227', + 'Saint-Laurent (23000)' => '23206', + 'Saint-Laurent (47130)' => '47249', + 'Saint-Laurent-Bretagne (64160)' => '64488', + 'Saint-Laurent-d\'Arce (33240)' => '33425', + 'Saint-Laurent-de-Belzagot (16190)' => '16328', + 'Saint-Laurent-de-Céris (16450)' => '16329', + 'Saint-Laurent-de-Cognac (16100)' => '16330', + 'Saint-Laurent-de-Gosse (40390)' => '40268', + 'Saint-Laurent-de-Jourdes (86410)' => '86228', + 'Saint-Laurent-de-la-Barrière (17380)' => '17352', + 'Saint-Laurent-de-la-Prée (17450)' => '17353', + 'Saint-Laurent-des-Combes (16480)' => '16331', + 'Saint-Laurent-des-Combes (33330)' => '33426', + 'Saint-Laurent-des-Hommes (24400)' => '24436', + 'Saint-Laurent-des-Vignes (24100)' => '24437', + 'Saint-Laurent-du-Bois (33540)' => '33427', + 'Saint-Laurent-du-Plan (33190)' => '33428', + 'Saint-Laurent-la-Vallée (24170)' => '24438', + 'Saint-Laurent-les-Églises (87240)' => '87157', + 'Saint-Laurent-Médoc (33112)' => '33424', + 'Saint-Laurent-sur-Gorre (87310)' => '87158', + 'Saint-Laurs (79160)' => '79263', + 'Saint-Léger (16250)' => '16332', + 'Saint-Léger (17800)' => '17354', + 'Saint-Léger (47160)' => '47250', + 'Saint-Léger-Bridereix (23300)' => '23207', + 'Saint-Léger-de-Balson (33113)' => '33429', + 'Saint-Léger-de-la-Martinière (79500)' => '79264', + 'Saint-Léger-de-Montbrillais (86120)' => '86229', + 'Saint-Léger-de-Montbrun (79100)' => '79265', + 'Saint-Léger-la-Montagne (87340)' => '87159', + 'Saint-Léger-le-Guérétois (23000)' => '23208', + 'Saint-Léger-Magnazeix (87190)' => '87160', + 'Saint-Léomer (86290)' => '86230', + 'Saint-Léon (33670)' => '33431', + 'Saint-Léon (47160)' => '47251', + 'Saint-Léon-d\'Issigeac (24560)' => '24441', + 'Saint-Léon-sur-l\'Isle (24110)' => '24442', + 'Saint-Léon-sur-Vézère (24290)' => '24443', + 'Saint-Léonard-de-Noblat (87400)' => '87161', + 'Saint-Lin (79420)' => '79267', + 'Saint-Lon-les-Mines (40300)' => '40269', + 'Saint-Loubert (33210)' => '33432', + 'Saint-Loubès (33450)' => '33433', + 'Saint-Loubouer (40320)' => '40270', + 'Saint-Louis-de-Montferrand (33440)' => '33434', + 'Saint-Louis-en-l\'Isle (24400)' => '24444', + 'Saint-Loup (17380)' => '17356', + 'Saint-Loup (23130)' => '23209', + 'Saint-Loup-Lamairé (79600)' => '79268', + 'Saint-Macaire (33490)' => '33435', + 'Saint-Macoux (86400)' => '86231', + 'Saint-Magne (33125)' => '33436', + 'Saint-Magne-de-Castillon (33350)' => '33437', + 'Saint-Maigrin (17520)' => '17357', + 'Saint-Maime-de-Péreyrol (24380)' => '24459', + 'Saint-Maixant (23200)' => '23210', + 'Saint-Maixant (33490)' => '33438', + 'Saint-Maixent-de-Beugné (79160)' => '79269', + 'Saint-Maixent-l\'École (79400)' => '79270', + 'Saint-Mandé-sur-Brédoire (17470)' => '17358', + 'Saint-Marc-à-Frongier (23200)' => '23211', + 'Saint-Marc-à-Loubaud (23460)' => '23212', + 'Saint-Marc-la-Lande (79310)' => '79271', + 'Saint-Marcel-du-Périgord (24510)' => '24445', + 'Saint-Marcory (24540)' => '24446', + 'Saint-Mard (17700)' => '17359', + 'Saint-Marien (23600)' => '23213', + 'Saint-Mariens (33620)' => '33439', + 'Saint-Martial (16190)' => '16334', + 'Saint-Martial (17330)' => '17361', + 'Saint-Martial (33490)' => '33440', + 'Saint-Martial-d\'Albarède (24160)' => '24448', + 'Saint-Martial-d\'Artenset (24700)' => '24449', + 'Saint-Martial-de-Gimel (19150)' => '19220', + 'Saint-Martial-de-Mirambeau (17150)' => '17362', + 'Saint-Martial-de-Nabirat (24250)' => '24450', + 'Saint-Martial-de-Valette (24300)' => '24451', + 'Saint-Martial-de-Vitaterne (17500)' => '17363', + 'Saint-Martial-Entraygues (19400)' => '19221', + 'Saint-Martial-le-Mont (23150)' => '23214', + 'Saint-Martial-le-Vieux (23100)' => '23215', + 'Saint-Martial-sur-Isop (87330)' => '87163', + 'Saint-Martial-sur-Né (17520)' => '17364', + 'Saint-Martial-Viveyrol (24320)' => '24452', + 'Saint-Martin-Château (23460)' => '23216', + 'Saint-Martin-Curton (47700)' => '47254', + 'Saint-Martin-d\'Arberoue (64640)' => '64489', + 'Saint-Martin-d\'Arrossa (64780)' => '64490', + 'Saint-Martin-d\'Ary (17270)' => '17365', + 'Saint-Martin-d\'Oney (40090)' => '40274', + 'Saint-Martin-de-Beauville (47270)' => '47255', + 'Saint-Martin-de-Bernegoue (79230)' => '79273', + 'Saint-Martin-de-Coux (17360)' => '17366', + 'Saint-Martin-de-Fressengeas (24800)' => '24453', + 'Saint-Martin-de-Gurson (24610)' => '24454', + 'Saint-Martin-de-Hinx (40390)' => '40272', + 'Saint-Martin-de-Juillers (17400)' => '17367', + 'Saint-Martin-de-Jussac (87200)' => '87164', + 'Saint-Martin-de-Laye (33910)' => '33442', + 'Saint-Martin-de-Lerm (33540)' => '33443', + 'Saint-Martin-de-Mâcon (79100)' => '79274', + 'Saint-Martin-de-Ré (17410)' => '17369', + 'Saint-Martin-de-Ribérac (24600)' => '24455', + 'Saint-Martin-de-Saint-Maixent (79400)' => '79276', + 'Saint-Martin-de-Sanzay (79290)' => '79277', + 'Saint-Martin-de-Seignanx (40390)' => '40273', + 'Saint-Martin-de-Sescas (33490)' => '33444', + 'Saint-Martin-de-Villeréal (47210)' => '47256', + 'Saint-Martin-des-Combes (24140)' => '24456', + 'Saint-Martin-du-Bois (33910)' => '33445', + 'Saint-Martin-du-Clocher (16700)' => '16335', + 'Saint-Martin-du-Fouilloux (79420)' => '79278', + 'Saint-Martin-du-Puy (33540)' => '33446', + 'Saint-Martin-l\'Ars (86350)' => '86234', + 'Saint-Martin-l\'Astier (24400)' => '24457', + 'Saint-Martin-la-Méanne (19320)' => '19222', + 'Saint-Martin-Lacaussade (33390)' => '33441', + 'Saint-Martin-le-Mault (87360)' => '87165', + 'Saint-Martin-le-Pin (24300)' => '24458', + 'Saint-Martin-le-Vieux (87700)' => '87166', + 'Saint-Martin-lès-Melle (79500)' => '79279', + 'Saint-Martin-Petit (47180)' => '47257', + 'Saint-Martin-Sainte-Catherine (23430)' => '23217', + 'Saint-Martin-Sepert (19210)' => '19223', + 'Saint-Martin-Terressus (87400)' => '87167', + 'Saint-Mary (16260)' => '16336', + 'Saint-Mathieu (87440)' => '87168', + 'Saint-Maurice-de-Lestapel (47290)' => '47259', + 'Saint-Maurice-des-Lions (16500)' => '16337', + 'Saint-Maurice-la-Clouère (86160)' => '86235', + 'Saint-Maurice-la-Souterraine (23300)' => '23219', + 'Saint-Maurice-les-Brousses (87800)' => '87169', + 'Saint-Maurice-près-Crocq (23260)' => '23218', + 'Saint-Maurice-sur-Adour (40270)' => '40275', + 'Saint-Maurin (47270)' => '47260', + 'Saint-Maxire (79410)' => '79281', + 'Saint-Méard (87130)' => '87170', + 'Saint-Méard-de-Drône (24600)' => '24460', + 'Saint-Méard-de-Gurçon (24610)' => '24461', + 'Saint-Médard (16300)' => '16338', + 'Saint-Médard (17500)' => '17372', + 'Saint-Médard (64370)' => '64491', + 'Saint-Médard (79370)' => '79282', + 'Saint-Médard-d\'Aunis (17220)' => '17373', + 'Saint-Médard-d\'Excideuil (24160)' => '24463', + 'Saint-Médard-d\'Eyrans (33650)' => '33448', + 'Saint-Médard-de-Guizières (33230)' => '33447', + 'Saint-Médard-de-Mussidan (24400)' => '24462', + 'Saint-Médard-en-Jalles (33160)' => '33449', + 'Saint-Médard-la-Rochette (23200)' => '23220', + 'Saint-Même-les-Carrières (16720)' => '16340', + 'Saint-Merd-de-Lapleau (19320)' => '19225', + 'Saint-Merd-la-Breuille (23100)' => '23221', + 'Saint-Merd-les-Oussines (19170)' => '19226', + 'Saint-Mesmin (24270)' => '24464', + 'Saint-Mexant (19330)' => '19227', + 'Saint-Michel (16470)' => '16341', + 'Saint-Michel (64220)' => '64492', + 'Saint-Michel-de-Castelnau (33840)' => '33450', + 'Saint-Michel-de-Double (24400)' => '24465', + 'Saint-Michel-de-Fronsac (33126)' => '33451', + 'Saint-Michel-de-Lapujade (33190)' => '33453', + 'Saint-Michel-de-Montaigne (24230)' => '24466', + 'Saint-Michel-de-Rieufret (33720)' => '33452', + 'Saint-Michel-de-Veisse (23480)' => '23222', + 'Saint-Michel-de-Villadeix (24380)' => '24468', + 'Saint-Michel-Escalus (40550)' => '40276', + 'Saint-Moreil (23400)' => '23223', + 'Saint-Morillon (33650)' => '33454', + 'Saint-Nazaire-sur-Charente (17780)' => '17375', + 'Saint-Nexans (24520)' => '24472', + 'Saint-Nicolas-de-la-Balerme (47220)' => '47262', + 'Saint-Oradoux-de-Chirouze (23100)' => '23224', + 'Saint-Oradoux-près-Crocq (23260)' => '23225', + 'Saint-Ouen-d\'Aunis (17230)' => '17376', + 'Saint-Ouen-la-Thène (17490)' => '17377', + 'Saint-Ouen-sur-Gartempe (87300)' => '87172', + 'Saint-Palais (33820)' => '33456', + 'Saint-Palais (64120)' => '64493', + 'Saint-Palais-de-Négrignac (17210)' => '17378', + 'Saint-Palais-de-Phiolin (17800)' => '17379', + 'Saint-Palais-du-Né (16300)' => '16342', + 'Saint-Palais-sur-Mer (17420)' => '17380', + 'Saint-Pancrace (24530)' => '24474', + 'Saint-Pandelon (40180)' => '40277', + 'Saint-Pantaléon-de-Lapleau (19160)' => '19228', + 'Saint-Pantaléon-de-Larche (19600)' => '19229', + 'Saint-Pantaly-d\'Ans (24640)' => '24475', + 'Saint-Pantaly-d\'Excideuil (24160)' => '24476', + 'Saint-Pardon-de-Conques (33210)' => '33457', + 'Saint-Pardoult (17400)' => '17381', + 'Saint-Pardoux (79310)' => '79285', + 'Saint-Pardoux (87250)' => '87173', + 'Saint-Pardoux-Corbier (19210)' => '19230', + 'Saint-Pardoux-d\'Arnet (23260)' => '23226', + 'Saint-Pardoux-de-Drône (24600)' => '24477', + 'Saint-Pardoux-du-Breuil (47200)' => '47263', + 'Saint-Pardoux-et-Vielvic (24170)' => '24478', + 'Saint-Pardoux-Isaac (47800)' => '47264', + 'Saint-Pardoux-l\'Ortigier (19270)' => '19234', + 'Saint-Pardoux-la-Croisille (19320)' => '19231', + 'Saint-Pardoux-la-Rivière (24470)' => '24479', + 'Saint-Pardoux-le-Neuf (19200)' => '19232', + 'Saint-Pardoux-le-Neuf (23200)' => '23228', + 'Saint-Pardoux-le-Vieux (19200)' => '19233', + 'Saint-Pardoux-les-Cards (23150)' => '23229', + 'Saint-Pardoux-Morterolles (23400)' => '23227', + 'Saint-Pastour (47290)' => '47265', + 'Saint-Paul (19150)' => '19235', + 'Saint-Paul (33390)' => '33458', + 'Saint-Paul (87260)' => '87174', + 'Saint-Paul-de-Serre (24380)' => '24480', + 'Saint-Paul-en-Born (40200)' => '40278', + 'Saint-Paul-en-Gâtine (79240)' => '79286', + 'Saint-Paul-la-Roche (24800)' => '24481', + 'Saint-Paul-lès-Dax (40990)' => '40279', + 'Saint-Paul-Lizonne (24320)' => '24482', + 'Saint-Pé-de-Léren (64270)' => '64494', + 'Saint-Pé-Saint-Simon (47170)' => '47266', + 'Saint-Pée-sur-Nivelle (64310)' => '64495', + 'Saint-Perdon (40090)' => '40280', + 'Saint-Perdoux (24560)' => '24483', + 'Saint-Pey-d\'Armens (33330)' => '33459', + 'Saint-Pey-de-Castets (33350)' => '33460', + 'Saint-Philippe-d\'Aiguille (33350)' => '33461', + 'Saint-Philippe-du-Seignal (33220)' => '33462', + 'Saint-Pierre-Bellevue (23460)' => '23232', + 'Saint-Pierre-Chérignat (23430)' => '23230', + 'Saint-Pierre-d\'Amilly (17700)' => '17382', + 'Saint-Pierre-d\'Aurillac (33490)' => '33463', + 'Saint-Pierre-d\'Exideuil (86400)' => '86237', + 'Saint-Pierre-d\'Eyraud (24130)' => '24487', + 'Saint-Pierre-d\'Irube (64990)' => '64496', + 'Saint-Pierre-d\'Oléron (17310)' => '17385', + 'Saint-Pierre-de-Bat (33760)' => '33464', + 'Saint-Pierre-de-Buzet (47160)' => '47267', + 'Saint-Pierre-de-Chignac (24330)' => '24484', + 'Saint-Pierre-de-Clairac (47270)' => '47269', + 'Saint-Pierre-de-Côle (24800)' => '24485', + 'Saint-Pierre-de-Frugie (24450)' => '24486', + 'Saint-Pierre-de-Fursac (23290)' => '23231', + 'Saint-Pierre-de-Juillers (17400)' => '17383', + 'Saint-Pierre-de-l\'Isle (17330)' => '17384', + 'Saint-Pierre-de-Maillé (86260)' => '86236', + 'Saint-Pierre-de-Mons (33210)' => '33465', + 'Saint-Pierre-des-Échaubrognes (79700)' => '79289', + 'Saint-Pierre-du-Mont (40280)' => '40281', + 'Saint-Pierre-du-Palais (17270)' => '17386', + 'Saint-Pierre-le-Bost (23600)' => '23233', + 'Saint-Pierre-sur-Dropt (47120)' => '47271', + 'Saint-Pompain (79160)' => '79290', + 'Saint-Pompont (24170)' => '24488', + 'Saint-Porchaire (17250)' => '17387', + 'Saint-Preuil (16130)' => '16343', + 'Saint-Priest (23110)' => '23234', + 'Saint-Priest-de-Gimel (19800)' => '19236', + 'Saint-Priest-la-Feuille (23300)' => '23235', + 'Saint-Priest-la-Plaine (23240)' => '23236', + 'Saint-Priest-les-Fougères (24450)' => '24489', + 'Saint-Priest-Ligoure (87800)' => '87176', + 'Saint-Priest-Palus (23400)' => '23237', + 'Saint-Priest-sous-Aixe (87700)' => '87177', + 'Saint-Priest-Taurion (87480)' => '87178', + 'Saint-Privat (19220)' => '19237', + 'Saint-Privat-des-Prés (24410)' => '24490', + 'Saint-Projet-Saint-Constant (16110)' => '16344', + 'Saint-Quantin-de-Rançanne (17800)' => '17388', + 'Saint-Quentin-de-Baron (33750)' => '33466', + 'Saint-Quentin-de-Caplong (33220)' => '33467', + 'Saint-Quentin-de-Chalais (16210)' => '16346', + 'Saint-Quentin-du-Dropt (47330)' => '47272', + 'Saint-Quentin-la-Chabanne (23500)' => '23238', + 'Saint-Quentin-sur-Charente (16150)' => '16345', + 'Saint-Rabier (24210)' => '24491', + 'Saint-Raphaël (24160)' => '24493', + 'Saint-Rémy (19290)' => '19238', + 'Saint-Rémy (24700)' => '24494', + 'Saint-Rémy (79410)' => '79293', + 'Saint-Rémy-sur-Creuse (86220)' => '86241', + 'Saint-Robert (19310)' => '19239', + 'Saint-Robert (47340)' => '47273', + 'Saint-Rogatien (17220)' => '17391', + 'Saint-Romain (16210)' => '16347', + 'Saint-Romain (86250)' => '86242', + 'Saint-Romain-de-Benet (17600)' => '17393', + 'Saint-Romain-de-Monpazier (24540)' => '24495', + 'Saint-Romain-et-Saint-Clément (24800)' => '24496', + 'Saint-Romain-la-Virvée (33240)' => '33470', + 'Saint-Romain-le-Noble (47270)' => '47274', + 'Saint-Romain-sur-Gironde (17240)' => '17392', + 'Saint-Romans-des-Champs (79230)' => '79294', + 'Saint-Romans-lès-Melle (79500)' => '79295', + 'Saint-Salvadour (19700)' => '19240', + 'Saint-Salvy (47360)' => '47275', + 'Saint-Sardos (47360)' => '47276', + 'Saint-Saturnin (16290)' => '16348', + 'Saint-Saturnin-du-Bois (17700)' => '17394', + 'Saint-Saud-Lacoussière (24470)' => '24498', + 'Saint-Sauvant (17610)' => '17395', + 'Saint-Sauvant (86600)' => '86244', + 'Saint-Sauveur (24520)' => '24499', + 'Saint-Sauveur (33250)' => '33471', + 'Saint-Sauveur-d\'Aunis (17540)' => '17396', + 'Saint-Sauveur-de-Meilhan (47180)' => '47277', + 'Saint-Sauveur-de-Puynormand (33660)' => '33472', + 'Saint-Sauveur-Lalande (24700)' => '24500', + 'Saint-Savin (33920)' => '33473', + 'Saint-Savin (86310)' => '86246', + 'Saint-Savinien (17350)' => '17397', + 'Saint-Saviol (86400)' => '86247', + 'Saint-Sébastien (23160)' => '23239', + 'Saint-Secondin (86350)' => '86248', + 'Saint-Selve (33650)' => '33474', + 'Saint-Sernin (47120)' => '47278', + 'Saint-Setiers (19290)' => '19241', + 'Saint-Seurin-de-Bourg (33710)' => '33475', + 'Saint-Seurin-de-Cadourne (33180)' => '33476', + 'Saint-Seurin-de-Cursac (33390)' => '33477', + 'Saint-Seurin-de-Palenne (17800)' => '17398', + 'Saint-Seurin-de-Prats (24230)' => '24501', + 'Saint-Seurin-sur-l\'Isle (33660)' => '33478', + 'Saint-Sève (33190)' => '33479', + 'Saint-Sever (40500)' => '40282', + 'Saint-Sever-de-Saintonge (17800)' => '17400', + 'Saint-Séverin (16390)' => '16350', + 'Saint-Séverin-d\'Estissac (24190)' => '24502', + 'Saint-Séverin-sur-Boutonne (17330)' => '17401', + 'Saint-Sigismond-de-Clermont (17240)' => '17402', + 'Saint-Silvain-Bas-le-Roc (23600)' => '23240', + 'Saint-Silvain-Bellegarde (23190)' => '23241', + 'Saint-Silvain-Montaigut (23320)' => '23242', + 'Saint-Silvain-sous-Toulx (23140)' => '23243', + 'Saint-Simeux (16120)' => '16351', + 'Saint-Simon (16120)' => '16352', + 'Saint-Simon-de-Bordes (17500)' => '17403', + 'Saint-Simon-de-Pellouaille (17260)' => '17404', + 'Saint-Sixte (47220)' => '47279', + 'Saint-Solve (19130)' => '19242', + 'Saint-Sorlin-de-Conac (17150)' => '17405', + 'Saint-Sornin (16220)' => '16353', + 'Saint-Sornin (17600)' => '17406', + 'Saint-Sornin-la-Marche (87210)' => '87179', + 'Saint-Sornin-Lavolps (19230)' => '19243', + 'Saint-Sornin-Leulac (87290)' => '87180', + 'Saint-Sulpice-d\'Arnoult (17250)' => '17408', + 'Saint-Sulpice-d\'Excideuil (24800)' => '24505', + 'Saint-Sulpice-de-Cognac (16370)' => '16355', + 'Saint-Sulpice-de-Faleyrens (33330)' => '33480', + 'Saint-Sulpice-de-Guilleragues (33580)' => '33481', + 'Saint-Sulpice-de-Mareuil (24340)' => '24503', + 'Saint-Sulpice-de-Pommiers (33540)' => '33482', + 'Saint-Sulpice-de-Roumagnac (24600)' => '24504', + 'Saint-Sulpice-de-Royan (17200)' => '17409', + 'Saint-Sulpice-de-Ruffec (16460)' => '16356', + 'Saint-Sulpice-et-Cameyrac (33450)' => '33483', + 'Saint-Sulpice-Laurière (87370)' => '87181', + 'Saint-Sulpice-le-Dunois (23800)' => '23244', + 'Saint-Sulpice-le-Guérétois (23000)' => '23245', + 'Saint-Sulpice-les-Bois (19250)' => '19244', + 'Saint-Sulpice-les-Champs (23480)' => '23246', + 'Saint-Sulpice-les-Feuilles (87160)' => '87182', + 'Saint-Sylvain (19380)' => '19245', + 'Saint-Sylvestre (87240)' => '87183', + 'Saint-Sylvestre-sur-Lot (47140)' => '47280', + 'Saint-Symphorien (33113)' => '33484', + 'Saint-Symphorien (79270)' => '79298', + 'Saint-Symphorien-sur-Couze (87140)' => '87184', + 'Saint-Thomas-de-Conac (17150)' => '17410', + 'Saint-Trojan (33710)' => '33486', + 'Saint-Trojan-les-Bains (17370)' => '17411', + 'Saint-Urcisse (47270)' => '47281', + 'Saint-Vaize (17100)' => '17412', + 'Saint-Vallier (16480)' => '16357', + 'Saint-Varent (79330)' => '79299', + 'Saint-Vaury (23320)' => '23247', + 'Saint-Viance (19240)' => '19246', + 'Saint-Victor (24350)' => '24508', + 'Saint-Victor-en-Marche (23000)' => '23248', + 'Saint-Victour (19200)' => '19247', + 'Saint-Victurnien (87420)' => '87185', + 'Saint-Vincent (64800)' => '64498', + 'Saint-Vincent-de-Connezac (24190)' => '24509', + 'Saint-Vincent-de-Cosse (24220)' => '24510', + 'Saint-Vincent-de-Lamontjoie (47310)' => '47282', + 'Saint-Vincent-de-Paul (33440)' => '33487', + 'Saint-Vincent-de-Paul (40990)' => '40283', + 'Saint-Vincent-de-Pertignas (33420)' => '33488', + 'Saint-Vincent-de-Tyrosse (40230)' => '40284', + 'Saint-Vincent-Jalmoutiers (24410)' => '24511', + 'Saint-Vincent-la-Châtre (79500)' => '79301', + 'Saint-Vincent-le-Paluel (24200)' => '24512', + 'Saint-Vincent-sur-l\'Isle (24420)' => '24513', + 'Saint-Vite (47500)' => '47283', + 'Saint-Vitte-sur-Briance (87380)' => '87186', + 'Saint-Vivien (17220)' => '17413', + 'Saint-Vivien (24230)' => '24514', + 'Saint-Vivien-de-Blaye (33920)' => '33489', + 'Saint-Vivien-de-Médoc (33590)' => '33490', + 'Saint-Vivien-de-Monségur (33580)' => '33491', + 'Saint-Xandre (17138)' => '17414', + 'Saint-Yaguen (40400)' => '40285', + 'Saint-Ybard (19140)' => '19248', + 'Saint-Yrieix-la-Montagne (23460)' => '23249', + 'Saint-Yrieix-la-Perche (87500)' => '87187', + 'Saint-Yrieix-le-Déjalat (19300)' => '19249', + 'Saint-Yrieix-les-Bois (23150)' => '23250', + 'Saint-Yrieix-sous-Aixe (87700)' => '87188', + 'Saint-Yrieix-sur-Charente (16710)' => '16358', + 'Saint-Yzan-de-Soudiac (33920)' => '33492', + 'Saint-Yzans-de-Médoc (33340)' => '33493', + 'Sainte-Alvère-Saint-Laurent Les Bâtons (24510)' => '24362', + 'Sainte-Anne-Saint-Priest (87120)' => '87134', + 'Sainte-Bazeille (47180)' => '47233', + 'Sainte-Blandine (79370)' => '79240', + 'Sainte-Colombe (16230)' => '16309', + 'Sainte-Colombe (17210)' => '17319', + 'Sainte-Colombe (33350)' => '33390', + 'Sainte-Colombe (40700)' => '40252', + 'Sainte-Colombe-de-Duras (47120)' => '47236', + 'Sainte-Colombe-de-Villeneuve (47300)' => '47237', + 'Sainte-Colombe-en-Bruilhois (47310)' => '47238', + 'Sainte-Colome (64260)' => '64473', + 'Sainte-Croix (24440)' => '24393', + 'Sainte-Croix-de-Mareuil (24340)' => '24394', + 'Sainte-Croix-du-Mont (33410)' => '33392', + 'Sainte-Eanne (79800)' => '79246', + 'Sainte-Engrâce (64560)' => '64475', + 'Sainte-Eulalie (33560)' => '33397', + 'Sainte-Eulalie-d\'Ans (24640)' => '24401', + 'Sainte-Eulalie-d\'Eymet (24500)' => '24402', + 'Sainte-Eulalie-en-Born (40200)' => '40257', + 'Sainte-Féréole (19270)' => '19202', + 'Sainte-Feyre (23000)' => '23193', + 'Sainte-Feyre-la-Montagne (23500)' => '23194', + 'Sainte-Florence (33350)' => '33401', + 'Sainte-Fortunade (19490)' => '19203', + 'Sainte-Foy (40190)' => '40258', + 'Sainte-Foy-de-Belvès (24170)' => '24406', + 'Sainte-Foy-de-Longas (24510)' => '24407', + 'Sainte-Foy-la-Grande (33220)' => '33402', + 'Sainte-Foy-la-Longue (33490)' => '33403', + 'Sainte-Gemme (17250)' => '17330', + 'Sainte-Gemme (33580)' => '33404', + 'Sainte-Gemme (79330)' => '79250', + 'Sainte-Gemme-Martaillac (47250)' => '47244', + 'Sainte-Hélène (33480)' => '33417', + 'Sainte-Innocence (24500)' => '24423', + 'Sainte-Lheurine (17520)' => '17355', + 'Sainte-Livrade-sur-Lot (47110)' => '47252', + 'Sainte-Marie-de-Chignac (24330)' => '24447', + 'Sainte-Marie-de-Gosse (40390)' => '40271', + 'Sainte-Marie-de-Ré (17740)' => '17360', + 'Sainte-Marie-de-Vaux (87420)' => '87162', + 'Sainte-Marie-Lapanouze (19160)' => '19219', + 'Sainte-Marthe (47430)' => '47253', + 'Sainte-Maure-de-Peyriac (47170)' => '47258', + 'Sainte-Même (17770)' => '17374', + 'Sainte-Mondane (24370)' => '24470', + 'Sainte-Nathalène (24200)' => '24471', + 'Sainte-Néomaye (79260)' => '79283', + 'Sainte-Orse (24210)' => '24473', + 'Sainte-Ouenne (79220)' => '79284', + 'Sainte-Radegonde (17250)' => '17389', + 'Sainte-Radegonde (24560)' => '24492', + 'Sainte-Radegonde (33350)' => '33468', + 'Sainte-Radegonde (79100)' => '79292', + 'Sainte-Radégonde (86300)' => '86239', + 'Sainte-Ramée (17240)' => '17390', + 'Sainte-Sévère (16200)' => '16349', + 'Sainte-Soline (79120)' => '79297', + 'Sainte-Souline (16480)' => '16354', + 'Sainte-Soulle (17220)' => '17407', + 'Sainte-Terre (33350)' => '33485', + 'Sainte-Trie (24160)' => '24507', + 'Sainte-Verge (79100)' => '79300', + 'Saintes (17100)' => '17415', + 'Saires (86420)' => '86249', + 'Saivres (79400)' => '79302', + 'Saix (86120)' => '86250', + 'Salagnac (24160)' => '24515', + 'Salaunes (33160)' => '33494', + 'Saleignes (17510)' => '17416', + 'Salies-de-Béarn (64270)' => '64499', + 'Salignac-de-Mirambeau (17130)' => '17417', + 'Salignac-Eyvigues (24590)' => '24516', + 'Salignac-sur-Charente (17800)' => '17418', + 'Salleboeuf (33370)' => '33496', + 'Salles (33770)' => '33498', + 'Salles (47150)' => '47284', + 'Salles (79800)' => '79303', + 'Salles-d\'Angles (16130)' => '16359', + 'Salles-de-Barbezieux (16300)' => '16360', + 'Salles-de-Belvès (24170)' => '24517', + 'Salles-de-Villefagnan (16700)' => '16361', + 'Salles-Lavalette (16190)' => '16362', + 'Salles-Mongiscard (64300)' => '64500', + 'Salles-sur-Mer (17220)' => '17420', + 'Sallespisse (64300)' => '64501', + 'Salon (24380)' => '24518', + 'Salon-la-Tour (19510)' => '19250', + 'Samadet (40320)' => '40286', + 'Samazan (47250)' => '47285', + 'Sames (64520)' => '64502', + 'Sammarçolles (86200)' => '86252', + 'Samonac (33710)' => '33500', + 'Samsons-Lion (64350)' => '64503', + 'Sanguinet (40460)' => '40287', + 'Sannat (23110)' => '23167', + 'Sansais (79270)' => '79304', + 'Sanxay (86600)' => '86253', + 'Sarbazan (40120)' => '40288', + 'Sardent (23250)' => '23168', + 'Sare (64310)' => '64504', + 'Sarlande (24270)' => '24519', + 'Sarlat-la-Canéda (24200)' => '24520', + 'Sarliac-sur-l\'Isle (24420)' => '24521', + 'Sarpourenx (64300)' => '64505', + 'Sarran (19800)' => '19251', + 'Sarrance (64490)' => '64506', + 'Sarrazac (24800)' => '24522', + 'Sarraziet (40500)' => '40289', + 'Sarron (40800)' => '40290', + 'Sarroux (19110)' => '19252', + 'Saubion (40230)' => '40291', + 'Saubole (64420)' => '64507', + 'Saubrigues (40230)' => '40292', + 'Saubusse (40180)' => '40293', + 'Saucats (33650)' => '33501', + 'Saucède (64400)' => '64508', + 'Saugnac-et-Cambran (40180)' => '40294', + 'Saugnacq-et-Muret (40410)' => '40295', + 'Saugon (33920)' => '33502', + 'Sauguis-Saint-Étienne (64470)' => '64509', + 'Saujon (17600)' => '17421', + 'Saulgé (86500)' => '86254', + 'Saulgond (16420)' => '16363', + 'Sault-de-Navailles (64300)' => '64510', + 'Sauméjan (47420)' => '47286', + 'Saumont (47600)' => '47287', + 'Saumos (33680)' => '33503', + 'Saurais (79200)' => '79306', + 'Saussignac (24240)' => '24523', + 'Sauternes (33210)' => '33504', + 'Sauvagnac (16310)' => '16364', + 'Sauvagnas (47340)' => '47288', + 'Sauvagnon (64230)' => '64511', + 'Sauvelade (64150)' => '64512', + 'Sauveterre-de-Béarn (64390)' => '64513', + 'Sauveterre-de-Guyenne (33540)' => '33506', + 'Sauveterre-la-Lémance (47500)' => '47292', + 'Sauveterre-Saint-Denis (47220)' => '47293', + 'Sauviac (33430)' => '33507', + 'Sauviat-sur-Vige (87400)' => '87190', + 'Sauvignac (16480)' => '16365', + 'Sauzé-Vaussais (79190)' => '79307', + 'Savennes (23000)' => '23170', + 'Savignac (33124)' => '33508', + 'Savignac-de-Duras (47120)' => '47294', + 'Savignac-de-l\'Isle (33910)' => '33509', + 'Savignac-de-Miremont (24260)' => '24524', + 'Savignac-de-Nontron (24300)' => '24525', + 'Savignac-Lédrier (24270)' => '24526', + 'Savignac-les-Églises (24420)' => '24527', + 'Savignac-sur-Leyze (47150)' => '47295', + 'Savigné (86400)' => '86255', + 'Savigny-Lévescault (86800)' => '86256', + 'Savigny-sous-Faye (86140)' => '86257', + 'Sceau-Saint-Angel (24300)' => '24528', + 'Sciecq (79000)' => '79308', + 'Scillé (79240)' => '79309', + 'Scorbé-Clairvaux (86140)' => '86258', + 'Séby (64410)' => '64514', + 'Secondigné-sur-Belle (79170)' => '79310', + 'Secondigny (79130)' => '79311', + 'Sedze-Maubecq (64160)' => '64515', + 'Sedzère (64160)' => '64516', + 'Ségalas (47410)' => '47296', + 'Segonzac (16130)' => '16366', + 'Segonzac (19310)' => '19253', + 'Segonzac (24600)' => '24529', + 'Ségur-le-Château (19230)' => '19254', + 'Seigné (17510)' => '17422', + 'Seignosse (40510)' => '40296', + 'Seilhac (19700)' => '19255', + 'Séligné (79170)' => '79312', + 'Sembas (47360)' => '47297', + 'Séméacq-Blachon (64350)' => '64517', + 'Semens (33490)' => '33510', + 'Semillac (17150)' => '17423', + 'Semoussac (17150)' => '17424', + 'Semussac (17120)' => '17425', + 'Sencenac-Puy-de-Fourches (24310)' => '24530', + 'Sendets (33690)' => '33511', + 'Sendets (64320)' => '64518', + 'Sénestis (47430)' => '47298', + 'Senillé-Saint-Sauveur (86100)' => '86245', + 'Sepvret (79120)' => '79313', + 'Sérandon (19160)' => '19256', + 'Séreilhac (87620)' => '87191', + 'Sergeac (24290)' => '24531', + 'Sérignac-Péboudou (47410)' => '47299', + 'Sérignac-sur-Garonne (47310)' => '47300', + 'Sérigny (86230)' => '86260', + 'Sérilhac (19190)' => '19257', + 'Sermur (23700)' => '23171', + 'Séron (65320)' => '65422', + 'Serres-Castet (64121)' => '64519', + 'Serres-et-Montguyard (24500)' => '24532', + 'Serres-Gaston (40700)' => '40298', + 'Serres-Morlaàs (64160)' => '64520', + 'Serres-Sainte-Marie (64170)' => '64521', + 'Serreslous-et-Arribans (40700)' => '40299', + 'Sers (16410)' => '16368', + 'Servanches (24410)' => '24533', + 'Servières-le-Château (19220)' => '19258', + 'Sévignacq (64160)' => '64523', + 'Sévignacq-Meyracq (64260)' => '64522', + 'Sèvres-Anxaumont (86800)' => '86261', + 'Sexcles (19430)' => '19259', + 'Seyches (47350)' => '47301', + 'Seyresse (40180)' => '40300', + 'Siecq (17490)' => '17427', + 'Siest (40180)' => '40301', + 'Sigalens (33690)' => '33512', + 'Sigogne (16200)' => '16369', + 'Sigoulès (24240)' => '24534', + 'Sillars (86320)' => '86262', + 'Sillas (33690)' => '33513', + 'Simacourbe (64350)' => '64524', + 'Simeyrols (24370)' => '24535', + 'Sindères (40110)' => '40302', + 'Singleyrac (24500)' => '24536', + 'Sioniac (19120)' => '19260', + 'Siorac-de-Ribérac (24600)' => '24537', + 'Siorac-en-Périgord (24170)' => '24538', + 'Sireuil (16440)' => '16370', + 'Siros (64230)' => '64525', + 'Smarves (86240)' => '86263', + 'Solférino (40210)' => '40303', + 'Solignac (87110)' => '87192', + 'Sommières-du-Clain (86160)' => '86264', + 'Sompt (79110)' => '79314', + 'Sonnac (17160)' => '17428', + 'Soorts-Hossegor (40150)' => '40304', + 'Sorbets (40320)' => '40305', + 'Sorde-l\'Abbaye (40300)' => '40306', + 'Sore (40430)' => '40307', + 'Sorges et Ligueux en Périgord (24420)' => '24540', + 'Sornac (19290)' => '19261', + 'Sort-en-Chalosse (40180)' => '40308', + 'Sos (47170)' => '47302', + 'Sossais (86230)' => '86265', + 'Soubise (17780)' => '17429', + 'Soubran (17150)' => '17430', + 'Soubrebost (23250)' => '23173', + 'Soudaine-Lavinadière (19370)' => '19262', + 'Soudan (79800)' => '79316', + 'Soudat (24360)' => '24541', + 'Soudeilles (19300)' => '19263', + 'Souffrignac (16380)' => '16372', + 'Soulac-sur-Mer (33780)' => '33514', + 'Soulaures (24540)' => '24542', + 'Soulignac (33760)' => '33515', + 'Soulignonne (17250)' => '17431', + 'Soumans (23600)' => '23174', + 'Soumensac (47120)' => '47303', + 'Souméras (17130)' => '17432', + 'Soumoulou (64420)' => '64526', + 'Souprosse (40250)' => '40309', + 'Souraïde (64250)' => '64527', + 'Soursac (19550)' => '19264', + 'Sourzac (24400)' => '24543', + 'Sous-Parsat (23150)' => '23175', + 'Sousmoulins (17130)' => '17433', + 'Soussac (33790)' => '33516', + 'Soussans (33460)' => '33517', + 'Soustons (40140)' => '40310', + 'Soutiers (79310)' => '79318', + 'Souvigné (16240)' => '16373', + 'Souvigné (79800)' => '79319', + 'Soyaux (16800)' => '16374', + 'Suaux (16260)' => '16375', + 'Suhescun (64780)' => '64528', + 'Surdoux (87130)' => '87193', + 'Surgères (17700)' => '17434', + 'Surin (79220)' => '79320', + 'Surin (86250)' => '86266', + 'Suris (16270)' => '16376', + 'Sus (64190)' => '64529', + 'Susmiou (64190)' => '64530', + 'Sussac (87130)' => '87194', + 'Tabaille-Usquain (64190)' => '64531', + 'Tabanac (33550)' => '33518', + 'Tadousse-Ussau (64330)' => '64532', + 'Taillant (17350)' => '17435', + 'Taillebourg (17350)' => '17436', + 'Taillebourg (47200)' => '47304', + 'Taillecavat (33580)' => '33520', + 'Taizé (79100)' => '79321', + 'Taizé-Aizie (16700)' => '16378', + 'Talais (33590)' => '33521', + 'Talence (33400)' => '33522', + 'Taller (40260)' => '40311', + 'Talmont-sur-Gironde (17120)' => '17437', + 'Tamniès (24620)' => '24544', + 'Tanzac (17260)' => '17438', + 'Taponnat-Fleurignac (16110)' => '16379', + 'Tardes (23170)' => '23251', + 'Tardets-Sorholus (64470)' => '64533', + 'Targon (33760)' => '33523', + 'Tarnac (19170)' => '19265', + 'Tarnès (33240)' => '33524', + 'Tarnos (40220)' => '40312', + 'Taron-Sadirac-Viellenave (64330)' => '64534', + 'Tarsacq (64360)' => '64535', + 'Tartas (40400)' => '40313', + 'Taugon (17170)' => '17439', + 'Tauriac (33710)' => '33525', + 'Tayac (33570)' => '33526', + 'Tayrac (47270)' => '47305', + 'Teillots (24390)' => '24545', + 'Temple-Laguyon (24390)' => '24546', + 'Tercé (86800)' => '86268', + 'Tercillat (23350)' => '23252', + 'Tercis-les-Bains (40180)' => '40314', + 'Ternant (17400)' => '17440', + 'Ternay (86120)' => '86269', + 'Terrasson-Lavilledieu (24120)' => '24547', + 'Tersannes (87360)' => '87195', + 'Tesson (17460)' => '17441', + 'Tessonnière (79600)' => '79325', + 'Téthieu (40990)' => '40315', + 'Teuillac (33710)' => '33530', + 'Teyjat (24300)' => '24548', + 'Thaims (17120)' => '17442', + 'Thairé (17290)' => '17443', + 'Thalamy (19200)' => '19266', + 'Thauron (23250)' => '23253', + 'Theil-Rabier (16240)' => '16381', + 'Thénac (17460)' => '17444', + 'Thénac (24240)' => '24549', + 'Thénezay (79390)' => '79326', + 'Thenon (24210)' => '24550', + 'Thézac (17600)' => '17445', + 'Thézac (47370)' => '47307', + 'Thèze (64450)' => '64536', + 'Thiat (87320)' => '87196', + 'Thiviers (24800)' => '24551', + 'Thollet (86290)' => '86270', + 'Thonac (24290)' => '24552', + 'Thorigné (79370)' => '79327', + 'Thorigny-sur-le-Mignon (79360)' => '79328', + 'Thors (17160)' => '17446', + 'Thouars (79100)' => '79329', + 'Thouars-sur-Garonne (47230)' => '47308', + 'Thouron (87140)' => '87197', + 'Thurageau (86110)' => '86271', + 'Thuré (86540)' => '86272', + 'Tilh (40360)' => '40316', + 'Tillou (79110)' => '79330', + 'Tizac-de-Curton (33420)' => '33531', + 'Tizac-de-Lapouyade (33620)' => '33532', + 'Tocane-Saint-Apre (24350)' => '24553', + 'Tombeboeuf (47380)' => '47309', + 'Tonnay-Boutonne (17380)' => '17448', + 'Tonnay-Charente (17430)' => '17449', + 'Tonneins (47400)' => '47310', + 'Torsac (16410)' => '16382', + 'Torxé (17380)' => '17450', + 'Tosse (40230)' => '40317', + 'Toulenne (33210)' => '33533', + 'Toulouzette (40250)' => '40318', + 'Toulx-Sainte-Croix (23600)' => '23254', + 'Tourliac (47210)' => '47311', + 'Tournon-d\'Agenais (47370)' => '47312', + 'Tourriers (16560)' => '16383', + 'Tourtenay (79100)' => '79331', + 'Tourtoirac (24390)' => '24555', + 'Tourtrès (47380)' => '47313', + 'Touvérac (16360)' => '16384', + 'Touvre (16600)' => '16385', + 'Touzac (16120)' => '16386', + 'Toy-Viam (19170)' => '19268', + 'Trayes (79240)' => '79332', + 'Treignac (19260)' => '19269', + 'Trélissac (24750)' => '24557', + 'Trémolat (24510)' => '24558', + 'Trémons (47140)' => '47314', + 'Trensacq (40630)' => '40319', + 'Trentels (47140)' => '47315', + 'Tresses (33370)' => '33535', + 'Triac-Lautrait (16200)' => '16387', + 'Trizay (17250)' => '17453', + 'Troche (19230)' => '19270', + 'Trois-Fonds (23230)' => '23255', + 'Trois-Palis (16730)' => '16388', + 'Trois-Villes (64470)' => '64537', + 'Tudeils (19120)' => '19271', + 'Tugéras-Saint-Maurice (17130)' => '17454', + 'Tulle (19000)' => '19272', + 'Turenne (19500)' => '19273', + 'Turgon (16350)' => '16389', + 'Tursac (24620)' => '24559', + 'Tusson (16140)' => '16390', + 'Tuzie (16700)' => '16391', + 'Uchacq-et-Parentis (40090)' => '40320', + 'Uhart-Cize (64220)' => '64538', + 'Uhart-Mixe (64120)' => '64539', + 'Urcuit (64990)' => '64540', + 'Urdès (64370)' => '64541', + 'Urdos (64490)' => '64542', + 'Urepel (64430)' => '64543', + 'Urgons (40320)' => '40321', + 'Urost (64160)' => '64544', + 'Urrugne (64122)' => '64545', + 'Urt (64240)' => '64546', + 'Urval (24480)' => '24560', + 'Ussac (19270)' => '19274', + 'Usseau (79210)' => '79334', + 'Usseau (86230)' => '86275', + 'Ussel (19200)' => '19275', + 'Usson-du-Poitou (86350)' => '86276', + 'Ustaritz (64480)' => '64547', + 'Uza (40170)' => '40322', + 'Uzan (64370)' => '64548', + 'Uzein (64230)' => '64549', + 'Uzerche (19140)' => '19276', + 'Uzeste (33730)' => '33537', + 'Uzos (64110)' => '64550', + 'Val d\'Issoire (87330)' => '87097', + 'Val de Virvée (33240)' => '33018', + 'Val des Vignes (16250)' => '16175', + 'Valdivienne (86300)' => '86233', + 'Valence (16460)' => '16392', + 'Valeuil (24310)' => '24561', + 'Valeyrac (33340)' => '33538', + 'Valiergues (19200)' => '19277', + 'Vallans (79270)' => '79335', + 'Vallereuil (24190)' => '24562', + 'Vallière (23120)' => '23257', + 'Valojoulx (24290)' => '24563', + 'Vançais (79120)' => '79336', + 'Vandré (17700)' => '17457', + 'Vanxains (24600)' => '24564', + 'Vanzac (17500)' => '17458', + 'Vanzay (79120)' => '79338', + 'Varaignes (24360)' => '24565', + 'Varaize (17400)' => '17459', + 'Vareilles (23300)' => '23258', + 'Varennes (24150)' => '24566', + 'Varennes (86110)' => '86277', + 'Varès (47400)' => '47316', + 'Varetz (19240)' => '19278', + 'Vars (16330)' => '16393', + 'Vars-sur-Roseix (19130)' => '19279', + 'Varzay (17460)' => '17460', + 'Vasles (79340)' => '79339', + 'Vaulry (87140)' => '87198', + 'Vaunac (24800)' => '24567', + 'Vausseroux (79420)' => '79340', + 'Vautebis (79420)' => '79341', + 'Vaux (86700)' => '86278', + 'Vaux-Lavalette (16320)' => '16394', + 'Vaux-Rouillac (16170)' => '16395', + 'Vaux-sur-Mer (17640)' => '17461', + 'Vaux-sur-Vienne (86220)' => '86279', + 'Vayres (33870)' => '33539', + 'Vayres (87600)' => '87199', + 'Végennes (19120)' => '19280', + 'Veix (19260)' => '19281', + 'Vélines (24230)' => '24568', + 'Vellèches (86230)' => '86280', + 'Vendays-Montalivet (33930)' => '33540', + 'Vendeuvre-du-Poitou (86380)' => '86281', + 'Vendoire (24320)' => '24569', + 'Vénérand (17100)' => '17462', + 'Vensac (33590)' => '33541', + 'Ventouse (16460)' => '16396', + 'Vérac (33240)' => '33542', + 'Verdelais (33490)' => '33543', + 'Verdets (64400)' => '64551', + 'Verdille (16140)' => '16397', + 'Verdon (24520)' => '24570', + 'Vergeroux (17300)' => '17463', + 'Vergné (17330)' => '17464', + 'Vergt (24380)' => '24571', + 'Vergt-de-Biron (24540)' => '24572', + 'Vérines (17540)' => '17466', + 'Verneiges (23170)' => '23259', + 'Verneuil (16310)' => '16398', + 'Verneuil-Moustiers (87360)' => '87200', + 'Verneuil-sur-Vienne (87430)' => '87201', + 'Vernon (86340)' => '86284', + 'Vernoux-en-Gâtine (79240)' => '79342', + 'Vernoux-sur-Boutonne (79170)' => '79343', + 'Verrières (16130)' => '16399', + 'Verrières (86410)' => '86285', + 'Verrue (86420)' => '86286', + 'Verruyes (79310)' => '79345', + 'Vert (40420)' => '40323', + 'Verteillac (24320)' => '24573', + 'Verteuil-d\'Agenais (47260)' => '47317', + 'Verteuil-sur-Charente (16510)' => '16400', + 'Vertheuil (33180)' => '33545', + 'Vervant (16330)' => '16401', + 'Vervant (17400)' => '17467', + 'Veyrac (87520)' => '87202', + 'Veyrières (19200)' => '19283', + 'Veyrignac (24370)' => '24574', + 'Veyrines-de-Domme (24250)' => '24575', + 'Veyrines-de-Vergt (24380)' => '24576', + 'Vézac (24220)' => '24577', + 'Vézières (86120)' => '86287', + 'Vialer (64330)' => '64552', + 'Viam (19170)' => '19284', + 'Vianne (47230)' => '47318', + 'Vibrac (16120)' => '16402', + 'Vibrac (17130)' => '17468', + 'Vicq-d\'Auribat (40380)' => '40324', + 'Vicq-sur-Breuilh (87260)' => '87203', + 'Vicq-sur-Gartempe (86260)' => '86288', + 'Vidaillat (23250)' => '23260', + 'Videix (87600)' => '87204', + 'Vielle-Saint-Girons (40560)' => '40326', + 'Vielle-Soubiran (40240)' => '40327', + 'Vielle-Tursan (40320)' => '40325', + 'Viellenave-d\'Arthez (64170)' => '64554', + 'Viellenave-de-Navarrenx (64190)' => '64555', + 'Vielleségure (64150)' => '64556', + 'Viennay (79200)' => '79347', + 'Viersat (23170)' => '23261', + 'Vieux-Boucau-les-Bains (40480)' => '40328', + 'Vieux-Mareuil (24340)' => '24579', + 'Vieux-Ruffec (16350)' => '16404', + 'Vigeois (19410)' => '19285', + 'Vigeville (23140)' => '23262', + 'Vignes (64410)' => '64557', + 'Vignolles (16300)' => '16405', + 'Vignols (19130)' => '19286', + 'Vignonet (33330)' => '33546', + 'Vilhonneur (16220)' => '16406', + 'Villac (24120)' => '24580', + 'Villamblard (24140)' => '24581', + 'Villandraut (33730)' => '33547', + 'Villard (23800)' => '23263', + 'Villars (24530)' => '24582', + 'Villars-en-Pons (17260)' => '17469', + 'Villars-les-Bois (17770)' => '17470', + 'Villebois-Lavalette (16320)' => '16408', + 'Villebramar (47380)' => '47319', + 'Villedoux (17230)' => '17472', + 'Villefagnan (16240)' => '16409', + 'Villefavard (87190)' => '87206', + 'Villefollet (79170)' => '79348', + 'Villefranche-de-Lonchat (24610)' => '24584', + 'Villefranche-du-Périgord (24550)' => '24585', + 'Villefranche-du-Queyran (47160)' => '47320', + 'Villefranque (64990)' => '64558', + 'Villegats (16700)' => '16410', + 'Villegouge (33141)' => '33548', + 'Villejésus (16140)' => '16411', + 'Villejoubert (16560)' => '16412', + 'Villemain (79110)' => '79349', + 'Villemorin (17470)' => '17473', + 'Villemort (86310)' => '86291', + 'Villenave (40110)' => '40330', + 'Villenave-d\'Ornon (33140)' => '33550', + 'Villenave-de-Rions (33550)' => '33549', + 'Villenave-près-Béarn (65500)' => '65476', + 'Villeneuve (33710)' => '33551', + 'Villeneuve-de-Duras (47120)' => '47321', + 'Villeneuve-de-Marsan (40190)' => '40331', + 'Villeneuve-la-Comtesse (17330)' => '17474', + 'Villeneuve-sur-Lot (47300)' => '47323', + 'Villeréal (47210)' => '47324', + 'Villeton (47400)' => '47325', + 'Villetoureix (24600)' => '24586', + 'Villexavier (17500)' => '17476', + 'Villiers (86190)' => '86292', + 'Villiers-Couture (17510)' => '17477', + 'Villiers-en-Bois (79360)' => '79350', + 'Villiers-en-Plaine (79160)' => '79351', + 'Villiers-le-Roux (16240)' => '16413', + 'Villiers-sur-Chizé (79170)' => '79352', + 'Villognon (16230)' => '16414', + 'Vinax (17510)' => '17478', + 'Vindelle (16430)' => '16415', + 'Viodos-Abense-de-Bas (64130)' => '64559', + 'Virazeil (47200)' => '47326', + 'Virelade (33720)' => '33552', + 'Virollet (17260)' => '17479', + 'Virsac (33240)' => '33553', + 'Virson (17290)' => '17480', + 'Vitrac (24200)' => '24587', + 'Vitrac-Saint-Vincent (16310)' => '16416', + 'Vitrac-sur-Montane (19800)' => '19287', + 'Viven (64450)' => '64560', + 'Viville (16120)' => '16417', + 'Vivonne (86370)' => '86293', + 'Voeuil-et-Giget (16400)' => '16418', + 'Voissay (17400)' => '17481', + 'Vouharte (16330)' => '16419', + 'Vouhé (17700)' => '17482', + 'Vouhé (79310)' => '79354', + 'Vouillé (79230)' => '79355', + 'Vouillé (86190)' => '86294', + 'Voulême (86400)' => '86295', + 'Voulgézac (16250)' => '16420', + 'Voulmentin (79150)' => '79242', + 'Voulon (86700)' => '86296', + 'Vouneuil-sous-Biard (86580)' => '86297', + 'Vouneuil-sur-Vienne (86210)' => '86298', + 'Voutezac (19130)' => '19288', + 'Vouthon (16220)' => '16421', + 'Vouzailles (86170)' => '86299', + 'Vouzan (16410)' => '16422', + 'Xaintrailles (47230)' => '47327', + 'Xaintray (79220)' => '79357', + 'Xambes (16330)' => '16423', + 'Ychoux (40160)' => '40332', + 'Ygos-Saint-Saturnin (40110)' => '40333', + 'Yssandon (19310)' => '19289', + 'Yversay (86170)' => '86300', + 'Yves (17340)' => '17483', + 'Yviers (16210)' => '16424', + 'Yvrac (33370)' => '33554', + 'Yvrac-et-Malleyrand (16110)' => '16425', + 'Yzosse (40180)' => '40334' + ]; } diff --git a/bridges/AtmoOccitanieBridge.php b/bridges/AtmoOccitanieBridge.php index a934bad8..2388d7e4 100644 --- a/bridges/AtmoOccitanieBridge.php +++ b/bridges/AtmoOccitanieBridge.php @@ -1,58 +1,60 @@ <?php -class AtmoOccitanieBridge extends BridgeAbstract { - - const NAME = 'Atmo Occitanie'; - const URI = 'https://www.atmo-occitanie.org/'; - const DESCRIPTION = 'Fetches the latest air polution of cities in Occitanie from Atmo'; - const MAINTAINER = 'floviolleau'; - const PARAMETERS = array(array( - 'city' => array( - 'name' => 'Ville', - 'required' => true, - 'exampleValue' => 'cahors' - ) - )); - const CACHE_TIMEOUT = 7200; - - public function collectData() { - $uri = self::URI . $this->getInput('city'); - - $html = getSimpleHTMLDOM($uri); - - $generalMessage = $html->find('.landing-ville .city-banner .iqa-avertissement', 0)->innertext; - $recommendationsDom = $html->find('.landing-ville .recommandations', 0); - $recommendationsItemDom = $recommendationsDom->find('.recommandation-item .label'); - - $recommendationsMessage = ''; - - $i = 0; - $len = count($recommendationsItemDom); - foreach ($recommendationsItemDom as $key => $value) { - if ($i == 0) { - $recommendationsMessage .= trim($value->innertext) . '.'; - } else { - $recommendationsMessage .= ' ' . trim($value->innertext) . '.'; - } - $i++; - } - - $lastRecommendationsDom = $recommendationsDom->find('.col-md-6', -1); - $informationHeaderMessage = $lastRecommendationsDom->find('.heading', 0)->innertext; - $indice = $lastRecommendationsDom->find('.current-indice .indice div', 0)->innertext; - $informationDescriptionMessage = $lastRecommendationsDom->find('.current-indice .description p', 0)->innertext; - - $message = "$generalMessage L'indice est de $indice/10. $informationDescriptionMessage. $recommendationsMessage"; - $city = $this->getInput('city'); - - $item['uri'] = $uri; - $today = date('d/m/Y'); - $item['title'] = "Bulletin de l'air du $today pour la ville : $city."; - //$item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-occitanie.org #QualiteAir. ' . $message; - $item['title'] .= ' #QualiteAir. ' . $message; - $item['author'] = 'floviolleau'; - $item['content'] = $message; - $item['uid'] = hash('sha256', $item['title']); - - $this->items[] = $item; - } + +class AtmoOccitanieBridge extends BridgeAbstract +{ + const NAME = 'Atmo Occitanie'; + const URI = 'https://www.atmo-occitanie.org/'; + const DESCRIPTION = 'Fetches the latest air polution of cities in Occitanie from Atmo'; + const MAINTAINER = 'floviolleau'; + const PARAMETERS = [[ + 'city' => [ + 'name' => 'Ville', + 'required' => true, + 'exampleValue' => 'cahors' + ] + ]]; + const CACHE_TIMEOUT = 7200; + + public function collectData() + { + $uri = self::URI . $this->getInput('city'); + + $html = getSimpleHTMLDOM($uri); + + $generalMessage = $html->find('.landing-ville .city-banner .iqa-avertissement', 0)->innertext; + $recommendationsDom = $html->find('.landing-ville .recommandations', 0); + $recommendationsItemDom = $recommendationsDom->find('.recommandation-item .label'); + + $recommendationsMessage = ''; + + $i = 0; + $len = count($recommendationsItemDom); + foreach ($recommendationsItemDom as $key => $value) { + if ($i == 0) { + $recommendationsMessage .= trim($value->innertext) . '.'; + } else { + $recommendationsMessage .= ' ' . trim($value->innertext) . '.'; + } + $i++; + } + + $lastRecommendationsDom = $recommendationsDom->find('.col-md-6', -1); + $informationHeaderMessage = $lastRecommendationsDom->find('.heading', 0)->innertext; + $indice = $lastRecommendationsDom->find('.current-indice .indice div', 0)->innertext; + $informationDescriptionMessage = $lastRecommendationsDom->find('.current-indice .description p', 0)->innertext; + + $message = "$generalMessage L'indice est de $indice/10. $informationDescriptionMessage. $recommendationsMessage"; + $city = $this->getInput('city'); + + $item['uri'] = $uri; + $today = date('d/m/Y'); + $item['title'] = "Bulletin de l'air du $today pour la ville : $city."; + //$item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-occitanie.org #QualiteAir. ' . $message; + $item['title'] .= ' #QualiteAir. ' . $message; + $item['author'] = 'floviolleau'; + $item['content'] = $message; + $item['uid'] = hash('sha256', $item['title']); + + $this->items[] = $item; + } } diff --git a/bridges/AutoJMBridge.php b/bridges/AutoJMBridge.php index 9a68dbdd..c9aaa660 100644 --- a/bridges/AutoJMBridge.php +++ b/bridges/AutoJMBridge.php @@ -1,135 +1,135 @@ <?php -class AutoJMBridge extends BridgeAbstract { - - const NAME = 'AutoJM'; - const URI = 'https://www.autojm.fr/'; - const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages'; - const MAINTAINER = 'sysadminstory'; - const PARAMETERS = array( - 'Afficher les offres de véhicules disponible sur la recheche AutoJM' => array( - 'url' => array( - 'name' => 'URL de la page de recherche', - 'type' => 'text', - 'required' => true, - 'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/', - 'exampleValue' => 'recherche?brands[]=peugeot&ranges[]=peugeot-nouvelle-308-2021-5p' - ), - ) - ); - const CACHE_TIMEOUT = 3600; - - public function getIcon() { - return self::URI . 'favicon.ico'; - } - - public function getName() { - switch($this->queriedContext) { - case 'Afficher les offres de véhicules disponible sur la recheche AutoJM': - return 'AutoJM | Recherche de véhicules'; - break; - default: - return parent::getName(); - } - - } - - public function collectData() { - - // Get the number of result for this search - $search_url = self::URI . $this->getInput('url') . '&open=energy&onlyFilters=false'; - - // Set the header 'X-Requested-With' like the website does it - $header = array( - 'X-Requested-With: XMLHttpRequest' - ); - - // Get the JSON content of the form - $json = getContents($search_url, $header); - - // Extract the HTML content from the JSON result - $data = json_decode($json); - - $nb_results = $data->nbResults; - $total_pages = ceil($nb_results / 15); - - // Limit the number of page to analyse to 10 - for($page = 1; $page <= $total_pages && $page <= 10; $page++) { - // Get the result the next page - $html = $this->getResults($page); - - // Go through every car of the search - $list = $html->find('div[class*=card-car card-car--listing]'); - foreach ($list as $car) { - - // Get the info about the car offer - $image = $car->find('div[class=card-car__header__img]', 0)->find('img', 0)->src; - // Decode HTML attribute JSON data - $car_data = json_decode(html_entity_decode($car->{'data-layer'})); - $car_model = $car->{'data-title'} . ' ' . $car->{'data-suptitle'}; - $availability = $car->find('div[class=card-car__modalites]', 0)->find('div[class=col]', 0)->plaintext; - $warranty = $car->find('div[data-type=WarrantyCard]', 0)->plaintext; - $discount_html = $car->find('div[class=subtext vehicle_reference_element]', 0); - // Check if there is any discount info displayed - if ($discount_html != null) { - $reference_price_value = $discount_html->find('span[data-cfg=vehicle__reference_price]', 0)->plaintext; - $discount_percent_value = $discount_html->find('span[data-cfg=vehicle__discount_percent]', 0)->plaintext; - $reference_price = '<li>Prix de référence : <s>' . $reference_price_value . '</s></li>'; - $discount_percent = '<li>Réduction : ' . $discount_percent_value . ' %</li>'; - } else { - $reference_price = ''; - $discount_percent = ''; - } - $price = $car_data->price; - $kilometer = $car->find('span[data-cfg=vehicle__kilometer]', 0)->plaintext; - $energy = $car->find('span[data-cfg=vehicle__energy__label]', 0)->plaintext; - $power = $car->find('span[data-cfg=vehicle__tax_horse_power]', 0)->plaintext; - $seats = $car->find('span[data-cfg=vehicle__seats]', 0)->plaintext; - $doors = $car->find('span[data-cfg=vehicle__door__label]', 0)->plaintext; - $transmission = $car->find('span[data-cfg=vehicle__transmission]', 0)->plaintext; - $loa_html = $car->find('span[data-cfg=vehicle__loa]', 0); - // Check if any LOA price is displayed - if($loa_html != null) { - $loa_value = $car->find('span[data-cfg=vehicle__loa]', 0)->plaintext; - $loa = '<li>LOA : à partir de ' . $loa_value . ' / mois </li>'; - } else { - $loa = ''; - } - - // Construct the new item - $item = array(); - $item['title'] = $car_model; - $item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' - . $car_model . '</p>'; - $item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>'; - $item['content'] .= '<li>Prix : ' . $price . ' €</li>'; - $item['content'] .= $reference_price; - $item['content'] .= $loa; - $item['content'] .= $discount_percent; - $item['content'] .= '<li>Garantie : ' . $warranty . '</li>'; - $item['content'] .= '<li>Kilométrage : ' . $kilometer . ' km</li>'; - $item['content'] .= '<li>Energie : ' . $energy . '</li>'; - $item['content'] .= '<li>Puissance: ' . $power . ' CV Fiscaux</li>'; - $item['content'] .= '<li>Nombre de Places : ' . $seats . ' place(s)</li>'; - $item['content'] .= '<li>Nombre de portes : ' . $doors . '</li>'; - $item['content'] .= '<li>Boite de vitesse : ' . $transmission . '</li></ul>'; - $item['uri'] = $car_data->{'uri'}; - $item['uid'] = hash('md5', $item['content']); - $this->items[] = $item; - } - } - } - - private function getResults(int $page) - { - $user_input = $this->getInput('url'); - $search_data = preg_replace('#(recherche|recherche/[0-9]{1,10})\?#', 'recherche/' . $page . '?', $user_input); - - $search_url = self::URI . $search_data . '&open=energy&onlyFilters=false'; - - // Get the HTML content of the page - $html = getSimpleHTMLDOMCached($search_url); - - return $html; - } +class AutoJMBridge extends BridgeAbstract +{ + const NAME = 'AutoJM'; + const URI = 'https://www.autojm.fr/'; + const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages'; + const MAINTAINER = 'sysadminstory'; + const PARAMETERS = [ + 'Afficher les offres de véhicules disponible sur la recheche AutoJM' => [ + 'url' => [ + 'name' => 'URL de la page de recherche', + 'type' => 'text', + 'required' => true, + 'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/', + 'exampleValue' => 'recherche?brands[]=peugeot&ranges[]=peugeot-nouvelle-308-2021-5p' + ], + ] + ]; + const CACHE_TIMEOUT = 3600; + + public function getIcon() + { + return self::URI . 'favicon.ico'; + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Afficher les offres de véhicules disponible sur la recheche AutoJM': + return 'AutoJM | Recherche de véhicules'; + break; + default: + return parent::getName(); + } + } + + public function collectData() + { + // Get the number of result for this search + $search_url = self::URI . $this->getInput('url') . '&open=energy&onlyFilters=false'; + + // Set the header 'X-Requested-With' like the website does it + $header = [ + 'X-Requested-With: XMLHttpRequest' + ]; + + // Get the JSON content of the form + $json = getContents($search_url, $header); + + // Extract the HTML content from the JSON result + $data = json_decode($json); + + $nb_results = $data->nbResults; + $total_pages = ceil($nb_results / 15); + + // Limit the number of page to analyse to 10 + for ($page = 1; $page <= $total_pages && $page <= 10; $page++) { + // Get the result the next page + $html = $this->getResults($page); + + // Go through every car of the search + $list = $html->find('div[class*=card-car card-car--listing]'); + foreach ($list as $car) { + // Get the info about the car offer + $image = $car->find('div[class=card-car__header__img]', 0)->find('img', 0)->src; + // Decode HTML attribute JSON data + $car_data = json_decode(html_entity_decode($car->{'data-layer'})); + $car_model = $car->{'data-title'} . ' ' . $car->{'data-suptitle'}; + $availability = $car->find('div[class=card-car__modalites]', 0)->find('div[class=col]', 0)->plaintext; + $warranty = $car->find('div[data-type=WarrantyCard]', 0)->plaintext; + $discount_html = $car->find('div[class=subtext vehicle_reference_element]', 0); + // Check if there is any discount info displayed + if ($discount_html != null) { + $reference_price_value = $discount_html->find('span[data-cfg=vehicle__reference_price]', 0)->plaintext; + $discount_percent_value = $discount_html->find('span[data-cfg=vehicle__discount_percent]', 0)->plaintext; + $reference_price = '<li>Prix de référence : <s>' . $reference_price_value . '</s></li>'; + $discount_percent = '<li>Réduction : ' . $discount_percent_value . ' %</li>'; + } else { + $reference_price = ''; + $discount_percent = ''; + } + $price = $car_data->price; + $kilometer = $car->find('span[data-cfg=vehicle__kilometer]', 0)->plaintext; + $energy = $car->find('span[data-cfg=vehicle__energy__label]', 0)->plaintext; + $power = $car->find('span[data-cfg=vehicle__tax_horse_power]', 0)->plaintext; + $seats = $car->find('span[data-cfg=vehicle__seats]', 0)->plaintext; + $doors = $car->find('span[data-cfg=vehicle__door__label]', 0)->plaintext; + $transmission = $car->find('span[data-cfg=vehicle__transmission]', 0)->plaintext; + $loa_html = $car->find('span[data-cfg=vehicle__loa]', 0); + // Check if any LOA price is displayed + if ($loa_html != null) { + $loa_value = $car->find('span[data-cfg=vehicle__loa]', 0)->plaintext; + $loa = '<li>LOA : à partir de ' . $loa_value . ' / mois </li>'; + } else { + $loa = ''; + } + + // Construct the new item + $item = []; + $item['title'] = $car_model; + $item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' + . $car_model . '</p>'; + $item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>'; + $item['content'] .= '<li>Prix : ' . $price . ' €</li>'; + $item['content'] .= $reference_price; + $item['content'] .= $loa; + $item['content'] .= $discount_percent; + $item['content'] .= '<li>Garantie : ' . $warranty . '</li>'; + $item['content'] .= '<li>Kilométrage : ' . $kilometer . ' km</li>'; + $item['content'] .= '<li>Energie : ' . $energy . '</li>'; + $item['content'] .= '<li>Puissance: ' . $power . ' CV Fiscaux</li>'; + $item['content'] .= '<li>Nombre de Places : ' . $seats . ' place(s)</li>'; + $item['content'] .= '<li>Nombre de portes : ' . $doors . '</li>'; + $item['content'] .= '<li>Boite de vitesse : ' . $transmission . '</li></ul>'; + $item['uri'] = $car_data->{'uri'}; + $item['uid'] = hash('md5', $item['content']); + $this->items[] = $item; + } + } + } + + private function getResults(int $page) + { + $user_input = $this->getInput('url'); + $search_data = preg_replace('#(recherche|recherche/[0-9]{1,10})\?#', 'recherche/' . $page . '?', $user_input); + + $search_url = self::URI . $search_data . '&open=energy&onlyFilters=false'; + + // Get the HTML content of the page + $html = getSimpleHTMLDOMCached($search_url); + + return $html; + } } diff --git a/bridges/AwwwardsBridge.php b/bridges/AwwwardsBridge.php index ad03e607..1596bec6 100644 --- a/bridges/AwwwardsBridge.php +++ b/bridges/AwwwardsBridge.php @@ -1,54 +1,63 @@ <?php -class AwwwardsBridge extends BridgeAbstract { - const NAME = 'Awwwards'; - const URI = 'https://www.awwwards.com/'; - const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards'; - const MAINTAINER = 'Paroleen'; - const CACHE_TIMEOUT = 3600; - - const SITESURI = 'https://www.awwwards.com/websites/sites_of_the_day/'; - const SITEURI = 'https://www.awwwards.com/sites/'; - const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/'; - - private $sites = array(); - - public function getIcon() { - return 'https://www.awwwards.com/favicon.ico'; - } - - private function fetchSites() { - Debug::log('Fetching all sites'); - $sites = getSimpleHTMLDOM(self::SITESURI); - - Debug::log('Parsing all JSON data'); - foreach($sites->find('li[data-model]') as $site) { - $decode = html_entity_decode($site->attr['data-model'], - ENT_QUOTES, 'utf-8'); - $decode = json_decode($decode, true); - $this->sites[] = $decode; - } - } - - public function collectData() { - $this->fetchSites(); - - Debug::log('Building RSS feed'); - foreach($this->sites as $site) { - $item = array(); - $item['title'] = $site['title']; - $item['timestamp'] = $site['createdAt']; - $item['categories'] = $site['tags']; - - $item['content'] = '<img src="' - . self::ASSETSURI - . $site['images']['thumbnail'] - . '">'; - $item['uri'] = self::SITEURI . $site['slug']; - - $this->items[] = $item; - - if(count($this->items) >= 10) - break; - } - } + +class AwwwardsBridge extends BridgeAbstract +{ + const NAME = 'Awwwards'; + const URI = 'https://www.awwwards.com/'; + const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards'; + const MAINTAINER = 'Paroleen'; + const CACHE_TIMEOUT = 3600; + + const SITESURI = 'https://www.awwwards.com/websites/sites_of_the_day/'; + const SITEURI = 'https://www.awwwards.com/sites/'; + const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/'; + + private $sites = []; + + public function getIcon() + { + return 'https://www.awwwards.com/favicon.ico'; + } + + private function fetchSites() + { + Debug::log('Fetching all sites'); + $sites = getSimpleHTMLDOM(self::SITESURI); + + Debug::log('Parsing all JSON data'); + foreach ($sites->find('li[data-model]') as $site) { + $decode = html_entity_decode( + $site->attr['data-model'], + ENT_QUOTES, + 'utf-8' + ); + $decode = json_decode($decode, true); + $this->sites[] = $decode; + } + } + + public function collectData() + { + $this->fetchSites(); + + Debug::log('Building RSS feed'); + foreach ($this->sites as $site) { + $item = []; + $item['title'] = $site['title']; + $item['timestamp'] = $site['createdAt']; + $item['categories'] = $site['tags']; + + $item['content'] = '<img src="' + . self::ASSETSURI + . $site['images']['thumbnail'] + . '">'; + $item['uri'] = self::SITEURI . $site['slug']; + + $this->items[] = $item; + + if (count($this->items) >= 10) { + break; + } + } + } } diff --git a/bridges/BAEBridge.php b/bridges/BAEBridge.php index 80c08362..6807d548 100644 --- a/bridges/BAEBridge.php +++ b/bridges/BAEBridge.php @@ -1,263 +1,269 @@ <?php -class BAEBridge extends BridgeAbstract { - const MAINTAINER = 'couraudt'; - const NAME = 'Bourse Aux Equipiers Bridge'; - const URI = 'https://www.bourse-aux-equipiers.com'; - const DESCRIPTION = 'Returns the newest sailing offers.'; - const PARAMETERS = array( - array( - 'keyword' => array( - 'name' => 'Filtrer par mots clés', - 'title' => 'Entrez le mot clé à filtrer ici' - ), - 'type' => array( - 'name' => 'Type de recherche', - 'title' => 'Afficher seuleument un certain type d\'annonce', - 'type' => 'list', - 'values' => array( - 'Toutes les annonces' => false, - 'Les embarquements' => 'boat', - 'Les skippers' => 'skipper', - 'Les équipiers' => 'crew' - ) - ) - ) - ); - public function collectData() { - $url = $this->getURI(); - $html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.'); +class BAEBridge extends BridgeAbstract +{ + const MAINTAINER = 'couraudt'; + const NAME = 'Bourse Aux Equipiers Bridge'; + const URI = 'https://www.bourse-aux-equipiers.com'; + const DESCRIPTION = 'Returns the newest sailing offers.'; + const PARAMETERS = [ + [ + 'keyword' => [ + 'name' => 'Filtrer par mots clés', + 'title' => 'Entrez le mot clé à filtrer ici' + ], + 'type' => [ + 'name' => 'Type de recherche', + 'title' => 'Afficher seuleument un certain type d\'annonce', + 'type' => 'list', + 'values' => [ + 'Toutes les annonces' => false, + 'Les embarquements' => 'boat', + 'Les skippers' => 'skipper', + 'Les équipiers' => 'crew' + ] + ] + ] + ]; - $annonces = $html->find('main article'); - foreach ($annonces as $annonce) { - $detail = $annonce->find('footer a', 0); + public function collectData() + { + $url = $this->getURI(); + $html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.'); - $htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href); - if (!$htmlDetail) - continue; + $annonces = $html->find('main article'); + foreach ($annonces as $annonce) { + $detail = $annonce->find('footer a', 0); - $item = array(); + $htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href); + if (!$htmlDetail) { + continue; + } - $item['title'] = $annonce->find('header h2', 0)->plaintext; - $item['uri'] = parent::getURI() . $detail->href; + $item = []; - $content = $htmlDetail->find('article p', 0)->innertext; - if (!empty($this->getInput('keyword'))) { - $keyword = $this->removeAccents(strtolower($this->getInput('keyword'))); - $cleanTitle = $this->removeAccents(strtolower($item['title'])); - if (strpos($cleanTitle, $keyword) === false) { - $cleanContent = $this->removeAccents(strtolower($content)); - if (strpos($cleanContent, $keyword) === false) { - continue; - } - } - } + $item['title'] = $annonce->find('header h2', 0)->plaintext; + $item['uri'] = parent::getURI() . $detail->href; - $content .= '<hr>'; - $content .= $htmlDetail->find('section', 0)->innertext; - $item['content'] = defaultLinkTo($content, parent::getURI()); - $image = $htmlDetail->find('#zoom', 0); - if ($image) { - $item['enclosures'] = array(parent::getURI() . $image->getAttribute('src')); - } - $this->items[] = $item; - } - } + $content = $htmlDetail->find('article p', 0)->innertext; + if (!empty($this->getInput('keyword'))) { + $keyword = $this->removeAccents(strtolower($this->getInput('keyword'))); + $cleanTitle = $this->removeAccents(strtolower($item['title'])); + if (strpos($cleanTitle, $keyword) === false) { + $cleanContent = $this->removeAccents(strtolower($content)); + if (strpos($cleanContent, $keyword) === false) { + continue; + } + } + } - public function getURI() { - $uri = parent::getURI(); - if (!empty($this->getInput('type'))) { - if ($this->getInput('type') == 'boat') { - $uri .= '/embarquements.html'; - } elseif ($this->getInput('type') == 'skipper') { - $uri .= '/skippers.html'; - } else { - $uri .= '/equipiers.html'; - } - } + $content .= '<hr>'; + $content .= $htmlDetail->find('section', 0)->innertext; + $item['content'] = defaultLinkTo($content, parent::getURI()); + $image = $htmlDetail->find('#zoom', 0); + if ($image) { + $item['enclosures'] = [parent::getURI() . $image->getAttribute('src')]; + } + $this->items[] = $item; + } + } - return $uri; - } + public function getURI() + { + $uri = parent::getURI(); + if (!empty($this->getInput('type'))) { + if ($this->getInput('type') == 'boat') { + $uri .= '/embarquements.html'; + } elseif ($this->getInput('type') == 'skipper') { + $uri .= '/skippers.html'; + } else { + $uri .= '/equipiers.html'; + } + } - private function removeAccents($string) { - $chars = array( - // Decompositions for Latin-1 Supplement - 'ª' => 'a', 'º' => 'o', - 'À' => 'A', 'Á' => 'A', - 'Â' => 'A', 'Ã' => 'A', - 'Ä' => 'A', 'Å' => 'A', - 'Æ' => 'AE', 'Ç' => 'C', - 'È' => 'E', 'É' => 'E', - 'Ê' => 'E', 'Ë' => 'E', - 'Ì' => 'I', 'Í' => 'I', - 'Î' => 'I', 'Ï' => 'I', - 'Ð' => 'D', 'Ñ' => 'N', - 'Ò' => 'O', 'Ó' => 'O', - 'Ô' => 'O', 'Õ' => 'O', - 'Ö' => 'O', 'Ù' => 'U', - 'Ú' => 'U', 'Û' => 'U', - 'Ü' => 'U', 'Ý' => 'Y', - 'Þ' => 'TH', 'ß' => 's', - 'à' => 'a', 'á' => 'a', - 'â' => 'a', 'ã' => 'a', - 'ä' => 'a', 'å' => 'a', - 'æ' => 'ae', 'ç' => 'c', - 'è' => 'e', 'é' => 'e', - 'ê' => 'e', 'ë' => 'e', - 'ì' => 'i', 'í' => 'i', - 'î' => 'i', 'ï' => 'i', - 'ð' => 'd', 'ñ' => 'n', - 'ò' => 'o', 'ó' => 'o', - 'ô' => 'o', 'õ' => 'o', - 'ö' => 'o', 'ø' => 'o', - 'ù' => 'u', 'ú' => 'u', - 'û' => 'u', 'ü' => 'u', - 'ý' => 'y', 'þ' => 'th', - 'ÿ' => 'y', 'Ø' => 'O', - // Decompositions for Latin Extended-A - 'Ā' => 'A', 'ā' => 'a', - 'Ă' => 'A', 'ă' => 'a', - 'Ą' => 'A', 'ą' => 'a', - 'Ć' => 'C', 'ć' => 'c', - 'Ĉ' => 'C', 'ĉ' => 'c', - 'Ċ' => 'C', 'ċ' => 'c', - 'Č' => 'C', 'č' => 'c', - 'Ď' => 'D', 'ď' => 'd', - 'Đ' => 'D', 'đ' => 'd', - 'Ē' => 'E', 'ē' => 'e', - 'Ĕ' => 'E', 'ĕ' => 'e', - 'Ė' => 'E', 'ė' => 'e', - 'Ę' => 'E', 'ę' => 'e', - 'Ě' => 'E', 'ě' => 'e', - 'Ĝ' => 'G', 'ĝ' => 'g', - 'Ğ' => 'G', 'ğ' => 'g', - 'Ġ' => 'G', 'ġ' => 'g', - 'Ģ' => 'G', 'ģ' => 'g', - 'Ĥ' => 'H', 'ĥ' => 'h', - 'Ħ' => 'H', 'ħ' => 'h', - 'Ĩ' => 'I', 'ĩ' => 'i', - 'Ī' => 'I', 'ī' => 'i', - 'Ĭ' => 'I', 'ĭ' => 'i', - 'Į' => 'I', 'į' => 'i', - 'İ' => 'I', 'ı' => 'i', - 'IJ' => 'IJ', 'ij' => 'ij', - 'Ĵ' => 'J', 'ĵ' => 'j', - 'Ķ' => 'K', 'ķ' => 'k', - 'ĸ' => 'k', 'Ĺ' => 'L', - 'ĺ' => 'l', 'Ļ' => 'L', - 'ļ' => 'l', 'Ľ' => 'L', - 'ľ' => 'l', 'Ŀ' => 'L', - 'ŀ' => 'l', 'Ł' => 'L', - 'ł' => 'l', 'Ń' => 'N', - 'ń' => 'n', 'Ņ' => 'N', - 'ņ' => 'n', 'Ň' => 'N', - 'ň' => 'n', 'ʼn' => 'n', - 'Ŋ' => 'N', 'ŋ' => 'n', - 'Ō' => 'O', 'ō' => 'o', - 'Ŏ' => 'O', 'ŏ' => 'o', - 'Ő' => 'O', 'ő' => 'o', - 'Œ' => 'OE', 'œ' => 'oe', - 'Ŕ' => 'R', 'ŕ' => 'r', - 'Ŗ' => 'R', 'ŗ' => 'r', - 'Ř' => 'R', 'ř' => 'r', - 'Ś' => 'S', 'ś' => 's', - 'Ŝ' => 'S', 'ŝ' => 's', - 'Ş' => 'S', 'ş' => 's', - 'Š' => 'S', 'š' => 's', - 'Ţ' => 'T', 'ţ' => 't', - 'Ť' => 'T', 'ť' => 't', - 'Ŧ' => 'T', 'ŧ' => 't', - 'Ũ' => 'U', 'ũ' => 'u', - 'Ū' => 'U', 'ū' => 'u', - 'Ŭ' => 'U', 'ŭ' => 'u', - 'Ů' => 'U', 'ů' => 'u', - 'Ű' => 'U', 'ű' => 'u', - 'Ų' => 'U', 'ų' => 'u', - 'Ŵ' => 'W', 'ŵ' => 'w', - 'Ŷ' => 'Y', 'ŷ' => 'y', - 'Ÿ' => 'Y', 'Ź' => 'Z', - 'ź' => 'z', 'Ż' => 'Z', - 'ż' => 'z', 'Ž' => 'Z', - 'ž' => 'z', 'ſ' => 's', - // Decompositions for Latin Extended-B - 'Ș' => 'S', 'ș' => 's', - 'Ț' => 'T', 'ț' => 't', - // Euro Sign - '€' => 'E', - // GBP (Pound) Sign - '£' => '', - // Vowels with diacritic (Vietnamese) - // unmarked - 'Ơ' => 'O', 'ơ' => 'o', - 'Ư' => 'U', 'ư' => 'u', - // grave accent - 'Ầ' => 'A', 'ầ' => 'a', - 'Ằ' => 'A', 'ằ' => 'a', - 'Ề' => 'E', 'ề' => 'e', - 'Ồ' => 'O', 'ồ' => 'o', - 'Ờ' => 'O', 'ờ' => 'o', - 'Ừ' => 'U', 'ừ' => 'u', - 'Ỳ' => 'Y', 'ỳ' => 'y', - // hook - 'Ả' => 'A', 'ả' => 'a', - 'Ẩ' => 'A', 'ẩ' => 'a', - 'Ẳ' => 'A', 'ẳ' => 'a', - 'Ẻ' => 'E', 'ẻ' => 'e', - 'Ể' => 'E', 'ể' => 'e', - 'Ỉ' => 'I', 'ỉ' => 'i', - 'Ỏ' => 'O', 'ỏ' => 'o', - 'Ổ' => 'O', 'ổ' => 'o', - 'Ở' => 'O', 'ở' => 'o', - 'Ủ' => 'U', 'ủ' => 'u', - 'Ử' => 'U', 'ử' => 'u', - 'Ỷ' => 'Y', 'ỷ' => 'y', - // tilde - 'Ẫ' => 'A', 'ẫ' => 'a', - 'Ẵ' => 'A', 'ẵ' => 'a', - 'Ẽ' => 'E', 'ẽ' => 'e', - 'Ễ' => 'E', 'ễ' => 'e', - 'Ỗ' => 'O', 'ỗ' => 'o', - 'Ỡ' => 'O', 'ỡ' => 'o', - 'Ữ' => 'U', 'ữ' => 'u', - 'Ỹ' => 'Y', 'ỹ' => 'y', - // acute accent - 'Ấ' => 'A', 'ấ' => 'a', - 'Ắ' => 'A', 'ắ' => 'a', - 'Ế' => 'E', 'ế' => 'e', - 'Ố' => 'O', 'ố' => 'o', - 'Ớ' => 'O', 'ớ' => 'o', - 'Ứ' => 'U', 'ứ' => 'u', - // dot below - 'Ạ' => 'A', 'ạ' => 'a', - 'Ậ' => 'A', 'ậ' => 'a', - 'Ặ' => 'A', 'ặ' => 'a', - 'Ẹ' => 'E', 'ẹ' => 'e', - 'Ệ' => 'E', 'ệ' => 'e', - 'Ị' => 'I', 'ị' => 'i', - 'Ọ' => 'O', 'ọ' => 'o', - 'Ộ' => 'O', 'ộ' => 'o', - 'Ợ' => 'O', 'ợ' => 'o', - 'Ụ' => 'U', 'ụ' => 'u', - 'Ự' => 'U', 'ự' => 'u', - 'Ỵ' => 'Y', 'ỵ' => 'y', - // Vowels with diacritic (Chinese, Hanyu Pinyin) - 'ɑ' => 'a', - // macron - 'Ǖ' => 'U', 'ǖ' => 'u', - // acute accent - 'Ǘ' => 'U', 'ǘ' => 'u', - // caron - 'Ǎ' => 'A', 'ǎ' => 'a', - 'Ǐ' => 'I', 'ǐ' => 'i', - 'Ǒ' => 'O', 'ǒ' => 'o', - 'Ǔ' => 'U', 'ǔ' => 'u', - 'Ǚ' => 'U', 'ǚ' => 'u', - // grave accent - 'Ǜ' => 'U', 'ǜ' => 'u', - ); + return $uri; + } - $string = strtr($string, $chars); + private function removeAccents($string) + { + $chars = [ + // Decompositions for Latin-1 Supplement + 'ª' => 'a', 'º' => 'o', + 'À' => 'A', 'Á' => 'A', + 'Â' => 'A', 'Ã' => 'A', + 'Ä' => 'A', 'Å' => 'A', + 'Æ' => 'AE', 'Ç' => 'C', + 'È' => 'E', 'É' => 'E', + 'Ê' => 'E', 'Ë' => 'E', + 'Ì' => 'I', 'Í' => 'I', + 'Î' => 'I', 'Ï' => 'I', + 'Ð' => 'D', 'Ñ' => 'N', + 'Ò' => 'O', 'Ó' => 'O', + 'Ô' => 'O', 'Õ' => 'O', + 'Ö' => 'O', 'Ù' => 'U', + 'Ú' => 'U', 'Û' => 'U', + 'Ü' => 'U', 'Ý' => 'Y', + 'Þ' => 'TH', 'ß' => 's', + 'à' => 'a', 'á' => 'a', + 'â' => 'a', 'ã' => 'a', + 'ä' => 'a', 'å' => 'a', + 'æ' => 'ae', 'ç' => 'c', + 'è' => 'e', 'é' => 'e', + 'ê' => 'e', 'ë' => 'e', + 'ì' => 'i', 'í' => 'i', + 'î' => 'i', 'ï' => 'i', + 'ð' => 'd', 'ñ' => 'n', + 'ò' => 'o', 'ó' => 'o', + 'ô' => 'o', 'õ' => 'o', + 'ö' => 'o', 'ø' => 'o', + 'ù' => 'u', 'ú' => 'u', + 'û' => 'u', 'ü' => 'u', + 'ý' => 'y', 'þ' => 'th', + 'ÿ' => 'y', 'Ø' => 'O', + // Decompositions for Latin Extended-A + 'Ā' => 'A', 'ā' => 'a', + 'Ă' => 'A', 'ă' => 'a', + 'Ą' => 'A', 'ą' => 'a', + 'Ć' => 'C', 'ć' => 'c', + 'Ĉ' => 'C', 'ĉ' => 'c', + 'Ċ' => 'C', 'ċ' => 'c', + 'Č' => 'C', 'č' => 'c', + 'Ď' => 'D', 'ď' => 'd', + 'Đ' => 'D', 'đ' => 'd', + 'Ē' => 'E', 'ē' => 'e', + 'Ĕ' => 'E', 'ĕ' => 'e', + 'Ė' => 'E', 'ė' => 'e', + 'Ę' => 'E', 'ę' => 'e', + 'Ě' => 'E', 'ě' => 'e', + 'Ĝ' => 'G', 'ĝ' => 'g', + 'Ğ' => 'G', 'ğ' => 'g', + 'Ġ' => 'G', 'ġ' => 'g', + 'Ģ' => 'G', 'ģ' => 'g', + 'Ĥ' => 'H', 'ĥ' => 'h', + 'Ħ' => 'H', 'ħ' => 'h', + 'Ĩ' => 'I', 'ĩ' => 'i', + 'Ī' => 'I', 'ī' => 'i', + 'Ĭ' => 'I', 'ĭ' => 'i', + 'Į' => 'I', 'į' => 'i', + 'İ' => 'I', 'ı' => 'i', + 'IJ' => 'IJ', 'ij' => 'ij', + 'Ĵ' => 'J', 'ĵ' => 'j', + 'Ķ' => 'K', 'ķ' => 'k', + 'ĸ' => 'k', 'Ĺ' => 'L', + 'ĺ' => 'l', 'Ļ' => 'L', + 'ļ' => 'l', 'Ľ' => 'L', + 'ľ' => 'l', 'Ŀ' => 'L', + 'ŀ' => 'l', 'Ł' => 'L', + 'ł' => 'l', 'Ń' => 'N', + 'ń' => 'n', 'Ņ' => 'N', + 'ņ' => 'n', 'Ň' => 'N', + 'ň' => 'n', 'ʼn' => 'n', + 'Ŋ' => 'N', 'ŋ' => 'n', + 'Ō' => 'O', 'ō' => 'o', + 'Ŏ' => 'O', 'ŏ' => 'o', + 'Ő' => 'O', 'ő' => 'o', + 'Œ' => 'OE', 'œ' => 'oe', + 'Ŕ' => 'R', 'ŕ' => 'r', + 'Ŗ' => 'R', 'ŗ' => 'r', + 'Ř' => 'R', 'ř' => 'r', + 'Ś' => 'S', 'ś' => 's', + 'Ŝ' => 'S', 'ŝ' => 's', + 'Ş' => 'S', 'ş' => 's', + 'Š' => 'S', 'š' => 's', + 'Ţ' => 'T', 'ţ' => 't', + 'Ť' => 'T', 'ť' => 't', + 'Ŧ' => 'T', 'ŧ' => 't', + 'Ũ' => 'U', 'ũ' => 'u', + 'Ū' => 'U', 'ū' => 'u', + 'Ŭ' => 'U', 'ŭ' => 'u', + 'Ů' => 'U', 'ů' => 'u', + 'Ű' => 'U', 'ű' => 'u', + 'Ų' => 'U', 'ų' => 'u', + 'Ŵ' => 'W', 'ŵ' => 'w', + 'Ŷ' => 'Y', 'ŷ' => 'y', + 'Ÿ' => 'Y', 'Ź' => 'Z', + 'ź' => 'z', 'Ż' => 'Z', + 'ż' => 'z', 'Ž' => 'Z', + 'ž' => 'z', 'ſ' => 's', + // Decompositions for Latin Extended-B + 'Ș' => 'S', 'ș' => 's', + 'Ț' => 'T', 'ț' => 't', + // Euro Sign + '€' => 'E', + // GBP (Pound) Sign + '£' => '', + // Vowels with diacritic (Vietnamese) + // unmarked + 'Ơ' => 'O', 'ơ' => 'o', + 'Ư' => 'U', 'ư' => 'u', + // grave accent + 'Ầ' => 'A', 'ầ' => 'a', + 'Ằ' => 'A', 'ằ' => 'a', + 'Ề' => 'E', 'ề' => 'e', + 'Ồ' => 'O', 'ồ' => 'o', + 'Ờ' => 'O', 'ờ' => 'o', + 'Ừ' => 'U', 'ừ' => 'u', + 'Ỳ' => 'Y', 'ỳ' => 'y', + // hook + 'Ả' => 'A', 'ả' => 'a', + 'Ẩ' => 'A', 'ẩ' => 'a', + 'Ẳ' => 'A', 'ẳ' => 'a', + 'Ẻ' => 'E', 'ẻ' => 'e', + 'Ể' => 'E', 'ể' => 'e', + 'Ỉ' => 'I', 'ỉ' => 'i', + 'Ỏ' => 'O', 'ỏ' => 'o', + 'Ổ' => 'O', 'ổ' => 'o', + 'Ở' => 'O', 'ở' => 'o', + 'Ủ' => 'U', 'ủ' => 'u', + 'Ử' => 'U', 'ử' => 'u', + 'Ỷ' => 'Y', 'ỷ' => 'y', + // tilde + 'Ẫ' => 'A', 'ẫ' => 'a', + 'Ẵ' => 'A', 'ẵ' => 'a', + 'Ẽ' => 'E', 'ẽ' => 'e', + 'Ễ' => 'E', 'ễ' => 'e', + 'Ỗ' => 'O', 'ỗ' => 'o', + 'Ỡ' => 'O', 'ỡ' => 'o', + 'Ữ' => 'U', 'ữ' => 'u', + 'Ỹ' => 'Y', 'ỹ' => 'y', + // acute accent + 'Ấ' => 'A', 'ấ' => 'a', + 'Ắ' => 'A', 'ắ' => 'a', + 'Ế' => 'E', 'ế' => 'e', + 'Ố' => 'O', 'ố' => 'o', + 'Ớ' => 'O', 'ớ' => 'o', + 'Ứ' => 'U', 'ứ' => 'u', + // dot below + 'Ạ' => 'A', 'ạ' => 'a', + 'Ậ' => 'A', 'ậ' => 'a', + 'Ặ' => 'A', 'ặ' => 'a', + 'Ẹ' => 'E', 'ẹ' => 'e', + 'Ệ' => 'E', 'ệ' => 'e', + 'Ị' => 'I', 'ị' => 'i', + 'Ọ' => 'O', 'ọ' => 'o', + 'Ộ' => 'O', 'ộ' => 'o', + 'Ợ' => 'O', 'ợ' => 'o', + 'Ụ' => 'U', 'ụ' => 'u', + 'Ự' => 'U', 'ự' => 'u', + 'Ỵ' => 'Y', 'ỵ' => 'y', + // Vowels with diacritic (Chinese, Hanyu Pinyin) + 'ɑ' => 'a', + // macron + 'Ǖ' => 'U', 'ǖ' => 'u', + // acute accent + 'Ǘ' => 'U', 'ǘ' => 'u', + // caron + 'Ǎ' => 'A', 'ǎ' => 'a', + 'Ǐ' => 'I', 'ǐ' => 'i', + 'Ǒ' => 'O', 'ǒ' => 'o', + 'Ǔ' => 'U', 'ǔ' => 'u', + 'Ǚ' => 'U', 'ǚ' => 'u', + // grave accent + 'Ǜ' => 'U', 'ǜ' => 'u', + ]; - return $string; - } + $string = strtr($string, $chars); + + return $string; + } } diff --git a/bridges/BadDragonBridge.php b/bridges/BadDragonBridge.php index dd3de6b4..2260bbd6 100644 --- a/bridges/BadDragonBridge.php +++ b/bridges/BadDragonBridge.php @@ -1,432 +1,440 @@ <?php -class BadDragonBridge extends BridgeAbstract { - const NAME = 'Bad Dragon Bridge'; - const URI = 'https://bad-dragon.com/'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Returns sales or new clearance items'; - const MAINTAINER = 'Roliga'; - const PARAMETERS = array( - 'Sales' => array( - ), - 'Clearance' => array( - 'ready_made' => array( - 'name' => 'Ready Made', - 'type' => 'checkbox' - ), - 'flop' => array( - 'name' => 'Flops', - 'type' => 'checkbox' - ), - 'skus' => array( - 'name' => 'Products', - 'exampleValue' => 'chanceflared, crackers', - 'title' => 'Comma separated list of product SKUs' - ), - 'onesize' => array( - 'name' => 'One-Size', - 'type' => 'checkbox' - ), - 'mini' => array( - 'name' => 'Mini', - 'type' => 'checkbox' - ), - 'small' => array( - 'name' => 'Small', - 'type' => 'checkbox' - ), - 'medium' => array( - 'name' => 'Medium', - 'type' => 'checkbox' - ), - 'large' => array( - 'name' => 'Large', - 'type' => 'checkbox' - ), - 'extralarge' => array( - 'name' => 'Extra Large', - 'type' => 'checkbox' - ), - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'All' => 'all', - 'Accessories' => 'accessories', - 'Merchandise' => 'merchandise', - 'Dildos' => 'insertable', - 'Masturbators' => 'penetrable', - 'Packers' => 'packer', - 'Lil\' Squirts' => 'shooter', - 'Lil\' Vibes' => 'vibrator', - 'Wearables' => 'wearable' - ), - 'defaultValue' => 'all', - ), - 'soft' => array( - 'name' => 'Soft Firmness', - 'type' => 'checkbox' - ), - 'med_firm' => array( - 'name' => 'Medium Firmness', - 'type' => 'checkbox' - ), - 'firm' => array( - 'name' => 'Firm', - 'type' => 'checkbox' - ), - 'split' => array( - 'name' => 'Split Firmness', - 'type' => 'checkbox' - ), - 'maxprice' => array( - 'name' => 'Max Price', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 300 - ), - 'minprice' => array( - 'name' => 'Min Price', - 'type' => 'number', - 'defaultValue' => 0 - ), - 'cumtube' => array( - 'name' => 'Cumtube', - 'type' => 'checkbox' - ), - 'suctionCup' => array( - 'name' => 'Suction Cup', - 'type' => 'checkbox' - ), - 'noAccessories' => array( - 'name' => 'No Accessories', - 'type' => 'checkbox' - ) - ) - ); - - /* - * This sets index $strFrom (or $strTo if set) in $outArr to 'on' if - * $inArr[$param] contains $strFrom. - * It is used for translating BD's shop filter URLs into something we can use. - * - * For the query '?type[]=ready_made&type[]=flop' we would have an array like: - * Array ( - * [type] => Array ( - * [0] => ready_made - * [1] => flop - * ) - * ) - * which could be translated into: - * Array ( - * [ready_made] => on - * [flop] => on - * ) - * */ - private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null) { - if(isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) { - $outArr[($strTo ?: $strFrom)] = 'on'; - } - } - - public function detectParameters($url) { - $params = array(); - - // Sale - $regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/'; - if(preg_match($regex, $url, $matches) > 0) { - return $params; - } - - // Clearance - $regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/'; - if(preg_match($regex, $url, $matches) > 0) { - parse_str(parse_url($url, PHP_URL_QUERY), $urlParams); - - $this->setParam($urlParams, $params, 'type', 'ready_made'); - $this->setParam($urlParams, $params, 'type', 'flop'); - - if(isset($urlParams['skus'])) { - $skus = array(); - foreach($urlParams['skus'] as $sku) { - is_string($sku) && $skus[] = $sku; - is_array($sku) && $skus[] = $sku[0]; - } - $params['skus'] = implode(',', $skus); - } - - $this->setParam($urlParams, $params, 'sizes', 'onesize'); - $this->setParam($urlParams, $params, 'sizes', 'mini'); - $this->setParam($urlParams, $params, 'sizes', 'small'); - $this->setParam($urlParams, $params, 'sizes', 'medium'); - $this->setParam($urlParams, $params, 'sizes', 'large'); - $this->setParam($urlParams, $params, 'sizes', 'extralarge'); - - if(isset($urlParams['category'])) { - $params['category'] = strtolower($urlParams['category']); - } else{ - $params['category'] = 'all'; - } - - $this->setParam($urlParams, $params, 'firmnessValues', 'soft'); - $this->setParam($urlParams, $params, 'firmnessValues', 'medium', 'med_firm'); - $this->setParam($urlParams, $params, 'firmnessValues', 'firm'); - $this->setParam($urlParams, $params, 'firmnessValues', 'split'); - - if(isset($urlParams['price'])) { - isset($urlParams['price']['max']) - && $params['maxprice'] = $urlParams['price']['max']; - isset($urlParams['price']['min']) - && $params['minprice'] = $urlParams['price']['min']; - } - - isset($urlParams['cumtube']) - && $urlParams['cumtube'] === '1' - && $params['cumtube'] = 'on'; - isset($urlParams['suctionCup']) - && $urlParams['suctionCup'] === '1' - && $params['suctionCup'] = 'on'; - isset($urlParams['noAccessories']) - && $urlParams['noAccessories'] === '1' - && $params['noAccessories'] = 'on'; - - return $params; - } - - return null; - } - - public function getName() { - switch($this->queriedContext) { - case 'Sales': - return 'Bad Dragon Sales'; - case 'Clearance': - return 'Bad Dragon Clearance Search'; - default: - return parent::getName(); - } - } - - public function getURI() { - switch($this->queriedContext) { - case 'Sales': - return self::URI . 'sales'; - case 'Clearance': - return $this->inputToURL(); - default: - return parent::getURI(); - } - } - - public function collectData() { - switch($this->queriedContext) { - case 'Sales': - $sales = json_decode(getContents(self::URI . 'api/sales')); - - foreach($sales as $sale) { - $item = array(); - - $item['title'] = $sale->title; - $item['timestamp'] = strtotime($sale->startDate); - - $item['uri'] = $this->getURI() . '/' . $sale->slug; - - $contentHTML = '<p><img src="' . $sale->image->url . '"></p>'; - if(isset($sale->endDate)) { - $contentHTML .= '<p><b>This promotion ends on ' - . gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate)) - . '</b></p>'; - } else { - $contentHTML .= '<p><b>This promotion never ends</b></p>'; - } - $ul = false; - $content = json_decode($sale->content); - foreach($content->blocks as $block) { - switch($block->type) { - case 'header-one': - $contentHTML .= '<h1>' . $block->text . '</h1>'; - break; - case 'header-two': - $contentHTML .= '<h2>' . $block->text . '</h2>'; - break; - case 'header-three': - $contentHTML .= '<h3>' . $block->text . '</h3>'; - break; - case 'unordered-list-item': - if(!$ul) { - $contentHTML .= '<ul>'; - $ul = true; - } - $contentHTML .= '<li>' . $block->text . '</li>'; - break; - default: - if($ul) { - $contentHTML .= '</ul>'; - $ul = false; - } - $contentHTML .= '<p>' . $block->text . '</p>'; - break; - } - } - $item['content'] = $contentHTML; - - $this->items[] = $item; - } - break; - case 'Clearance': - $toyData = json_decode(getContents($this->inputToURL(true))); - - $productList = json_decode(getContents(self::URI - . 'api/inventory-toy/product-list')); - - foreach($toyData->toys as $toy) { - $item = array(); - - $item['uri'] = $this->getURI() - . '#' - . $toy->id; - $item['timestamp'] = strtotime($toy->created); - - foreach($productList as $product) { - if($product->sku == $toy->sku) { - $item['title'] = $product->name; - break; - } - } - - // images - $content = '<p>'; - foreach($toy->images as $image) { - $content .= '<a href="' - . $image->fullFilename - . '"><img src="' - . $image->thumbFilename - . '" /></a>'; - } - // price - $content .= '</p><p><b>Price:</b> $' - . $toy->price - // size - . '<br /><b>Size:</b> ' - . $toy->size - // color - . '<br /><b>Color:</b> ' - . $toy->color - // features - . '<br /><b>Features:</b> ' - . ($toy->suction_cup ? 'Suction cup' : '') - . ($toy->suction_cup && $toy->cumtube ? ', ' : '') - . ($toy->cumtube ? 'Cumtube' : '') - . ($toy->suction_cup || $toy->cumtube ? '' : 'None'); - // firmness - $firmnessTexts = array( - '2' => 'Extra soft', - '3' => 'Soft', - '5' => 'Medium', - '8' => 'Firm' - ); - $firmnesses = explode('/', $toy->firmness); - if(count($firmnesses) === 2) { - $content .= '<br /><b>Firmness:</b> ' - . $firmnessTexts[$firmnesses[0]] - . ', ' - . $firmnessTexts[$firmnesses[1]]; - } else{ - $content .= '<br /><b>Firmness:</b> ' - . $firmnessTexts[$firmnesses[0]]; - } - // flop - if($toy->type === 'flop') { - $content .= '<br /><b>Flop reason:</b> ' - . $toy->flop_reason; - } - $content .= '</p>'; - $item['content'] = $content; - - $enclosures = array(); - foreach($toy->images as $image) { - $enclosures[] = $image->fullFilename; - } - $item['enclosures'] = $enclosures; - - $categories = array(); - $categories[] = $toy->sku; - $categories[] = $toy->type; - $categories[] = $toy->size; - if($toy->cumtube) { - $categories[] = 'cumtube'; - } - if($toy->suction_cup) { - $categories[] = 'suction_cup'; - } - $item['categories'] = $categories; - - $this->items[] = $item; - } - break; - } - } - - private function inputToURL($api = false) { - $url = self::URI; - $url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?'); - - // Default parameters - $url .= 'limit=60'; - $url .= '&page=1'; - $url .= '&sort[field]=created'; - $url .= '&sort[direction]=desc'; - - // Product types - $url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : ''); - $url .= ($this->getInput('flop') ? '&type[]=flop' : ''); - - // Product names - foreach(array_filter(explode(',', $this->getInput('skus'))) as $sku) { - $url .= '&skus[]=' . urlencode(trim($sku)); - } - - // Size - $url .= ($this->getInput('onesize') ? '&sizes[]=onesize' : ''); - $url .= ($this->getInput('mini') ? '&sizes[]=mini' : ''); - $url .= ($this->getInput('small') ? '&sizes[]=small' : ''); - $url .= ($this->getInput('medium') ? '&sizes[]=medium' : ''); - $url .= ($this->getInput('large') ? '&sizes[]=large' : ''); - $url .= ($this->getInput('extralarge') ? '&sizes[]=extralarge' : ''); - - // Category - $url .= ($this->getInput('category') ? '&category=' - . urlencode($this->getInput('category')) : ''); - - // Firmness - if($api) { - $url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : ''); - $url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : ''); - $url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : ''); - if($this->getInput('split')) { - $url .= '&firmnessValues[]=3/5'; - $url .= '&firmnessValues[]=3/8'; - $url .= '&firmnessValues[]=8/3'; - $url .= '&firmnessValues[]=5/8'; - $url .= '&firmnessValues[]=8/5'; - } - } else{ - $url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : ''); - $url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : ''); - $url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : ''); - $url .= ($this->getInput('split') ? '&firmnessValues[]=split' : ''); - } - - // Price - $url .= ($this->getInput('maxprice') ? '&price[max]=' - . $this->getInput('maxprice') : '&price[max]=300'); - $url .= ($this->getInput('minprice') ? '&price[min]=' - . $this->getInput('minprice') : '&price[min]=0'); - - // Features - $url .= ($this->getInput('cumtube') ? '&cumtube=1' : ''); - $url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : ''); - $url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : ''); - - return $url; - } + +class BadDragonBridge extends BridgeAbstract +{ + const NAME = 'Bad Dragon Bridge'; + const URI = 'https://bad-dragon.com/'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Returns sales or new clearance items'; + const MAINTAINER = 'Roliga'; + const PARAMETERS = [ + 'Sales' => [ + ], + 'Clearance' => [ + 'ready_made' => [ + 'name' => 'Ready Made', + 'type' => 'checkbox' + ], + 'flop' => [ + 'name' => 'Flops', + 'type' => 'checkbox' + ], + 'skus' => [ + 'name' => 'Products', + 'exampleValue' => 'chanceflared, crackers', + 'title' => 'Comma separated list of product SKUs' + ], + 'onesize' => [ + 'name' => 'One-Size', + 'type' => 'checkbox' + ], + 'mini' => [ + 'name' => 'Mini', + 'type' => 'checkbox' + ], + 'small' => [ + 'name' => 'Small', + 'type' => 'checkbox' + ], + 'medium' => [ + 'name' => 'Medium', + 'type' => 'checkbox' + ], + 'large' => [ + 'name' => 'Large', + 'type' => 'checkbox' + ], + 'extralarge' => [ + 'name' => 'Extra Large', + 'type' => 'checkbox' + ], + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'All' => 'all', + 'Accessories' => 'accessories', + 'Merchandise' => 'merchandise', + 'Dildos' => 'insertable', + 'Masturbators' => 'penetrable', + 'Packers' => 'packer', + 'Lil\' Squirts' => 'shooter', + 'Lil\' Vibes' => 'vibrator', + 'Wearables' => 'wearable' + ], + 'defaultValue' => 'all', + ], + 'soft' => [ + 'name' => 'Soft Firmness', + 'type' => 'checkbox' + ], + 'med_firm' => [ + 'name' => 'Medium Firmness', + 'type' => 'checkbox' + ], + 'firm' => [ + 'name' => 'Firm', + 'type' => 'checkbox' + ], + 'split' => [ + 'name' => 'Split Firmness', + 'type' => 'checkbox' + ], + 'maxprice' => [ + 'name' => 'Max Price', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 300 + ], + 'minprice' => [ + 'name' => 'Min Price', + 'type' => 'number', + 'defaultValue' => 0 + ], + 'cumtube' => [ + 'name' => 'Cumtube', + 'type' => 'checkbox' + ], + 'suctionCup' => [ + 'name' => 'Suction Cup', + 'type' => 'checkbox' + ], + 'noAccessories' => [ + 'name' => 'No Accessories', + 'type' => 'checkbox' + ] + ] + ]; + + /* + * This sets index $strFrom (or $strTo if set) in $outArr to 'on' if + * $inArr[$param] contains $strFrom. + * It is used for translating BD's shop filter URLs into something we can use. + * + * For the query '?type[]=ready_made&type[]=flop' we would have an array like: + * Array ( + * [type] => Array ( + * [0] => ready_made + * [1] => flop + * ) + * ) + * which could be translated into: + * Array ( + * [ready_made] => on + * [flop] => on + * ) + * */ + private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null) + { + if (isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) { + $outArr[($strTo ?: $strFrom)] = 'on'; + } + } + + public function detectParameters($url) + { + $params = []; + + // Sale + $regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/'; + if (preg_match($regex, $url, $matches) > 0) { + return $params; + } + + // Clearance + $regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/'; + if (preg_match($regex, $url, $matches) > 0) { + parse_str(parse_url($url, PHP_URL_QUERY), $urlParams); + + $this->setParam($urlParams, $params, 'type', 'ready_made'); + $this->setParam($urlParams, $params, 'type', 'flop'); + + if (isset($urlParams['skus'])) { + $skus = []; + foreach ($urlParams['skus'] as $sku) { + is_string($sku) && $skus[] = $sku; + is_array($sku) && $skus[] = $sku[0]; + } + $params['skus'] = implode(',', $skus); + } + + $this->setParam($urlParams, $params, 'sizes', 'onesize'); + $this->setParam($urlParams, $params, 'sizes', 'mini'); + $this->setParam($urlParams, $params, 'sizes', 'small'); + $this->setParam($urlParams, $params, 'sizes', 'medium'); + $this->setParam($urlParams, $params, 'sizes', 'large'); + $this->setParam($urlParams, $params, 'sizes', 'extralarge'); + + if (isset($urlParams['category'])) { + $params['category'] = strtolower($urlParams['category']); + } else { + $params['category'] = 'all'; + } + + $this->setParam($urlParams, $params, 'firmnessValues', 'soft'); + $this->setParam($urlParams, $params, 'firmnessValues', 'medium', 'med_firm'); + $this->setParam($urlParams, $params, 'firmnessValues', 'firm'); + $this->setParam($urlParams, $params, 'firmnessValues', 'split'); + + if (isset($urlParams['price'])) { + isset($urlParams['price']['max']) + && $params['maxprice'] = $urlParams['price']['max']; + isset($urlParams['price']['min']) + && $params['minprice'] = $urlParams['price']['min']; + } + + isset($urlParams['cumtube']) + && $urlParams['cumtube'] === '1' + && $params['cumtube'] = 'on'; + isset($urlParams['suctionCup']) + && $urlParams['suctionCup'] === '1' + && $params['suctionCup'] = 'on'; + isset($urlParams['noAccessories']) + && $urlParams['noAccessories'] === '1' + && $params['noAccessories'] = 'on'; + + return $params; + } + + return null; + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Sales': + return 'Bad Dragon Sales'; + case 'Clearance': + return 'Bad Dragon Clearance Search'; + default: + return parent::getName(); + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Sales': + return self::URI . 'sales'; + case 'Clearance': + return $this->inputToURL(); + default: + return parent::getURI(); + } + } + + public function collectData() + { + switch ($this->queriedContext) { + case 'Sales': + $sales = json_decode(getContents(self::URI . 'api/sales')); + + foreach ($sales as $sale) { + $item = []; + + $item['title'] = $sale->title; + $item['timestamp'] = strtotime($sale->startDate); + + $item['uri'] = $this->getURI() . '/' . $sale->slug; + + $contentHTML = '<p><img src="' . $sale->image->url . '"></p>'; + if (isset($sale->endDate)) { + $contentHTML .= '<p><b>This promotion ends on ' + . gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate)) + . '</b></p>'; + } else { + $contentHTML .= '<p><b>This promotion never ends</b></p>'; + } + $ul = false; + $content = json_decode($sale->content); + foreach ($content->blocks as $block) { + switch ($block->type) { + case 'header-one': + $contentHTML .= '<h1>' . $block->text . '</h1>'; + break; + case 'header-two': + $contentHTML .= '<h2>' . $block->text . '</h2>'; + break; + case 'header-three': + $contentHTML .= '<h3>' . $block->text . '</h3>'; + break; + case 'unordered-list-item': + if (!$ul) { + $contentHTML .= '<ul>'; + $ul = true; + } + $contentHTML .= '<li>' . $block->text . '</li>'; + break; + default: + if ($ul) { + $contentHTML .= '</ul>'; + $ul = false; + } + $contentHTML .= '<p>' . $block->text . '</p>'; + break; + } + } + $item['content'] = $contentHTML; + + $this->items[] = $item; + } + break; + case 'Clearance': + $toyData = json_decode(getContents($this->inputToURL(true))); + + $productList = json_decode(getContents(self::URI + . 'api/inventory-toy/product-list')); + + foreach ($toyData->toys as $toy) { + $item = []; + + $item['uri'] = $this->getURI() + . '#' + . $toy->id; + $item['timestamp'] = strtotime($toy->created); + + foreach ($productList as $product) { + if ($product->sku == $toy->sku) { + $item['title'] = $product->name; + break; + } + } + + // images + $content = '<p>'; + foreach ($toy->images as $image) { + $content .= '<a href="' + . $image->fullFilename + . '"><img src="' + . $image->thumbFilename + . '" /></a>'; + } + // price + $content .= '</p><p><b>Price:</b> $' + . $toy->price + // size + . '<br /><b>Size:</b> ' + . $toy->size + // color + . '<br /><b>Color:</b> ' + . $toy->color + // features + . '<br /><b>Features:</b> ' + . ($toy->suction_cup ? 'Suction cup' : '') + . ($toy->suction_cup && $toy->cumtube ? ', ' : '') + . ($toy->cumtube ? 'Cumtube' : '') + . ($toy->suction_cup || $toy->cumtube ? '' : 'None'); + // firmness + $firmnessTexts = [ + '2' => 'Extra soft', + '3' => 'Soft', + '5' => 'Medium', + '8' => 'Firm' + ]; + $firmnesses = explode('/', $toy->firmness); + if (count($firmnesses) === 2) { + $content .= '<br /><b>Firmness:</b> ' + . $firmnessTexts[$firmnesses[0]] + . ', ' + . $firmnessTexts[$firmnesses[1]]; + } else { + $content .= '<br /><b>Firmness:</b> ' + . $firmnessTexts[$firmnesses[0]]; + } + // flop + if ($toy->type === 'flop') { + $content .= '<br /><b>Flop reason:</b> ' + . $toy->flop_reason; + } + $content .= '</p>'; + $item['content'] = $content; + + $enclosures = []; + foreach ($toy->images as $image) { + $enclosures[] = $image->fullFilename; + } + $item['enclosures'] = $enclosures; + + $categories = []; + $categories[] = $toy->sku; + $categories[] = $toy->type; + $categories[] = $toy->size; + if ($toy->cumtube) { + $categories[] = 'cumtube'; + } + if ($toy->suction_cup) { + $categories[] = 'suction_cup'; + } + $item['categories'] = $categories; + + $this->items[] = $item; + } + break; + } + } + + private function inputToURL($api = false) + { + $url = self::URI; + $url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?'); + + // Default parameters + $url .= 'limit=60'; + $url .= '&page=1'; + $url .= '&sort[field]=created'; + $url .= '&sort[direction]=desc'; + + // Product types + $url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : ''); + $url .= ($this->getInput('flop') ? '&type[]=flop' : ''); + + // Product names + foreach (array_filter(explode(',', $this->getInput('skus'))) as $sku) { + $url .= '&skus[]=' . urlencode(trim($sku)); + } + + // Size + $url .= ($this->getInput('onesize') ? '&sizes[]=onesize' : ''); + $url .= ($this->getInput('mini') ? '&sizes[]=mini' : ''); + $url .= ($this->getInput('small') ? '&sizes[]=small' : ''); + $url .= ($this->getInput('medium') ? '&sizes[]=medium' : ''); + $url .= ($this->getInput('large') ? '&sizes[]=large' : ''); + $url .= ($this->getInput('extralarge') ? '&sizes[]=extralarge' : ''); + + // Category + $url .= ($this->getInput('category') ? '&category=' + . urlencode($this->getInput('category')) : ''); + + // Firmness + if ($api) { + $url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : ''); + $url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : ''); + $url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : ''); + if ($this->getInput('split')) { + $url .= '&firmnessValues[]=3/5'; + $url .= '&firmnessValues[]=3/8'; + $url .= '&firmnessValues[]=8/3'; + $url .= '&firmnessValues[]=5/8'; + $url .= '&firmnessValues[]=8/5'; + } + } else { + $url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : ''); + $url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : ''); + $url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : ''); + $url .= ($this->getInput('split') ? '&firmnessValues[]=split' : ''); + } + + // Price + $url .= ($this->getInput('maxprice') ? '&price[max]=' + . $this->getInput('maxprice') : '&price[max]=300'); + $url .= ($this->getInput('minprice') ? '&price[min]=' + . $this->getInput('minprice') : '&price[min]=0'); + + // Features + $url .= ($this->getInput('cumtube') ? '&cumtube=1' : ''); + $url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : ''); + $url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : ''); + + return $url; + } } diff --git a/bridges/BakaUpdatesMangaReleasesBridge.php b/bridges/BakaUpdatesMangaReleasesBridge.php index aa4ab967..10d59c83 100644 --- a/bridges/BakaUpdatesMangaReleasesBridge.php +++ b/bridges/BakaUpdatesMangaReleasesBridge.php @@ -1,186 +1,211 @@ <?php -class BakaUpdatesMangaReleasesBridge extends BridgeAbstract { - const NAME = 'Baka Updates Manga Releases'; - const URI = 'https://www.mangaupdates.com/'; - const DESCRIPTION = 'Get the latest series releases'; - const MAINTAINER = 'fulmeek, KamaleiZestri'; - const PARAMETERS = array( - 'By series' => array( - 'series_id' => array( - 'name' => 'Series ID', - 'type' => 'number', - 'required' => true, - 'exampleValue' => '188066' - ) - ), - 'By list' => array( - 'list_id' => array( - 'name' => 'List ID and Type', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '4395&list=read' - ) - ) - ); - const LIMIT_COLS = 5; - const LIMIT_ITEMS = 10; - const RELEASES_URL = 'https://www.mangaupdates.com/releases.html'; - - private $feedName = ''; - - public function collectData() { - if($this -> queriedContext == 'By series') - $this -> collectDataBySeries(); - else //queriedContext == 'By list' - $this -> collectDataByList(); - } - - public function getURI(){ - if($this -> queriedContext == 'By series') { - $series_id = $this->getInput('series_id'); - if (!empty($series_id)) { - return self::URI . 'releases.html?search=' . $series_id . '&stype=series'; - } - } else //queriedContext == 'By list' - return self::RELEASES_URL; - - return self::URI; - } - - public function getName(){ - if(!empty($this->feedName)) { - return $this->feedName . ' - ' . self::NAME; - } - return parent::getName(); - } - - private function getSanitizedHash($string) { - return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string)))); - } - - private function filterText($text) { - return rtrim($text, '* '); - } - - private function filterHTML($text) { - return $this->filterText(html_entity_decode($text)); - } - - private function findID($manga) { - // sometimes new series are on the release list that have no ID. just drop them. - if(@$this -> filterHTML($manga -> find('a', 0) -> href) != null) { - preg_match('/id=([0-9]*)/', $this -> filterHTML($manga -> find('a', 0) -> href), $match); - return $match[1]; - } else - return 0; - } - - private function collectDataBySeries() { - $html = getSimpleHTMLDOM($this->getURI()); - - // content is an unstructured pile of divs, ugly to parse - $cols = $html->find('div#main_content div.row > div.text'); - if (!$cols) - returnServerError('No releases'); - - $rows = array_slice( - array_chunk($cols, self::LIMIT_COLS), 0, self::LIMIT_ITEMS - ); - - if (isset($rows[0][1])) { - $this->feedName = $this->filterHTML($rows[0][1]->plaintext); - } - - foreach($rows as $cols) { - if (count($cols) < self::LIMIT_COLS) continue; - - $item = array(); - $title = array(); - - $item['content'] = ''; - - $objDate = $cols[0]; - if ($objDate) - $item['timestamp'] = strtotime($objDate->plaintext); - - $objTitle = $cols[1]; - if ($objTitle) { - $title[] = $this->filterHTML($objTitle->plaintext); - $item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>'; - } - - $objVolume = $cols[2]; - if ($objVolume && !empty($objVolume->plaintext)) - $title[] = 'Vol.' . $objVolume->plaintext; - - $objChapter = $cols[3]; - if ($objChapter && !empty($objChapter->plaintext)) - $title[] = 'Chp.' . $objChapter->plaintext; - - $objAuthor = $cols[4]; - if ($objAuthor && !empty($objAuthor->plaintext)) { - $item['author'] = $this->filterHTML($objAuthor->plaintext); - $item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>'; - } - - $item['title'] = implode(' ', $title); - $item['uri'] = $this->getURI(); - $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']); - - $this->items[] = $item; - } - } - - private function collectDataByList() { - $this -> feedName = 'Releases'; - $list = array(); - - $releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL); - - $list_id = $this -> getInput('list_id'); - $listHTML = getSimpleHTMLDOM('https://www.mangaupdates.com/mylist.html?id=' . $list_id); - - //get ids of the manga that the user follows, - $parts = $listHTML -> find('table#ptable tr > td.pl'); - foreach($parts as $part) { - $list[] = $this -> findID($part); - } - - //similar to above, but the divs are in groups of 3. - $cols = $releasesHTML -> find('div#main_content div.row > div.pbreak'); - $rows = array_slice(array_chunk($cols, 3), 0); - - foreach($rows as $cols) { - //check if current manga is in user's list. - $id = $this -> findId($cols[0]); - if(!array_search($id, $list)) continue; - - $item = array(); - $title = array(); - - $item['content'] = ''; - - $objTitle = $cols[0]; - if ($objTitle) { - $title[] = $this->filterHTML($objTitle->plaintext); - $item['content'] .= '<p>Series: ' . $this->filterHTML($objTitle -> innertext) . '</p>'; - } - - $objVolChap = $cols[1]; - if ($objVolChap && !empty($objVolChap->plaintext)) - $title[] = $this -> filterHTML($objVolChap -> innertext); - - $objAuthor = $cols[2]; - if ($objAuthor && !empty($objAuthor->plaintext)) { - $item['author'] = $this->filterHTML($objAuthor -> plaintext); - $item['content'] .= '<p>Groups: ' . $this->filterHTML($objAuthor -> innertext) . '</p>'; - } - - $item['title'] = implode(' ', $title); - $item['uri'] = self::URI . 'releases.html?search=' . $id . '&stype=series'; - $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']); - - $this->items[] = $item; - } - } + +class BakaUpdatesMangaReleasesBridge extends BridgeAbstract +{ + const NAME = 'Baka Updates Manga Releases'; + const URI = 'https://www.mangaupdates.com/'; + const DESCRIPTION = 'Get the latest series releases'; + const MAINTAINER = 'fulmeek, KamaleiZestri'; + const PARAMETERS = [ + 'By series' => [ + 'series_id' => [ + 'name' => 'Series ID', + 'type' => 'number', + 'required' => true, + 'exampleValue' => '188066' + ] + ], + 'By list' => [ + 'list_id' => [ + 'name' => 'List ID and Type', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '4395&list=read' + ] + ] + ]; + const LIMIT_COLS = 5; + const LIMIT_ITEMS = 10; + const RELEASES_URL = 'https://www.mangaupdates.com/releases.html'; + + private $feedName = ''; + + public function collectData() + { + if ($this -> queriedContext == 'By series') { + $this -> collectDataBySeries(); + } else { //queriedContext == 'By list' + $this -> collectDataByList(); + } + } + + public function getURI() + { + if ($this -> queriedContext == 'By series') { + $series_id = $this->getInput('series_id'); + if (!empty($series_id)) { + return self::URI . 'releases.html?search=' . $series_id . '&stype=series'; + } + } else { //queriedContext == 'By list' + return self::RELEASES_URL; + } + + return self::URI; + } + + public function getName() + { + if (!empty($this->feedName)) { + return $this->feedName . ' - ' . self::NAME; + } + return parent::getName(); + } + + private function getSanitizedHash($string) + { + return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string)))); + } + + private function filterText($text) + { + return rtrim($text, '* '); + } + + private function filterHTML($text) + { + return $this->filterText(html_entity_decode($text)); + } + + private function findID($manga) + { + // sometimes new series are on the release list that have no ID. just drop them. + if (@$this -> filterHTML($manga -> find('a', 0) -> href) != null) { + preg_match('/id=([0-9]*)/', $this -> filterHTML($manga -> find('a', 0) -> href), $match); + return $match[1]; + } else { + return 0; + } + } + + private function collectDataBySeries() + { + $html = getSimpleHTMLDOM($this->getURI()); + + // content is an unstructured pile of divs, ugly to parse + $cols = $html->find('div#main_content div.row > div.text'); + if (!$cols) { + returnServerError('No releases'); + } + + $rows = array_slice( + array_chunk($cols, self::LIMIT_COLS), + 0, + self::LIMIT_ITEMS + ); + + if (isset($rows[0][1])) { + $this->feedName = $this->filterHTML($rows[0][1]->plaintext); + } + + foreach ($rows as $cols) { + if (count($cols) < self::LIMIT_COLS) { + continue; + } + + $item = []; + $title = []; + + $item['content'] = ''; + + $objDate = $cols[0]; + if ($objDate) { + $item['timestamp'] = strtotime($objDate->plaintext); + } + + $objTitle = $cols[1]; + if ($objTitle) { + $title[] = $this->filterHTML($objTitle->plaintext); + $item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>'; + } + + $objVolume = $cols[2]; + if ($objVolume && !empty($objVolume->plaintext)) { + $title[] = 'Vol.' . $objVolume->plaintext; + } + + $objChapter = $cols[3]; + if ($objChapter && !empty($objChapter->plaintext)) { + $title[] = 'Chp.' . $objChapter->plaintext; + } + + $objAuthor = $cols[4]; + if ($objAuthor && !empty($objAuthor->plaintext)) { + $item['author'] = $this->filterHTML($objAuthor->plaintext); + $item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>'; + } + + $item['title'] = implode(' ', $title); + $item['uri'] = $this->getURI(); + $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']); + + $this->items[] = $item; + } + } + + private function collectDataByList() + { + $this -> feedName = 'Releases'; + $list = []; + + $releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL); + + $list_id = $this -> getInput('list_id'); + $listHTML = getSimpleHTMLDOM('https://www.mangaupdates.com/mylist.html?id=' . $list_id); + + //get ids of the manga that the user follows, + $parts = $listHTML -> find('table#ptable tr > td.pl'); + foreach ($parts as $part) { + $list[] = $this -> findID($part); + } + + //similar to above, but the divs are in groups of 3. + $cols = $releasesHTML -> find('div#main_content div.row > div.pbreak'); + $rows = array_slice(array_chunk($cols, 3), 0); + + foreach ($rows as $cols) { + //check if current manga is in user's list. + $id = $this -> findId($cols[0]); + if (!array_search($id, $list)) { + continue; + } + + $item = []; + $title = []; + + $item['content'] = ''; + + $objTitle = $cols[0]; + if ($objTitle) { + $title[] = $this->filterHTML($objTitle->plaintext); + $item['content'] .= '<p>Series: ' . $this->filterHTML($objTitle -> innertext) . '</p>'; + } + + $objVolChap = $cols[1]; + if ($objVolChap && !empty($objVolChap->plaintext)) { + $title[] = $this -> filterHTML($objVolChap -> innertext); + } + + $objAuthor = $cols[2]; + if ($objAuthor && !empty($objAuthor->plaintext)) { + $item['author'] = $this->filterHTML($objAuthor -> plaintext); + $item['content'] .= '<p>Groups: ' . $this->filterHTML($objAuthor -> innertext) . '</p>'; + } + + $item['title'] = implode(' ', $title); + $item['uri'] = self::URI . 'releases.html?search=' . $id . '&stype=series'; + $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']); + + $this->items[] = $item; + } + } } diff --git a/bridges/BandcampBridge.php b/bridges/BandcampBridge.php index 181038d1..20b4ea93 100644 --- a/bridges/BandcampBridge.php +++ b/bridges/BandcampBridge.php @@ -1,408 +1,420 @@ <?php -class BandcampBridge extends BridgeAbstract { - - const MAINTAINER = 'sebsauvage, Roliga'; - const NAME = 'Bandcamp Bridge'; - const URI = 'https://bandcamp.com/'; - const CACHE_TIMEOUT = 600; // 10min - const DESCRIPTION = 'New bandcamp releases by tag, band or album'; - const PARAMETERS = array( - 'By tag' => array( - 'tag' => array( - 'name' => 'tag', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'hip-hop-rap' - ) - ), - 'By band' => array( - 'band' => array( - 'name' => 'band', - 'type' => 'text', - 'title' => 'Band name as seen in the band page URL', - 'required' => true, - 'exampleValue' => 'aesoprock' - ), - 'type' => array( - 'name' => 'Articles are', - 'type' => 'list', - 'values' => array( - 'Releases' => 'releases', - 'Releases, new one when track list changes' => 'changes', - 'Individual tracks' => 'tracks' - ), - 'defaultValue' => 'changes' - ), - 'limit' => array( - 'name' => 'limit', - 'type' => 'number', - 'required' => true, - 'title' => 'Number of releases to return', - 'defaultValue' => 5 - ) - ), - 'By label' => array( - 'label' => array( - 'name' => 'label', - 'type' => 'text', - 'title' => 'label name as seen in the label page URL', - 'required' => true - ), - 'type' => array( - 'name' => 'Articles are', - 'type' => 'list', - 'values' => array( - 'Releases' => 'releases', - 'Releases, new one when track list changes' => 'changes', - 'Individual tracks' => 'tracks' - ), - 'defaultValue' => 'changes' - ), - 'limit' => array( - 'name' => 'limit', - 'type' => 'number', - 'title' => 'Number of releases to return', - 'defaultValue' => 5 - ) - ), - 'By album' => array( - 'band' => array( - 'name' => 'band', - 'type' => 'text', - 'title' => 'Band name as seen in the album page URL', - 'required' => true, - 'exampleValue' => 'aesoprock' - ), - 'album' => array( - 'name' => 'album', - 'type' => 'text', - 'title' => 'Album name as seen in the album page URL', - 'required' => true, - 'exampleValue' => 'appleseed' - ), - 'type' => array( - 'name' => 'Articles are', - 'type' => 'list', - 'values' => array( - 'Releases' => 'releases', - 'Releases, new one when track list changes' => 'changes', - 'Individual tracks' => 'tracks' - ), - 'defaultValue' => 'tracks' - ) - ) - ); - const IMGURI = 'https://f4.bcbits.com/'; - const IMGSIZE_300PX = 23; - const IMGSIZE_700PX = 16; - - private $feedName; - - public function getIcon() { - return 'https://s4.bcbits.com/img/bc_favicon.ico'; - } - - public function collectData(){ - switch($this->queriedContext) { - case 'By tag': - $url = self::URI . 'api/hub/1/dig_deeper'; - $data = $this->buildRequestJson(); - $header = array( - 'Content-Type: application/json', - 'Content-Length: ' . strlen($data) - ); - $opts = array( - CURLOPT_CUSTOMREQUEST => 'POST', - CURLOPT_POSTFIELDS => $data - ); - $content = getContents($url, $header, $opts); - - $json = json_decode($content); - - if ($json->ok !== true) { - returnServerError('Invalid response'); - } - - foreach ($json->items as $entry) { - $url = $entry->tralbum_url; - $artist = $entry->artist; - $title = $entry->title; - // e.g. record label is the releaser, but not the artist - $releaser = $entry->band_name !== $entry->artist ? $entry->band_name : null; - - $full_title = $artist . ' - ' . $title; - $full_artist = $artist; - if (isset($releaser)) { - $full_title .= ' (' . $releaser . ')'; - $full_artist .= ' (' . $releaser . ')'; - } - $small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX); - $img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX); - - $item = array( - 'uri' => $url, - 'author' => $full_artist, - 'title' => $full_title - ); - $item['content'] = "<img src='$small_img' /><br/>$full_title"; - $item['enclosures'] = array($img); - $this->items[] = $item; - } - break; - case 'By band': - case 'By label': - case 'By album': - $html = getSimpleHTMLDOMCached($this->getURI(), 86400); - - if ($html->find('meta[name=title]', 0)) { - $this->feedName = $html->find('meta[name=title]', 0)->content; - } else { - $this->feedName = str_replace('Music | ', '', $html->find('title', 0)->plaintext); - } - - $regex = '/band_id=(\d+)/'; - if(preg_match($regex, $html, $matches) == false) - returnServerError('Unable to find band ID on: ' . $this->getURI()); - $band_id = $matches[1]; - - $tralbums = array(); - switch($this->queriedContext) { - case 'By band': - case 'By label': - $query_data = array( - 'band_id' => $band_id - ); - $band_data = $this->apiGet('mobile/22/band_details', $query_data); - - $num_albums = min(count($band_data->discography), $this->getInput('limit')); - for($i = 0; $i < $num_albums; $i++) { - $album_basic_data = $band_data->discography[$i]; - - // 'a' or 't' for albums and individual tracks respectively - $tralbum_type = substr($album_basic_data->item_type, 0, 1); - - $query_data = array( - 'band_id' => $band_id, - 'tralbum_type' => $tralbum_type, - 'tralbum_id' => $album_basic_data->item_id - ); - $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data); - } - break; - case 'By album': - $regex = '/album=(\d+)/'; - if(preg_match($regex, $html, $matches) == false) - returnServerError('Unable to find album ID on: ' . $this->getURI()); - $album_id = $matches[1]; - - $query_data = array( - 'band_id' => $band_id, - 'tralbum_type' => 'a', - 'tralbum_id' => $album_id - ); - $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data); - - break; - } - - foreach ($tralbums as $tralbum_data) { - if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') { - foreach ($tralbum_data->tracks as $track) { - $query_data = array( - 'band_id' => $band_id, - 'tralbum_type' => 't', - 'tralbum_id' => $track->track_id - ); - $track_data = $this->apiGet('mobile/22/tralbum_details', $query_data); - - $this->items[] = $this->buildTralbumItem($track_data); - } - } else { - $this->items[] = $this->buildTralbumItem($tralbum_data); - } - } - break; - } - } - - private function buildTralbumItem($tralbum_data){ - $band_data = $tralbum_data->band; - - // Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER) - // Format artist/author like: ARTIST (OPTIONAL RELEASER) - // - // If the album/track is released under a label/a band other than the artist - // themselves, append that releaser name to the title and artist/author. - // - // This sadly doesn't always work right for individual tracks as the artist - // of the track is always set to the releaser. - $artist = $tralbum_data->tralbum_artist; - $full_title = $artist . ' - ' . $tralbum_data->title; - $full_artist = $artist; - if (isset($tralbum_data->label)) { - $full_title .= ' (' . $tralbum_data->label . ')'; - $full_artist .= ' (' . $tralbum_data->label . ')'; - } elseif ($band_data->name !== $artist) { - $full_title .= ' (' . $band_data->name . ')'; - $full_artist .= ' (' . $band_data->name . ')'; - } - - $small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX); - $img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX); - - $item = array( - 'uri' => $tralbum_data->bandcamp_url, - 'author' => $full_artist, - 'title' => $full_title, - 'enclosures' => array($img), - 'timestamp' => $tralbum_data->release_date - ); - - $item['categories'] = array(); - foreach ($tralbum_data->tags as $tag) { - $item['categories'][] = $tag->norm_name; - } - - // Give articles a unique UID depending on its track list - // Releases should then show up as new articles when tracks are added - if ($this->getInput('type') === 'changes') { - $item['uid'] = "bandcamp/$band_data->band_id/$tralbum_data->id/"; - foreach ($tralbum_data->tracks as $track) { - $item['uid'] .= $track->track_id; - } - } - - $item['content'] = "<img src='$small_img' /><br/>$full_title<br/>"; - if ($tralbum_data->type === 'a') { - $item['content'] .= '<ol>'; - foreach ($tralbum_data->tracks as $track) { - $item['content'] .= "<li>$track->title</li>"; - } - $item['content'] .= '</ol>'; - } - if (!empty($tralbum_data->about)) { - $item['content'] .= '<p>' - . nl2br($tralbum_data->about) - . '</p>'; - } - - return $item; - } - - private function buildRequestJson(){ - $requestJson = array( - 'tag' => $this->getInput('tag'), - 'page' => 1, - 'sort' => 'date' - ); - return json_encode($requestJson); - } - - private function getImageUrl($id, $size){ - return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg'; - } - - private function apiGet($endpoint, $query_data) { - $url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data); - $data = json_decode(getContents($url)); - return $data; - } - - public function getURI(){ - switch($this->queriedContext) { - case 'By tag': - if(!is_null($this->getInput('tag'))) { - return self::URI - . 'tag/' - . urlencode($this->getInput('tag')) - . '?sort_field=date'; - } - break; - case 'By label': - if(!is_null($this->getInput('label'))) { - return 'https://' - . $this->getInput('label') - . '.bandcamp.com/music'; - } - break; - case 'By band': - if(!is_null($this->getInput('band'))) { - return 'https://' - . $this->getInput('band') - . '.bandcamp.com/music'; - } - break; - case 'By album': - if(!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) { - return 'https://' - . $this->getInput('band') - . '.bandcamp.com/album/' - . $this->getInput('album'); - } - break; - } - - return parent::getURI(); - } - - public function getName(){ - switch($this->queriedContext) { - case 'By tag': - if(!is_null($this->getInput('tag'))) { - return $this->getInput('tag') . ' - Bandcamp Tag'; - } - break; - case 'By band': - if(isset($this->feedName)) { - return $this->feedName . ' - Bandcamp Band'; - } elseif(!is_null($this->getInput('band'))) { - return $this->getInput('band') . ' - Bandcamp Band'; - } - break; - case 'By label': - if(isset($this->feedName)) { - return $this->feedName . ' - Bandcamp Label'; - } elseif(!is_null($this->getInput('label'))) { - return $this->getInput('label') . ' - Bandcamp Label'; - } - break; - case 'By album': - if(isset($this->feedName)) { - return $this->feedName . ' - Bandcamp Album'; - } elseif(!is_null($this->getInput('album'))) { - return $this->getInput('album') . ' - Bandcamp Album'; - } - break; - } - - return parent::getName(); - } - - public function detectParameters($url) { - $params = array(); - - // By tag - $regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['tag'] = urldecode($matches[2]); - return $params; - } - - // By band - $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['band'] = urldecode($matches[2]); - return $params; - } - - // By album - $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['band'] = urldecode($matches[2]); - $params['album'] = urldecode($matches[3]); - return $params; - } - - return null; - } + +class BandcampBridge extends BridgeAbstract +{ + const MAINTAINER = 'sebsauvage, Roliga'; + const NAME = 'Bandcamp Bridge'; + const URI = 'https://bandcamp.com/'; + const CACHE_TIMEOUT = 600; // 10min + const DESCRIPTION = 'New bandcamp releases by tag, band or album'; + const PARAMETERS = [ + 'By tag' => [ + 'tag' => [ + 'name' => 'tag', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'hip-hop-rap' + ] + ], + 'By band' => [ + 'band' => [ + 'name' => 'band', + 'type' => 'text', + 'title' => 'Band name as seen in the band page URL', + 'required' => true, + 'exampleValue' => 'aesoprock' + ], + 'type' => [ + 'name' => 'Articles are', + 'type' => 'list', + 'values' => [ + 'Releases' => 'releases', + 'Releases, new one when track list changes' => 'changes', + 'Individual tracks' => 'tracks' + ], + 'defaultValue' => 'changes' + ], + 'limit' => [ + 'name' => 'limit', + 'type' => 'number', + 'required' => true, + 'title' => 'Number of releases to return', + 'defaultValue' => 5 + ] + ], + 'By label' => [ + 'label' => [ + 'name' => 'label', + 'type' => 'text', + 'title' => 'label name as seen in the label page URL', + 'required' => true + ], + 'type' => [ + 'name' => 'Articles are', + 'type' => 'list', + 'values' => [ + 'Releases' => 'releases', + 'Releases, new one when track list changes' => 'changes', + 'Individual tracks' => 'tracks' + ], + 'defaultValue' => 'changes' + ], + 'limit' => [ + 'name' => 'limit', + 'type' => 'number', + 'title' => 'Number of releases to return', + 'defaultValue' => 5 + ] + ], + 'By album' => [ + 'band' => [ + 'name' => 'band', + 'type' => 'text', + 'title' => 'Band name as seen in the album page URL', + 'required' => true, + 'exampleValue' => 'aesoprock' + ], + 'album' => [ + 'name' => 'album', + 'type' => 'text', + 'title' => 'Album name as seen in the album page URL', + 'required' => true, + 'exampleValue' => 'appleseed' + ], + 'type' => [ + 'name' => 'Articles are', + 'type' => 'list', + 'values' => [ + 'Releases' => 'releases', + 'Releases, new one when track list changes' => 'changes', + 'Individual tracks' => 'tracks' + ], + 'defaultValue' => 'tracks' + ] + ] + ]; + const IMGURI = 'https://f4.bcbits.com/'; + const IMGSIZE_300PX = 23; + const IMGSIZE_700PX = 16; + + private $feedName; + + public function getIcon() + { + return 'https://s4.bcbits.com/img/bc_favicon.ico'; + } + + public function collectData() + { + switch ($this->queriedContext) { + case 'By tag': + $url = self::URI . 'api/hub/1/dig_deeper'; + $data = $this->buildRequestJson(); + $header = [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data) + ]; + $opts = [ + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => $data + ]; + $content = getContents($url, $header, $opts); + + $json = json_decode($content); + + if ($json->ok !== true) { + returnServerError('Invalid response'); + } + + foreach ($json->items as $entry) { + $url = $entry->tralbum_url; + $artist = $entry->artist; + $title = $entry->title; + // e.g. record label is the releaser, but not the artist + $releaser = $entry->band_name !== $entry->artist ? $entry->band_name : null; + + $full_title = $artist . ' - ' . $title; + $full_artist = $artist; + if (isset($releaser)) { + $full_title .= ' (' . $releaser . ')'; + $full_artist .= ' (' . $releaser . ')'; + } + $small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX); + $img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX); + + $item = [ + 'uri' => $url, + 'author' => $full_artist, + 'title' => $full_title + ]; + $item['content'] = "<img src='$small_img' /><br/>$full_title"; + $item['enclosures'] = [$img]; + $this->items[] = $item; + } + break; + case 'By band': + case 'By label': + case 'By album': + $html = getSimpleHTMLDOMCached($this->getURI(), 86400); + + if ($html->find('meta[name=title]', 0)) { + $this->feedName = $html->find('meta[name=title]', 0)->content; + } else { + $this->feedName = str_replace('Music | ', '', $html->find('title', 0)->plaintext); + } + + $regex = '/band_id=(\d+)/'; + if (preg_match($regex, $html, $matches) == false) { + returnServerError('Unable to find band ID on: ' . $this->getURI()); + } + $band_id = $matches[1]; + + $tralbums = []; + switch ($this->queriedContext) { + case 'By band': + case 'By label': + $query_data = [ + 'band_id' => $band_id + ]; + $band_data = $this->apiGet('mobile/22/band_details', $query_data); + + $num_albums = min(count($band_data->discography), $this->getInput('limit')); + for ($i = 0; $i < $num_albums; $i++) { + $album_basic_data = $band_data->discography[$i]; + + // 'a' or 't' for albums and individual tracks respectively + $tralbum_type = substr($album_basic_data->item_type, 0, 1); + + $query_data = [ + 'band_id' => $band_id, + 'tralbum_type' => $tralbum_type, + 'tralbum_id' => $album_basic_data->item_id + ]; + $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data); + } + break; + case 'By album': + $regex = '/album=(\d+)/'; + if (preg_match($regex, $html, $matches) == false) { + returnServerError('Unable to find album ID on: ' . $this->getURI()); + } + $album_id = $matches[1]; + + $query_data = [ + 'band_id' => $band_id, + 'tralbum_type' => 'a', + 'tralbum_id' => $album_id + ]; + $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data); + + break; + } + + foreach ($tralbums as $tralbum_data) { + if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') { + foreach ($tralbum_data->tracks as $track) { + $query_data = [ + 'band_id' => $band_id, + 'tralbum_type' => 't', + 'tralbum_id' => $track->track_id + ]; + $track_data = $this->apiGet('mobile/22/tralbum_details', $query_data); + + $this->items[] = $this->buildTralbumItem($track_data); + } + } else { + $this->items[] = $this->buildTralbumItem($tralbum_data); + } + } + break; + } + } + + private function buildTralbumItem($tralbum_data) + { + $band_data = $tralbum_data->band; + + // Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER) + // Format artist/author like: ARTIST (OPTIONAL RELEASER) + // + // If the album/track is released under a label/a band other than the artist + // themselves, append that releaser name to the title and artist/author. + // + // This sadly doesn't always work right for individual tracks as the artist + // of the track is always set to the releaser. + $artist = $tralbum_data->tralbum_artist; + $full_title = $artist . ' - ' . $tralbum_data->title; + $full_artist = $artist; + if (isset($tralbum_data->label)) { + $full_title .= ' (' . $tralbum_data->label . ')'; + $full_artist .= ' (' . $tralbum_data->label . ')'; + } elseif ($band_data->name !== $artist) { + $full_title .= ' (' . $band_data->name . ')'; + $full_artist .= ' (' . $band_data->name . ')'; + } + + $small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX); + $img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX); + + $item = [ + 'uri' => $tralbum_data->bandcamp_url, + 'author' => $full_artist, + 'title' => $full_title, + 'enclosures' => [$img], + 'timestamp' => $tralbum_data->release_date + ]; + + $item['categories'] = []; + foreach ($tralbum_data->tags as $tag) { + $item['categories'][] = $tag->norm_name; + } + + // Give articles a unique UID depending on its track list + // Releases should then show up as new articles when tracks are added + if ($this->getInput('type') === 'changes') { + $item['uid'] = "bandcamp/$band_data->band_id/$tralbum_data->id/"; + foreach ($tralbum_data->tracks as $track) { + $item['uid'] .= $track->track_id; + } + } + + $item['content'] = "<img src='$small_img' /><br/>$full_title<br/>"; + if ($tralbum_data->type === 'a') { + $item['content'] .= '<ol>'; + foreach ($tralbum_data->tracks as $track) { + $item['content'] .= "<li>$track->title</li>"; + } + $item['content'] .= '</ol>'; + } + if (!empty($tralbum_data->about)) { + $item['content'] .= '<p>' + . nl2br($tralbum_data->about) + . '</p>'; + } + + return $item; + } + + private function buildRequestJson() + { + $requestJson = [ + 'tag' => $this->getInput('tag'), + 'page' => 1, + 'sort' => 'date' + ]; + return json_encode($requestJson); + } + + private function getImageUrl($id, $size) + { + return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg'; + } + + private function apiGet($endpoint, $query_data) + { + $url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data); + $data = json_decode(getContents($url)); + return $data; + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'By tag': + if (!is_null($this->getInput('tag'))) { + return self::URI + . 'tag/' + . urlencode($this->getInput('tag')) + . '?sort_field=date'; + } + break; + case 'By label': + if (!is_null($this->getInput('label'))) { + return 'https://' + . $this->getInput('label') + . '.bandcamp.com/music'; + } + break; + case 'By band': + if (!is_null($this->getInput('band'))) { + return 'https://' + . $this->getInput('band') + . '.bandcamp.com/music'; + } + break; + case 'By album': + if (!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) { + return 'https://' + . $this->getInput('band') + . '.bandcamp.com/album/' + . $this->getInput('album'); + } + break; + } + + return parent::getURI(); + } + + public function getName() + { + switch ($this->queriedContext) { + case 'By tag': + if (!is_null($this->getInput('tag'))) { + return $this->getInput('tag') . ' - Bandcamp Tag'; + } + break; + case 'By band': + if (isset($this->feedName)) { + return $this->feedName . ' - Bandcamp Band'; + } elseif (!is_null($this->getInput('band'))) { + return $this->getInput('band') . ' - Bandcamp Band'; + } + break; + case 'By label': + if (isset($this->feedName)) { + return $this->feedName . ' - Bandcamp Label'; + } elseif (!is_null($this->getInput('label'))) { + return $this->getInput('label') . ' - Bandcamp Label'; + } + break; + case 'By album': + if (isset($this->feedName)) { + return $this->feedName . ' - Bandcamp Album'; + } elseif (!is_null($this->getInput('album'))) { + return $this->getInput('album') . ' - Bandcamp Album'; + } + break; + } + + return parent::getName(); + } + + public function detectParameters($url) + { + $params = []; + + // By tag + $regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['tag'] = urldecode($matches[2]); + return $params; + } + + // By band + $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['band'] = urldecode($matches[2]); + return $params; + } + + // By album + $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['band'] = urldecode($matches[2]); + $params['album'] = urldecode($matches[3]); + return $params; + } + + return null; + } } diff --git a/bridges/BandcampDailyBridge.php b/bridges/BandcampDailyBridge.php index 827cf9cd..57299a17 100644 --- a/bridges/BandcampDailyBridge.php +++ b/bridges/BandcampDailyBridge.php @@ -1,159 +1,164 @@ <?php -class BandcampDailyBridge extends BridgeAbstract { - const NAME = 'Bandcamp Daily Bridge'; - const URI = 'https://daily.bandcamp.com'; - const DESCRIPTION = 'Returns newest articles'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - 'Latest articles' => array(), - 'Best of' => array( - 'best-content' => array( - 'name' => 'content', - 'type' => 'list', - 'values' => array( - 'Best Ambient' => 'best-ambient', - 'Best Beat Tapes' => 'best-beat-tapes', - 'Best Dance 12\'s' => 'best-dance-12s', - 'Best Contemporary Classical' => 'best-contemporary-classical', - 'Best Electronic' => 'best-electronic', - 'Best Experimental' => 'best-experimental', - 'Best Hip-Hop' => 'best-hip-hop', - 'Best Jazz' => 'best-jazz', - 'Best Metal' => 'best-metal', - 'Best Punk' => 'best-punk', - 'Best Reissues' => 'best-reissues', - 'Best Soul' => 'best-soul', - ), - 'defaultValue' => 'best-ambient', - ), - ), - 'Genres' => array( - 'genres-content' => array( - 'name' => 'content', - 'type' => 'list', - 'values' => array( - 'Acoustic' => 'genres/acoustic', - 'Alternative' => 'genres/alternative', - 'Ambient' => 'genres/ambient', - 'Blues' => 'genres/blues', - 'Classical' => 'genres/classical', - 'Comedy' => 'genres/comedy', - 'Country' => 'genres/country', - 'Devotional' => 'genres/devotional', - 'Electronic' => 'genres/electronic', - 'Experimental' => 'genres/experimental', - 'Folk' => 'genres/folk', - 'Funk' => 'genres/funk', - 'Hip-Hop/Rap' => 'genres/hip-hop-rap', - 'Jazz' => 'genres/jazz', - 'Kids' => 'genres/kids', - 'Latin' => 'genres/latin', - 'Metal' => 'genres/metal', - 'Pop' => 'genres/pop', - 'Punk' => 'genres/punk', - 'R&B/Soul' => 'genres/r-b-soul', - 'Reggae' => 'genres/reggae', - 'Rock' => 'genres/rock', - 'Soundtrack' => 'genres/soundtrack', - 'Spoken Word' => 'genres/spoken-word', - 'World' => 'genres/world', - ), - 'defaultValue' => 'genres/acoustic', - ), - ), - 'Franchises' => array( - 'franchises-content' => array( - 'name' => 'content', - 'type' => 'list', - 'values' => array( - 'Lists' => 'lists', - 'Features' => 'features', - 'Album of the Day' => 'album-of-the-day', - 'Acid Test' => 'acid-test', - 'Bandcamp Navigator' => 'bandcamp-navigator', - 'Big Ups' => 'big-ups', - 'Certified' => 'certified', - 'Gallery' => 'gallery', - 'Hidden Gems' => 'hidden-gems', - 'High Scores' => 'high-scores', - 'Label Profile' => 'label-profile', - 'Lifetime Achievement' => 'lifetime-achievement', - 'Scene Report' => 'scene-report', - 'Seven Essential Releases' => 'seven-essential-releases', - 'The Merch Table' => 'the-merch-table', - ), - 'defaultValue' => 'lists', - ), - ) - ); - - const CACHE_TIMEOUT = 3600; // 1 hour - - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()) - or returnServerError('Could not request: ' . $this->getURI()); - - $html = defaultLinkTo($html, self::URI); - - $articles = $html->find('articles-list', 0); - - foreach($articles->find('div.list-article') as $index => $article) { - $item = array(); - - $articlePath = $article->find('a.title', 0)->href; - - $articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600) - or returnServerError('Could not request: ' . $articlePath); - - $item['uri'] = $articlePath; - $item['title'] = $articlePageHtml->find('article-title', 0)->innertext; - $item['author'] = $articlePageHtml->find('article-credits > a', 0)->innertext; - $item['content'] = html_entity_decode($articlePageHtml->find('meta[name="description"]', 0)->content, ENT_QUOTES); - $item['timestamp'] = $articlePageHtml->find('meta[property="article:published_time"]', 0)->content; - $item['categories'][] = $articlePageHtml->find('meta[property="article:section"]', 0)->content; - - if ($articlePageHtml->find('meta[property="article:tag"]', 0)) { - $item['categories'][] = $articlePageHtml->find('meta[property="article:tag"]', 0)->content; - } - - $item['enclosures'][] = $articlePageHtml->find('meta[name="twitter:image"]', 0)->content; - - $this->items[] = $item; - - if (count($this->items) >= 10) { - break; - } - } - } - - public function getURI() { - switch($this->queriedContext) { - case 'Latest articles': - return self::URI . '/latest'; - case 'Best of': - case 'Genres': - case 'Franchises': - // TODO Switch to array_key_first once php >= 7.3 - $contentKey = key(self::PARAMETERS[$this->queriedContext]); - return self::URI . '/' . $this->getInput($contentKey); - default: - return parent::getURI(); - } - } - - public function getName() { - switch($this->queriedContext) { - case 'Latest articles': - return $this->queriedContext . ' - Bandcamp Daily'; - case 'Best of': - case 'Genres': - case 'Franchises': - $contentKey = array_key_first(self::PARAMETERS[$this->queriedContext]); - $contentValues = array_flip(self::PARAMETERS[$this->queriedContext][$contentKey]['values']); - - return $contentValues[$this->getInput($contentKey)] . ' - Bandcamp Daily'; - default: - return parent::getName(); - } - } + +class BandcampDailyBridge extends BridgeAbstract +{ + const NAME = 'Bandcamp Daily Bridge'; + const URI = 'https://daily.bandcamp.com'; + const DESCRIPTION = 'Returns newest articles'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + 'Latest articles' => [], + 'Best of' => [ + 'best-content' => [ + 'name' => 'content', + 'type' => 'list', + 'values' => [ + 'Best Ambient' => 'best-ambient', + 'Best Beat Tapes' => 'best-beat-tapes', + 'Best Dance 12\'s' => 'best-dance-12s', + 'Best Contemporary Classical' => 'best-contemporary-classical', + 'Best Electronic' => 'best-electronic', + 'Best Experimental' => 'best-experimental', + 'Best Hip-Hop' => 'best-hip-hop', + 'Best Jazz' => 'best-jazz', + 'Best Metal' => 'best-metal', + 'Best Punk' => 'best-punk', + 'Best Reissues' => 'best-reissues', + 'Best Soul' => 'best-soul', + ], + 'defaultValue' => 'best-ambient', + ], + ], + 'Genres' => [ + 'genres-content' => [ + 'name' => 'content', + 'type' => 'list', + 'values' => [ + 'Acoustic' => 'genres/acoustic', + 'Alternative' => 'genres/alternative', + 'Ambient' => 'genres/ambient', + 'Blues' => 'genres/blues', + 'Classical' => 'genres/classical', + 'Comedy' => 'genres/comedy', + 'Country' => 'genres/country', + 'Devotional' => 'genres/devotional', + 'Electronic' => 'genres/electronic', + 'Experimental' => 'genres/experimental', + 'Folk' => 'genres/folk', + 'Funk' => 'genres/funk', + 'Hip-Hop/Rap' => 'genres/hip-hop-rap', + 'Jazz' => 'genres/jazz', + 'Kids' => 'genres/kids', + 'Latin' => 'genres/latin', + 'Metal' => 'genres/metal', + 'Pop' => 'genres/pop', + 'Punk' => 'genres/punk', + 'R&B/Soul' => 'genres/r-b-soul', + 'Reggae' => 'genres/reggae', + 'Rock' => 'genres/rock', + 'Soundtrack' => 'genres/soundtrack', + 'Spoken Word' => 'genres/spoken-word', + 'World' => 'genres/world', + ], + 'defaultValue' => 'genres/acoustic', + ], + ], + 'Franchises' => [ + 'franchises-content' => [ + 'name' => 'content', + 'type' => 'list', + 'values' => [ + 'Lists' => 'lists', + 'Features' => 'features', + 'Album of the Day' => 'album-of-the-day', + 'Acid Test' => 'acid-test', + 'Bandcamp Navigator' => 'bandcamp-navigator', + 'Big Ups' => 'big-ups', + 'Certified' => 'certified', + 'Gallery' => 'gallery', + 'Hidden Gems' => 'hidden-gems', + 'High Scores' => 'high-scores', + 'Label Profile' => 'label-profile', + 'Lifetime Achievement' => 'lifetime-achievement', + 'Scene Report' => 'scene-report', + 'Seven Essential Releases' => 'seven-essential-releases', + 'The Merch Table' => 'the-merch-table', + ], + 'defaultValue' => 'lists', + ], + ] + ]; + + const CACHE_TIMEOUT = 3600; // 1 hour + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()) + or returnServerError('Could not request: ' . $this->getURI()); + + $html = defaultLinkTo($html, self::URI); + + $articles = $html->find('articles-list', 0); + + foreach ($articles->find('div.list-article') as $index => $article) { + $item = []; + + $articlePath = $article->find('a.title', 0)->href; + + $articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600) + or returnServerError('Could not request: ' . $articlePath); + + $item['uri'] = $articlePath; + $item['title'] = $articlePageHtml->find('article-title', 0)->innertext; + $item['author'] = $articlePageHtml->find('article-credits > a', 0)->innertext; + $item['content'] = html_entity_decode($articlePageHtml->find('meta[name="description"]', 0)->content, ENT_QUOTES); + $item['timestamp'] = $articlePageHtml->find('meta[property="article:published_time"]', 0)->content; + $item['categories'][] = $articlePageHtml->find('meta[property="article:section"]', 0)->content; + + if ($articlePageHtml->find('meta[property="article:tag"]', 0)) { + $item['categories'][] = $articlePageHtml->find('meta[property="article:tag"]', 0)->content; + } + + $item['enclosures'][] = $articlePageHtml->find('meta[name="twitter:image"]', 0)->content; + + $this->items[] = $item; + + if (count($this->items) >= 10) { + break; + } + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Latest articles': + return self::URI . '/latest'; + case 'Best of': + case 'Genres': + case 'Franchises': + // TODO Switch to array_key_first once php >= 7.3 + $contentKey = key(self::PARAMETERS[$this->queriedContext]); + return self::URI . '/' . $this->getInput($contentKey); + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Latest articles': + return $this->queriedContext . ' - Bandcamp Daily'; + case 'Best of': + case 'Genres': + case 'Franchises': + $contentKey = array_key_first(self::PARAMETERS[$this->queriedContext]); + $contentValues = array_flip(self::PARAMETERS[$this->queriedContext][$contentKey]['values']); + + return $contentValues[$this->getInput($contentKey)] . ' - Bandcamp Daily'; + default: + return parent::getName(); + } + } } diff --git a/bridges/BastaBridge.php b/bridges/BastaBridge.php index b8174c60..4c0df273 100644 --- a/bridges/BastaBridge.php +++ b/bridges/BastaBridge.php @@ -1,31 +1,33 @@ <?php -class BastaBridge extends BridgeAbstract { - const MAINTAINER = 'qwertygc'; - const NAME = 'Bastamag Bridge'; - const URI = 'https://www.bastamag.net/'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Returns the newest articles.'; +class BastaBridge extends BridgeAbstract +{ + const MAINTAINER = 'qwertygc'; + const NAME = 'Bastamag Bridge'; + const URI = 'https://www.bastamag.net/'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Returns the newest articles.'; - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend'); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend'); - $limit = 0; + $limit = 0; - foreach($html->find('item') as $element) { - if($limit < 10) { - $item = array(); - $item['title'] = $element->find('title', 0)->innertext; - $item['uri'] = $element->find('guid', 0)->plaintext; - $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext); + foreach ($html->find('item') as $element) { + if ($limit < 10) { + $item = []; + $item['title'] = $element->find('title', 0)->innertext; + $item['uri'] = $element->find('guid', 0)->plaintext; + $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext); - $html = getSimpleHTMLDOM($item['uri']); - $html = defaultLinkTo($html, self::URI); + $html = getSimpleHTMLDOM($item['uri']); + $html = defaultLinkTo($html, self::URI); - $item['content'] = $html->find('div.texte', 0)->innertext; - $this->items[] = $item; - $limit++; - } - } - } + $item['content'] = $html->find('div.texte', 0)->innertext; + $this->items[] = $item; + $limit++; + } + } + } } diff --git a/bridges/BinanceBridge.php b/bridges/BinanceBridge.php index 573a2172..73dbf0b9 100644 --- a/bridges/BinanceBridge.php +++ b/bridges/BinanceBridge.php @@ -1,41 +1,45 @@ <?php -class BinanceBridge extends BridgeAbstract { - const NAME = 'Binance Blog'; - const URI = 'https://www.binance.com/en/blog'; - const DESCRIPTION = 'Subscribe to the Binance blog.'; - const MAINTAINER = 'thefranke'; - const CACHE_TIMEOUT = 3600; // 1h - - public function getIcon() { - return 'https://bin.bnbstatic.com/static/images/common/favicon.ico'; - } - - public function collectData() { - $html = getSimpleHTMLDOM(self::URI) - or returnServerError('Could not fetch Binance blog data.'); - - $appData = $html->find('script[id="__APP_DATA"]'); - $appDataJson = json_decode($appData[0]->innertext); - - foreach($appDataJson->pageData->redux->blogList->blogList as $element) { - - $date = $element->postTime; - $abstract = $element->brief; - $uri = self::URI . '/' . $element->lang . '/blog/' . $element->idStr; - $title = $element->title; - $content = $element->content; - - $item = array(); - $item['title'] = $title; - $item['uri'] = $uri; - $item['timestamp'] = substr($date, 0, -3); - $item['author'] = 'Binance'; - $item['content'] = $content; - - $this->items[] = $item; - - if (count($this->items) >= 10) - break; - } - } + +class BinanceBridge extends BridgeAbstract +{ + const NAME = 'Binance Blog'; + const URI = 'https://www.binance.com/en/blog'; + const DESCRIPTION = 'Subscribe to the Binance blog.'; + const MAINTAINER = 'thefranke'; + const CACHE_TIMEOUT = 3600; // 1h + + public function getIcon() + { + return 'https://bin.bnbstatic.com/static/images/common/favicon.ico'; + } + + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI) + or returnServerError('Could not fetch Binance blog data.'); + + $appData = $html->find('script[id="__APP_DATA"]'); + $appDataJson = json_decode($appData[0]->innertext); + + foreach ($appDataJson->pageData->redux->blogList->blogList as $element) { + $date = $element->postTime; + $abstract = $element->brief; + $uri = self::URI . '/' . $element->lang . '/blog/' . $element->idStr; + $title = $element->title; + $content = $element->content; + + $item = []; + $item['title'] = $title; + $item['uri'] = $uri; + $item['timestamp'] = substr($date, 0, -3); + $item['author'] = 'Binance'; + $item['content'] = $content; + + $this->items[] = $item; + + if (count($this->items) >= 10) { + break; + } + } + } } diff --git a/bridges/BlaguesDeMerdeBridge.php b/bridges/BlaguesDeMerdeBridge.php index 9b776407..cc0f485e 100644 --- a/bridges/BlaguesDeMerdeBridge.php +++ b/bridges/BlaguesDeMerdeBridge.php @@ -1,45 +1,44 @@ <?php -class BlaguesDeMerdeBridge extends BridgeAbstract { - const MAINTAINER = 'superbaillot.net, logmanoriginal'; - const NAME = 'Blagues De Merde'; - const URI = 'http://www.blaguesdemerde.fr/'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Blagues De Merde'; +class BlaguesDeMerdeBridge extends BridgeAbstract +{ + const MAINTAINER = 'superbaillot.net, logmanoriginal'; + const NAME = 'Blagues De Merde'; + const URI = 'http://www.blaguesdemerde.fr/'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Blagues De Merde'; - public function getIcon() { - return self::URI . 'assets/img/favicon.ico'; - } + public function getIcon() + { + return self::URI . 'assets/img/favicon.ico'; + } - public function collectData(){ + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $html = getSimpleHTMLDOM(self::URI); + foreach ($html->find('div.blague') as $element) { + $item = []; - foreach($html->find('div.blague') as $element) { + $item['uri'] = static::URI . '#' . $element->id; + $item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext; - $item = array(); + // Let the title be everything up to the first <br> + $item['title'] = trim(explode("\n", $element->find('div.text', 0)->plaintext)[0]); - $item['uri'] = static::URI . '#' . $element->id; - $item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext; + $item['content'] = strip_tags($element->find('div.text', 0)); - // Let the title be everything up to the first <br> - $item['title'] = trim(explode("\n", $element->find('div.text', 0)->plaintext)[0]); + // timestamp is part of: + // <p>Par <strong>{author}</strong> le {date} dans <strong>{category}</strong></p> + preg_match( + '/.+le(.+)dans.*/', + $element->find('div[class="blague-footer"]', 0)->plaintext, + $matches + ); - $item['content'] = strip_tags($element->find('div.text', 0)); + $item['timestamp'] = strtotime($matches[1]); - // timestamp is part of: - // <p>Par <strong>{author}</strong> le {date} dans <strong>{category}</strong></p> - preg_match( - '/.+le(.+)dans.*/', - $element->find('div[class="blague-footer"]', 0)->plaintext, - $matches - ); - - $item['timestamp'] = strtotime($matches[1]); - - $this->items[] = $item; - - } - - } + $this->items[] = $item; + } + } } diff --git a/bridges/BleepingComputerBridge.php b/bridges/BleepingComputerBridge.php index 78ec3125..c1d3d568 100644 --- a/bridges/BleepingComputerBridge.php +++ b/bridges/BleepingComputerBridge.php @@ -1,29 +1,32 @@ <?php -class BleepingComputerBridge extends FeedExpander { - const MAINTAINER = 'csisoap'; - const NAME = 'Bleeping Computer'; - const URI = 'https://www.bleepingcomputer.com/'; - const DESCRIPTION = 'Returns the newest articles.'; +class BleepingComputerBridge extends FeedExpander +{ + const MAINTAINER = 'csisoap'; + const NAME = 'Bleeping Computer'; + const URI = 'https://www.bleepingcomputer.com/'; + const DESCRIPTION = 'Returns the newest articles.'; - protected function parseItem($item){ - $item = parent::parseItem($item); + protected function parseItem($item) + { + $item = parent::parseItem($item); - $article_html = getSimpleHTMLDOMCached($item['uri']); - if(!$article_html) { - $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; - return $item; - } + $article_html = getSimpleHTMLDOMCached($item['uri']); + if (!$article_html) { + $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; + return $item; + } - $article_content = $article_html->find('div.articleBody', 0)->innertext; - $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="cz-related-article-wrapp'); - $item['content'] = trim($article_content); + $article_content = $article_html->find('div.articleBody', 0)->innertext; + $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="cz-related-article-wrapp'); + $item['content'] = trim($article_content); - return $item; - } + return $item; + } - public function collectData(){ - $feed = static::URI . 'feed/'; - $this->collectExpandableDatas($feed); - } + public function collectData() + { + $feed = static::URI . 'feed/'; + $this->collectExpandableDatas($feed); + } } diff --git a/bridges/BlizzardNewsBridge.php b/bridges/BlizzardNewsBridge.php index 156dc290..3930e0a4 100644 --- a/bridges/BlizzardNewsBridge.php +++ b/bridges/BlizzardNewsBridge.php @@ -1,60 +1,60 @@ <?php -class BlizzardNewsBridge extends XPathAbstract { +class BlizzardNewsBridge extends XPathAbstract +{ + const NAME = 'Blizzard News'; + const URI = 'https://news.blizzard.com'; + const DESCRIPTION = 'Blizzard (game company) newsfeed'; + const MAINTAINER = 'Niehztog'; + const PARAMETERS = [ + '' => [ + 'locale' => [ + 'name' => 'Language', + 'type' => 'list', + 'values' => [ + 'Deutsch' => 'de-de', + 'English (EU)' => 'en-gb', + 'English (US)' => 'en-us', + 'Español (EU)' => 'es-es', + 'Español (AL)' => 'es-mx', + 'Français' => 'fr-fr', + 'Italiano' => 'it-it', + '日本語' => 'ja-jp', + '한국어' => 'ko-kr', + 'Polski' => 'pl-pl', + 'Português (AL)' => 'pt-br', + 'Русский' => 'ru-ru', + 'ภาษาไทย' => 'th-th', + '简体中文' => 'zh-cn', + '繁體中文' => 'zh-tw' + ], + 'defaultValue' => 'en-us', + 'title' => 'Select your language' + ] + ] + ]; + const CACHE_TIMEOUT = 3600; - const NAME = 'Blizzard News'; - const URI = 'https://news.blizzard.com'; - const DESCRIPTION = 'Blizzard (game company) newsfeed'; - const MAINTAINER = 'Niehztog'; - const PARAMETERS = array( - '' => array( - 'locale' => array( - 'name' => 'Language', - 'type' => 'list', - 'values' => array( - 'Deutsch' => 'de-de', - 'English (EU)' => 'en-gb', - 'English (US)' => 'en-us', - 'Español (EU)' => 'es-es', - 'Español (AL)' => 'es-mx', - 'Français' => 'fr-fr', - 'Italiano' => 'it-it', - '日本語' => 'ja-jp', - '한국어' => 'ko-kr', - 'Polski' => 'pl-pl', - 'Português (AL)' => 'pt-br', - 'Русский' => 'ru-ru', - 'ภาษาไทย' => 'th-th', - '简体中文' => 'zh-cn', - '繁體中文' => 'zh-tw' - ), - 'defaultValue' => 'en-us', - 'title' => 'Select your language' - ) - ) - ); - const CACHE_TIMEOUT = 3600; + const XPATH_EXPRESSION_ITEM = '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article'; + const XPATH_EXPRESSION_ITEM_TITLE = './/div/div[2]/h2'; + const XPATH_EXPRESSION_ITEM_CONTENT = './/div[@class="ArticleListItem-description"]/div[@class="h6"]'; + const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ArticleLink ArticleLink"]/@href'; + const XPATH_EXPRESSION_ITEM_AUTHOR = ''; + const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp'; + const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/div[@class="ArticleListItem-image"]/@style'; + const XPATH_EXPRESSION_ITEM_CATEGORIES = './/div[@class="ArticleListItem-label"]'; + const SETTING_FIX_ENCODING = true; - const XPATH_EXPRESSION_ITEM = '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article'; - const XPATH_EXPRESSION_ITEM_TITLE = './/div/div[2]/h2'; - const XPATH_EXPRESSION_ITEM_CONTENT = './/div[@class="ArticleListItem-description"]/div[@class="h6"]'; - const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ArticleLink ArticleLink"]/@href'; - const XPATH_EXPRESSION_ITEM_AUTHOR = ''; - const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp'; - const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/div[@class="ArticleListItem-image"]/@style'; - const XPATH_EXPRESSION_ITEM_CATEGORIES = './/div[@class="ArticleListItem-label"]'; - const SETTING_FIX_ENCODING = true; - - /** - * Source Web page URL (should provide either HTML or XML content) - * @return string - */ - protected function getSourceUrl(){ - - $locale = $this->getInput('locale'); - if('zh-cn' === $locale) { - return 'https://cn.news.blizzard.com'; - } - return 'https://news.blizzard.com/' . $locale; - } + /** + * Source Web page URL (should provide either HTML or XML content) + * @return string + */ + protected function getSourceUrl() + { + $locale = $this->getInput('locale'); + if ('zh-cn' === $locale) { + return 'https://cn.news.blizzard.com'; + } + return 'https://news.blizzard.com/' . $locale; + } } diff --git a/bridges/BookMyShowBridge.php b/bridges/BookMyShowBridge.php index 342085b4..7064df91 100644 --- a/bridges/BookMyShowBridge.php +++ b/bridges/BookMyShowBridge.php @@ -1,1288 +1,1298 @@ <?php -class BookMyShowBridge extends BridgeAbstract { - - const MAINTAINER = 'captn3m0'; - const NAME = 'BookMyShow Bridge'; - const URI = 'https://in.bookmyshow.com'; - const MOVIES_IMAGE_BASE_FORMAT = 'https://in.bmscdn.com/iedb/movies/images/mobile/thumbnail/large/%s.jpg'; - const DESCRIPTION = 'Returns the latest events on BookMyShow'; - - const TIMEZONE = 'Asia/Kolkata'; - - const PLAYS = 'PL'; - const EVENTS = 'CT'; - const MOVIES = 'MT'; - - const CATEGORIES = [ - self::PLAYS => 'Plays', - self::EVENTS => 'Events', - self::MOVIES => 'Movies', - ]; - - const CITIES = [ - // Most popular cities - 'Mumbai' => 'MUMBAI', - 'National Capital Region (NCR)' => 'NCR', - 'Bengaluru' => 'BANG', - 'Hyderabad' => 'HYD', - 'Ahmedabad' => 'AHD', - 'Chandigarh' => 'CHD', - 'Chennai' => 'CHEN', - 'Pune' => 'PUNE', - 'Kolkata' => 'KOLK', - 'Kochi' => 'KOCH', - - // Less common cities - 'Aalo' => 'AALU', - 'Abohar' => 'ABOR', - 'Abu Road' => 'ABRD', - 'Acharapakkam' => 'ACHA', - 'Adilabad' => 'ADIL', - 'Agar Malwa' => 'AGOR', - 'Agartala' => 'AGAR', - 'Agra' => 'AGRA', - 'Ahmedgarh' => 'AHMG', - 'Ahmednagar' => 'AHMED', - 'Aizawl' => 'AIZW', - 'Ajmer' => 'AJMER', - 'Akaltara' => 'AKAL', - 'Akividu' => 'AKVD', - 'Akola' => 'AKOL', - 'Alangudi' => 'ALNI', - 'Alappuzha' => 'ALPZ', - 'Alathur' => 'ALAR', - 'Alibaug' => 'ALBG', - 'Aligarh' => 'ALI', - 'Allagadda' => 'ALGD', - 'Almora' => 'ALMO', - 'Alwar' => 'ALWR', - 'Amadalavalasa' => 'ADAM', - 'Amalapuram' => 'AMAP', - 'Amaravathi' => 'AVTI', - 'Ambala' => 'AMB', - 'Ambikapur' => 'AMBI', - 'Ambur' => 'AMBR', - 'Amgaon' => 'AMGN', - 'Amravati' => 'AMRA', - 'Amritsar' => 'AMRI', - 'Anakapalle' => 'ANKP', - 'Anand' => 'AND', - 'Anantapalli' => 'ANTT', - 'Anantapur' => 'ANAN', - 'Anchal' => 'ANHL', - 'Angadipuram' => 'ANDM', - 'Angamaly' => 'ANGA', - 'Angara' => 'ANGR', - 'Angul' => 'ANGL', - 'Anjad' => 'ANJA', - 'Anjar' => 'ANJR', - 'Anklav' => 'ANKV', - 'Ankleshwar' => 'ANKL', - 'Annigeri' => 'ANGI', - 'Arakkonam' => 'ARAK', - 'Arambagh' => 'AMBH', - 'Aranthangi' => 'ARNT', - 'Ariyalur' => 'ARIY', - 'Arni' => 'ARNI', - 'Arsikere' => 'ARSI', - 'Aruppukottai' => 'ARUP', - 'Asansol' => 'ASANSOL', - 'Ashoknagar (West Bengal)' => 'ASNA', - 'Ashoknagar' => 'AKMP', - 'Aswaraopeta' => 'ASWA', - 'Atpadi' => 'ATPA', - 'Attili' => 'ATLI', - 'Aurangabad (Bihar)' => 'AUBI', - 'Aurangabad (West Bengal)' => 'AURW', - 'Aurangabad' => 'AURA', - 'Avinashi' => 'AVII', - 'Azamgarh' => 'AZMG', - 'B. Kothakota' => 'BKOT', - 'Badaun' => 'BADN', - 'Baddi' => 'BADD', - 'Badnawar' => 'BADR', - 'Bagbahara' => 'BBHA', - 'Bagha Purana' => 'BAPU', - 'Bagru' => 'BAGU', - 'Bahadurgarh' => 'BAHD', - 'Bahraich' => 'BHRH', - 'Baihar' => 'BIAH', - 'Baikunthpur' => 'BKTH', - 'Baindur' => 'BAND', - 'Bakhrahat' => 'BART', - 'Balaghat' => 'BLGT', - 'Balangir' => 'BALG', - 'Balasore' => 'BLSR', - 'Balijipeta' => 'BLIJ', - 'Balod' => 'BALD', - 'Baloda Bazar' => 'BBCH', - 'Balotra' => 'BALO', - 'Balrampur' => 'BLUR', - 'Balurghat' => 'BALU', - 'Bangarpet' => 'BAGT', - 'Banswada' => 'BNSA', - 'Banswara' => 'BANS', - 'Bantumilli' => 'BANT', - 'Barabanki' => 'BARK', - 'Baramati' => 'BARA', - 'Baraut' => 'BARL', - 'Bardoli' => 'BRDL', - 'Bareilly' => 'BARE', - 'Bargarh' => 'BARG', - 'Baripada' => 'BARI', - 'Barmer' => 'BARM', - 'Barnala' => 'BAR', - 'Barshi' => 'BRHI', - 'Barwani' => 'BRWN', - 'Basna' => 'BASN', - 'Basti' => 'BAST', - 'Bathinda' => 'BHAT', - 'Batlagundu' => 'BTGD', - 'Beawar' => 'BEAW', - 'Beed' => 'BEED', - 'Belagavi (Belgaum)' => 'BELG', - 'Bellampalli' => 'BELL', - 'Bellary' => 'BLRY', - 'Belur' => 'BELU', - 'Bemetara' => 'BMTA', - 'Berachampa' => 'BRAC', - 'Berhampore' => 'BEHA', - 'Berhampur' => 'BERP', - 'Bestavaripeta' => 'BEST', - 'Betul' => 'BETU', - 'Bhadrachalam' => 'BHDR', - 'Bhadrak' => 'BHAD', - 'Bhadravati' => 'BDVT', - 'Bhainsa' => 'BHAN', - 'Bhandara' => 'BHAA', - 'Bharamasagara' => 'BASA', - 'Bharuch' => 'BHAR', - 'Bhatapara' => 'BTAP', - 'Bhatkal' => 'BAKL', - 'Bhattiprolu' => 'BATT', - 'Bhavnagar' => 'BHNG', - 'Bhilai' => 'BHILAI', - 'Bhilwara' => 'BHIL', - 'Bhimadole' => 'BMDE', - 'Bhimavaram' => 'BHIM', - 'Bhiwadi' => 'BHWD', - 'Bhiwani' => 'BHWN', - 'Bhopal' => 'BHOP', - 'Bhubaneswar' => 'BHUB', - 'Bhuj' => 'BHUJ', - 'Bhuntar' => 'BHUN', - 'Bhupalpalle' => 'BHUP', - 'Bhusawal' => 'BHUS', - 'Biaora' => 'BIAR', - 'Bidar' => 'BIDR', - 'Bijnor' => 'BIJ', - 'Bijoynagar' => 'BIJO', - 'Bikaner' => 'BIK', - 'Bilara' => 'BILR', - 'Bilaspur (Himachal Pradesh)' => 'BIPS', - 'Bilaspur' => 'BILA', - 'Bilimora' => 'BILI', - 'Biraul' => 'BIRL', - 'Bishrampur' => 'BSRM', - 'Bodinayakanur' => 'BODI', - 'Boisar' => 'BOIS', - 'Bokaro' => 'BOKA', - 'Bolpur' => 'BLPR', - 'Bommidi' => 'BOMM', - 'Bongaigaon' => 'BONG', - 'Bongaon' => 'BONI', - 'Borsad' => 'BORM', - 'Brahmapur' => 'KHUB', - 'Brahmapuri' => 'BHMP', - 'Brajrajnagar' => 'BJNG', - 'Bulandshahr' => 'BULA', - 'Buldana' => 'BULD', - 'Bundu' => 'BUND', - 'Burdwan' => 'BURD', - 'Burhanpur' => 'BRHP', - 'Byadagi' => 'BYAD', - 'Chagallu' => 'CHAG', - 'Challakere' => 'CHLA', - 'Challapalli' => 'CHAP', - 'Champa' => 'CHAM', - 'Chanchal' => 'CCWC', - 'Chandausi' => 'CHDN', - 'Chandragiri' => 'CHAD', - 'Chandrakona' => 'CKNA', - 'Chandrapur' => 'CHAN', - 'Changanassery' => 'CNSY', - 'Channagiri' => 'CHGI', - 'Channarayapatna' => 'CHNN', - 'Chaygaon' => 'CHOG', - 'Cheepurupalli' => 'CHEE', - 'Chendrapinni' => 'CNPI', - 'Chengannur' => 'CHEG', - 'Chennur' => 'CHNU', - 'Cherial' => 'CHRY', - 'Cheyyar' => 'CHEY', - 'Chhibramau' => 'CHHI', - 'Chhindwara' => 'CHIN', - 'Chickmagaluru' => 'CHKA', - 'Chidambaram' => 'CHID', - 'Chikkaballapur' => 'CHIK', - 'Chikodi' => 'CHOK', - 'Chinturu' => 'CHTN', - 'Chirala' => 'CHIR', - 'Chitradurga' => 'CHIT', - 'Chittoor' => 'CHTT', - 'Chodavaram' => 'CDVM', - 'Chotila' => 'CHOT', - 'Coimbatore' => 'COIM', - 'Cooch Behar' => 'COBE', - 'Cuddalore' => 'CUDD', - 'Cuttack' => 'CUTT', - 'Dabra' => 'DABR', - 'Dahanu' => 'DHAU', - 'Dahegam' => 'DHGM', - 'Dahod' => 'DAHO', - 'Dakshin Barasat' => 'DAKS', - 'Dalli Rajhara' => 'DALL', - 'Daman' => 'DAMA', - 'Damoh' => 'DAMO', - 'Darjeeling' => 'DARJ', - 'Darsi' => 'DARS', - 'Dasuya' => 'DASU', - 'Dausa' => 'DAUS', - 'Davanagere' => 'DAVA', - 'Davuluru' => 'DVLR', - 'Deesa' => 'DEES', - 'Dehradun' => 'DEH', - 'Deoghar' => 'DOGH', - 'Devadurga' => 'DEVD', - 'Devarakonda' => 'DEVK', - 'Devgad' => 'DEGA', - 'Dewas' => 'DEWAS', - 'Dhampur' => 'DHPR', - 'Dhamtari' => 'DHMT', - 'Dhanbad' => 'DHAN', - 'Dhar' => 'DARH', - 'Dharamsala' => 'DMSL', - 'Dharapuram' => 'DHAR', - 'Dharmapuri' => 'DMPI', - 'Dharmavaram' => 'DDMA', - 'Dharwad' => 'DHAW', - 'Dhenkanal' => 'DNAL', - 'Dhoraji' => 'DHOR', - 'Dhule' => 'DHLE', - 'Dhuri' => 'DHRI', - 'Dibrugarh' => 'DIB', - 'Digras' => 'DIGR', - 'Dimapur' => 'DMPR', - 'Dindigul' => 'DIND', - 'Doddaballapura' => 'DDBP', - 'Domkal' => 'DMKL', - 'Dongargarh' => 'DONG', - 'Doraha' => 'DORH', - 'Durg' => 'DURG', - 'Durgapur' => 'DURGA', - 'Edappal' => 'EDPL', - 'Edlapadu' => 'EDLP', - 'Eluru' => 'ELRU', - 'Erattupetta' => 'ERAT', - 'Ernakulam' => 'ERNK', - 'Erode' => 'EROD', - 'Etawah' => 'ETWH', - 'Ettumanoor' => 'ETTU', - 'Faizabad' => 'FAZA', - 'Falna' => 'FALN', - 'Faridkot' => 'DKOT', - 'Fatehgarh Sahib' => 'FASA', - 'Fatehpur' => 'FATE', - 'Fatehpur(Rajasthan)' => 'FATR', - 'Firozpur' => 'FRZR', - 'G.Mamidada' => 'GMAD', - 'Gadag' => 'GADG', - 'Gadarwara' => 'GDWR', - 'Gadchiroli' => 'GDRO', - 'Gajendragarh' => 'GJGH', - 'Gajwel' => 'GAJW', - 'Ganapavaram' => 'GANP', - 'Gandhidham' => 'GDHAM', - 'Gandhinagar' => 'GNAGAR', - 'Gangavati' => 'GAVT', - 'Gangoh' => 'GANZ', - 'Gangtok' => 'GANG', - 'Ganjbasoda' => 'GANJ', - 'Garla' => 'GALA', - 'Gauribidanur' => 'GAUR', - 'Gaya' => 'GAYA', - 'Gingee' => 'GING', - 'Goa' => 'GOA', - 'Gobichettipalayam' => 'GOBI', - 'Godavarikhani' => 'GDVK', - 'Godhra' => 'GODH', - 'Gokak' => 'GKGK', - 'Gokavaram' => 'GOKM', - 'Golaghat' => 'GHT', - 'Gollaprolu' => 'GOLL', - 'Gonda' => 'GOND', - 'Gondia' => 'GNDA', - 'Gopalganj' => 'GOPG', - 'Gorakhpur' => 'GRKP', - 'Gorantla' => 'GORA', - 'Gotegaon' => 'GTGN', - 'Gownipalli' => 'GOWP', - 'Gudivada' => 'GUDI', - 'Gudiyatham' => 'GDTM', - 'Gudur' => 'GUDR', - 'Gulaothi' => 'GULL', - 'Guledgudda' => 'GULD', - 'Gummadidala' => 'GUMM', - 'Guna' => 'GUNA', - 'Guntakal' => 'GUNL', - 'Guntur' => 'GUNT', - 'Gurazala' => 'GURZ', - 'Guwahati' => 'GUW', - 'Gwalior' => 'GWAL', - 'Habra' => 'HARR', - 'Hagaribommanahalli' => 'HHGG', - 'Hajipur' => 'HAJI', - 'Haldia' => 'HLDI', - 'Haldwani' => 'HALD', - 'Haliya' => 'HALI', - 'Hampi' => 'HMPI', - 'Hardoi' => 'HRDI', - 'Haridwar' => 'HRDR', - 'Harihar' => 'HRRR', - 'Haripad' => 'HRPD', - 'Harugeri' => 'HARU', - 'Hasanpur' => 'HANS', - 'Hazaribagh' => 'HAZA', - 'Himmatnagar' => 'HIMM', - 'Hindaun City' => 'HIND', - 'Hisar' => 'HISR', - 'Honnali' => 'HONV', - 'Honnavara' => 'HNVR', - 'Hooghly' => 'HOOG', - 'Hoshiarpur' => 'HOSH', - 'Hoskote' => 'HOKT', - 'Hospet' => 'HOSP', - 'Hosur' => 'HSUR', - 'Howrah' => 'HWRH', - 'Hubballi (Hubli)' => 'HUBL', - 'Huvinahadagali' => 'HULI', - 'Ichalkaranji' => 'ICHL', - 'Ichchapuram' => 'ICPR', - 'Idappadi' => 'IDPI', - 'Idar' => 'IDAR', - 'Indapur' => 'INDA', - 'Indi' => 'IIND', - 'Indore' => 'IND', - 'Irinjalakuda' => 'IRNK', - 'Itanagar' => 'ITNG', - 'Itarsi' => 'ITAR', - 'Jabalpur' => 'JABL', - 'Jadcherla' => 'JADC', - 'Jagalur' => 'JAGA', - 'Jagatdal' => 'JGDL', - 'Jagdalpur' => 'JAGD', - 'Jaggampeta' => 'JAGG', - 'Jaggayyapeta' => 'JGGY', - 'Jagtial' => 'JGTL', - 'Jaipur' => 'JAIP', - 'Jaisalmer' => 'JSMR', - 'Jajpur Road' => 'JAJP', - 'Jalakandapuram' => 'JAKA', - 'Jalalabad' => 'JLAB', - 'Jalandhar' => 'JALA', - 'Jalgaon' => 'JALG', - 'Jalna' => 'JALN', - 'Jalpaiguri' => 'JPG', - 'Jami' => 'JAMI', - 'Jamkhed' => 'JAMK', - 'Jammalamadugu' => 'JAMD', - 'Jammu' => 'JAMM', - 'Jamnagar' => 'JAM', - 'Jamner' => 'JAMN', - 'Jamshedpur' => 'JMDP', - 'Jangaon' => 'JNGN', - 'Jangareddy Gudem' => 'JANG', - 'Janjgir' => 'JANR', - 'Jasdan' => 'JASD', - 'Jaunpur' => 'JANP', - 'Jehanabad' => 'JEHA', - 'Jetpur' => 'JETP', - 'Jewar' => 'JEWR', - 'Jeypore' => 'JEYP', - 'Jhabua' => 'JHAB', - 'Jhajjar' => 'JHAJ', - 'Jhansi' => 'JNSI', - 'Jharsuguda' => 'JRSG', - 'Jiaganj' => 'JAGJ', - 'Jind' => 'JIND', - 'Jodhpur' => 'JODH', - 'Jorhat' => 'JORT', - 'Junagadh' => 'JUGH', - 'Kadapa' => 'KDPA', - 'Kadi' => 'KADI', - 'Kaikaluru' => 'KAIK', - 'Kaithal' => 'KAIT', - 'Kakarapalli' => 'KAAP', - 'Kakinada' => 'KAKI', - 'Kalaburagi (Gulbarga)' => 'GULB', - 'Kalimpong' => 'KALI', - 'Kallakurichi' => 'KALL', - 'Kalol (Panchmahal)' => 'PANH', - 'Kalwakurthy' => 'KALW', - 'Kalyani' => 'KALY', - 'Kamanaickenpalayam' => 'KPLA', - 'Kamareddy' => 'KMRD', - 'Kamavarapukota' => 'KPKT', - 'Kambainallur' => 'KAMR', - 'Kamptee' => 'KAMP', - 'Kanakapura' => 'KAKP', - 'Kanchikacherla' => 'KNCH', - 'Kanchipuram' => 'KNPM', - 'Kandukur' => 'KAND', - 'Kangayam' => 'KGKM', - 'Kangra' => 'KANG', - 'Kanichar' => 'KANC', - 'Kanigiri' => 'KANI', - 'Kanipakam' => 'KAAM', - 'Kanjirappally' => 'KNNJ', - 'Kanker' => 'KANK', - 'Kannauj' => 'KANJ', - 'Kannur' => 'KANN', - 'Kanpur' => 'KANP', - 'Kanyakumari' => 'KAKM', - 'Karad' => 'KARD', - 'Karaikal' => 'KARA', - 'Karanja Lad' => 'KLAD', - 'Kareli' => 'KARE', - 'Karimangalam' => 'KARI', - 'Karimganj' => 'KRNJ', - 'Karimnagar' => 'KARIM', - 'Karjat' => 'KART', - 'Karkala' => 'KARK', - 'Karnal' => 'KARN', - 'Karunagapally' => 'KARG', - 'Karur' => 'KARU', - 'Karwar' => 'KWAR', - 'Kasdol' => 'KASD', - 'Kasgunj' => 'KASG', - 'Kashipur' => 'KASH', - 'Kasibugga' => 'KSBG', - 'Kathipudi' => 'KATP', - 'Kathua' => 'KATH', - 'Katihar' => 'KATI', - 'Kattappana' => 'AWCK', - 'Kaveripattinam' => 'KANM', - 'Kekri' => 'KEKR', - 'Keonjhar' => 'KNJH', - 'Kesinga' => 'KEGA', - 'Khachrod' => 'KHCU', - 'Khajipet' => 'KHAJ', - 'Khalilabad' => 'KHBD', - 'Khamgaon' => 'KHMG', - 'Khammam' => 'KHAM', - 'Khandwa' => 'KHDW', - 'Khanna' => 'KHAN', - 'Kharagpur' => 'KGPR', - 'Kharsia' => 'KHAS', - 'Khed' => 'KHED', - 'Khopoli' => 'KHOP', - 'Khurja' => 'KHUR', - 'Kichha' => 'KCHA', - 'Kishanganj' => 'KSGJ', - 'Kodad' => 'KODA', - 'Kodagu (Coorg)' => 'COOR', - 'Kodakara' => 'KDKR', - 'Kodungallur' => 'KODU', - 'Kokrajhar' => 'KKJR', - 'Kolar' => 'OLAR', - 'Kolhapur' => 'KOLH', - 'Kollam' => 'KOLM', - 'Kollengode' => 'KOLE', - 'Komarapalayam' => 'KOMA', - 'Kondagaon' => 'KNGN', - 'Kondlahalli' => 'KNAI', - 'Korba' => 'KRBA', - 'Kosamba' => 'KOSA', - 'Kota (AP)' => 'KOAN', - 'Kota' => 'KOTA', - 'Kothagudem' => 'KTGM', - 'Kothamangalam' => 'KTMM', - 'Kotkapura' => 'KOTK', - 'Kotpad' => 'KTPD', - 'Kotputli' => 'KPLI', - 'Kottayam' => 'KTYM', - 'Kovur (Nellore)' => 'KOVR', - 'Kovvur' => 'KOVU', - 'Koyyalagudem' => 'KOEM', - 'Kozhikode' => 'KOZH', - 'Kozhinjampara' => 'KOZA', - 'Krishnagiri' => 'KRHN', - 'Krishnanagar' => 'KNWB', - 'Krosuru' => 'KRSR', - 'Kruthivennu' => 'KRTH', - 'Kuchaman City' => 'KHCY', - 'Kukshi' => 'KUKS', - 'Kulithalai' => 'KULI', - 'Kullu' => 'KULU', - 'Kumbakonam' => 'KUMB', - 'Kunkuri' => 'KKRI', - 'Kurnool' => 'KURN', - 'Kurukshetra' => 'KURU', - 'Kutch' => 'KTCH', - 'Lakhimpur Kheri' => 'LKPK', - 'Lakhimpur' => 'LAHA', - 'Lakkavaram' => 'LRAM', - 'Lakshmeshwara' => 'LKSH', - 'Latur' => 'LAT', - 'Leh' => 'LEHL', - 'Lingasugur' => 'LING', - 'Lohardaga' => 'LOHA', - 'Lonavala' => 'LNVL', - 'Loni' => 'LONI', - 'Lucknow' => 'LUCK', - 'Ludhiana' => 'LUDH', - 'Macherla' => 'MACH', - 'Machilipatnam' => 'MAPM', - 'Madanapalle' => 'MDNP', - 'Maddur' => 'MADD', - 'Madhavaram' => 'MDHA', - 'Madhepura' => 'MHEA', - 'Madhira' => 'MADR', - 'Madurai' => 'MADU', - 'Magadi' => 'MAGA', - 'Mahabubabad' => 'MAHA', - 'Mahad' => 'MHAD', - 'Mahbubnagar' => 'MAHB', - 'Maheshwar' => 'MAHE', - 'Mahishadal' => 'MMAI', - 'Mahudha' => 'MAHU', - 'Malebennur' => 'MEBN', - 'Malegaon' => 'MALE', - 'Malerkotla' => 'MALR', - 'Mall' => 'MAAL', - 'Malout' => 'MALO', - 'Mamallapuram' => 'MMLL', - 'Manali' => 'MANA', - 'Manapparai' => 'MAPI', - 'Manawar' => 'MANW', - 'Mancherial' => 'MANC', - 'Mandapeta' => 'MAND', - 'Mandi Gobindgarh' => 'MBBH', - 'Mandla' => 'MADL', - 'Mandsaur' => 'MNDS', - 'Mandya' => 'MND', - 'Manendragarh' => 'MANE', - 'Mangalagiri' => 'MGLR', - 'Mangaldoi' => 'MANG', - 'Mangaluru (Mangalore)' => 'MLR', - 'Manikonda (AP)' => 'MNAP', - 'Manipal' => 'MANI', - 'Manjeri' => 'MAJR', - 'Mannargudi' => 'MANB', - 'Mannarkkad' => 'MKKA', - 'Mansa' => 'MNSA', - 'Manuguru' => 'MNGU', - 'Maraimalai Nagar' => 'MMNR', - 'Markapur' => 'MARK', - 'Marripeda' => 'MARR', - 'Marthandam' => 'MRDM', - 'Mathura' => 'MATH', - 'Mattannur' => 'MATT', - 'Mavellikara' => 'MVLR', - 'Medak' => 'MDAK', - 'Medarametla' => 'MDRM', - 'Meerut' => 'MERT', - 'Mehsana' => 'MEHS', - 'Memari' => 'MMRR', - 'Metpally' => 'METT', - 'Mettuppalayam' => 'MTPM', - 'Miryalaguda' => 'MRGD', - 'Mirzapur' => 'MIZP', - 'Moga' => 'MOGA', - 'Mohali' => 'MOHL', - 'Molakalmuru' => 'MOLA', - 'Moodbidri' => 'MOOD', - 'Moradabad' => 'MORA', - 'Moranhat' => 'MORH', - 'Morbi' => 'MOBI', - 'Morena' => 'MRMP', - 'Motihari' => 'MOTI', - 'Moyna' => 'MAYN', - 'Muddebihal' => 'MUDD', - 'Mudhol' => 'MUDL', - 'Mughalsarai' => 'MGSI', - 'Mukkam' => 'MUKM', - 'Muktsar' => 'MKST', - 'Mullanpur' => 'MULL', - 'Mummidivaram' => 'MUMM', - 'Mundakayam' => 'MUAM', - 'Mundra' => 'MUDA', - 'MUNNAR' => 'MUNN', - 'Muradnagar' => 'MRDG', - 'Murtizapur' => 'MUUR', - 'Musiri' => 'MUSI', - 'Mussoorie' => 'MSS', - 'Muvattupuzha' => 'MUVA', - 'Muzaffarnagar' => 'MUZ', - 'Muzaffarpur' => 'MUZA', - 'Mydukur' => 'MYDU', - 'Mysuru (Mysore)' => 'MYS', - 'Nabadwip' => 'NABB', - 'Nadiad' => 'NADI', - 'Nagaon' => 'NAAM', - 'Nagapattinam' => 'NGPT', - 'Nagari' => 'NAGI', - 'Nagarkurnool' => 'NGKL', - 'Nagda' => 'NAGD', - 'Nagercoil' => 'NAGE', - 'Nagothane' => 'NAGO', - 'Nagpur' => 'NAGP', - 'Naihati' => 'NHTA', - 'Nainital' => 'NAIN', - 'Nakhatrana' => 'NKHT', - 'Nalgonda' => 'NALK', - 'Namakkal' => 'NMKL', - 'Namchi' => 'NAMI', - 'Nanded' => 'NAND', - 'Nandigama' => 'NDGM', - 'Nandurbar' => 'NDNB', - 'Nandyal' => 'NADY', - 'Nanjanagudu' => 'NJGU', - 'Nanpara' => 'NANP', - 'Narasannapeta' => 'NRPT', - 'Narasaraopet' => 'NSPT', - 'Narayankhed' => 'NARY', - 'Narayanpur' => 'NRYA', - 'Nargund' => 'NRGD', - 'Narnaul' => 'NARN', - 'Narsampet' => 'NASP', - 'Narsapur' => 'NARP', - 'Narsipatnam' => 'NARS', - 'Nashik' => 'NASK', - 'Nathdwara' => 'NATW', - 'Navsari' => 'NVSR', - 'Nawalgarh' => 'NANA', - 'Nawanshahr' => 'NAVN', - 'Nawapara' => 'NAWA', - 'Nazira' => 'NZRA', - 'Nedumkandam' => 'NEDU', - 'Neemuch' => 'NMCH', - 'Nellimarla' => 'NLEM', - 'Ner Parsopant' => 'NERP', - 'New Tehri' => 'TEHR', - 'Neyveli' => 'NYVL', - 'Nidadavolu' => 'NDVD', - 'Nilagiri' => 'NIGA', - 'Nimbahera' => 'NIPA', - 'Nipani' => 'NIPN', - 'Nizamabad' => 'NIZA', - 'Nokha' => 'NKHA', - 'Nuzvid' => 'NZVD', - 'Nyamathi' => 'NYNT', - 'Ongole' => 'ONGL', - 'Ooty' => 'OOTY', - 'Osmanabad' => 'OSMA', - 'Ottapalam' => 'OTTP', - 'Padrauna' => 'PADR', - 'Pakala' => 'PAKA', - 'Pala' => 'PALL', - 'Palakkad' => 'PLKK', - 'Palakollu' => 'PLKL', - 'Palakonda' => 'PALK', - 'Palampur' => 'PALM', - 'Palanpur' => 'PALN', - 'Palasa' => 'PALS', - 'Palghar' => 'PALG', - 'Pali' => 'PAAL', - 'Pallipalayam' => 'PLLI', - 'Palwal' => 'PLWL', - 'Palwancha' => 'PLWA', - 'Pamarru' => 'PAMA', - 'Panchkula' => 'PNCH', - 'Pandalam' => 'PADM', - 'Pandharpur' => 'PNDH', - 'Panipat' => 'PAN', - 'Panruti' => 'PANT', - 'Papanasam' => 'PAPA', - 'Paralakhemundi' => 'PRKM', - 'Paratwada' => 'PARA', - 'Parbhani' => 'PARB', - 'Parchur' => 'PARC', - 'Parigi (Telangana)' => 'PARI', - 'Parvathipuram' => 'PRVT', - 'Patan' => 'PATA', - 'Pathalgaon' => 'PAHT', - 'Pathanamthitta' => 'PTNM', - 'Pathankot' => 'PATH', - 'Pathsala' => 'PATS', - 'Patiala' => 'PATI', - 'Patna' => 'PATN', - 'Pattambi' => 'PTMB', - 'Pattukkottai' => 'PATU', - 'Payakaraopeta' => 'PATE', - 'Payyanur' => 'PAYY', - 'Pedanandipadu' => 'PEDN', - 'Peddapalli' => 'PEDA', - 'Peddapuram' => 'PEDP', - 'Pen' => 'PEN', - 'Pendra' => 'PEND', - 'Pennagaram' => 'PENM', - 'Penuganchiprolu' => 'PENU', - 'Penugonda' => 'PDDG', - 'Perambalur' => 'PERA', - 'Peringottukurissi' => 'PERN', - 'Perinthalmanna' => 'PNTM', - 'Phagwara' => 'PHAG', - 'Phalodi' => 'PHLD', - 'Phaltan' => 'PHAL', - 'Pileru' => 'PLRU', - 'Pipariya' => 'PIPY', - 'Pithampur' => 'PITH', - 'Podili' => 'PODI', - 'Polavaram' => 'PLAB', - 'Pollachi' => 'POLL', - 'Pondicherry' => 'POND', - 'Ponduru' => 'PONU', - 'Ponnani' => 'PONN', - 'Porumamilla' => 'PORU', - 'Pratapgarh (Rajasthan)' => 'PTRT', - 'Pratapgarh (UP)' => 'PRAT', - 'Prathipadu' => 'PRTH', - 'Prayagraj (Allahabad)' => 'ALLH', - 'Proddatur' => 'PROD', - 'Pulluvila' => 'PULA', - 'Pulpally' => 'PULP', - 'Punalur' => 'PUNA', - 'Punganur' => 'PGNR', - 'Purnea' => 'PURN', - 'Purulia' => 'PURU', - 'Pusad' => 'PUSD', - 'Pusapatirega' => 'PREG', - 'Puttur' => 'PUTT', - 'Raebareli' => 'RAEB', - 'Rahimatpur' => 'RAHI', - 'Raibag' => 'RAIB', - 'Raigad' => 'RAI', - 'Raigarh' => 'RAIG', - 'Railway Koduru' => 'RLKD', - 'Raipur' => 'RAIPUR', - 'Raisinghnagar' => 'RSNG', - 'Rajamahendravaram (Rajahmundry)' => 'RJMU', - 'Rajapalayam' => 'RAYM', - 'Rajkot' => 'RAJK', - 'Rajnandgaon' => 'RAJA', - 'Rajpipla' => 'RJPA', - 'Rajpur' => 'RAJP', - 'Rajpura' => 'RARA', - 'Rajula' => 'RJLA', - 'Ramanagara' => 'RANG', - 'Ramayampet' => 'RAMP', - 'Ramgarhwa' => 'RGHA', - 'Ramnagar' => 'RAMN', - 'Rampur' => 'RAMU', - 'Ranaghat' => 'RANA', - 'Ranchi' => 'RANC', - 'Ranebennur' => 'RANE', - 'Rangia' => 'RAAA', - 'Raniganj' => 'RNGJ', - 'Ranipet' => 'RANI', - 'Ratlam' => 'RATL', - 'Ratnagiri (Odisha)' => 'RATO', - 'Ratnagiri' => 'RATN', - 'Ravulapalem' => 'RVPL', - 'Raxaul' => 'RAXA', - 'Rayachoti' => 'RYCT', - 'Rayavaram' => 'RAYA', - 'Renukoot' => 'RENU', - 'Repalle' => 'REPA', - 'Rewa' => 'RWAA', - 'Rewari' => 'REWA', - 'Rishikesh' => 'RKES', - 'Rishra' => 'RSRA', - 'Rohtak' => 'ROH', - 'Rourkela' => 'RKOR', - 'Routhulapudi' => 'ROUT', - 'Rudrapur' => 'RUDP', - 'Rupnagar' => 'RUPN', - 'Sadasivpet' => 'SADA', - 'Safidon' => 'SAFI', - 'Sagar' => 'SAMP', - 'Saharanpur' => 'SAHA', - 'Sakleshpur' => 'SASA', - 'Sakti' => 'SAKT', - 'Salem' => 'SALM', - 'Saligrama' => 'SGMA', - 'Salihundam' => 'SAHM', - 'Salur' => 'SALU', - 'Samalkota' => 'SAMA', - 'Sambalpur' => 'SAMB', - 'Sambhal' => 'SAML', - 'Samsi' => 'SAMS', - 'Sanawad' => 'SNWD', - 'Sangamner' => 'SMNE', - 'Sangareddy' => 'SARE', - 'Sangaria' => 'SAGR', - 'Sangli' => 'SANG', - 'Sangola' => 'SNGO', - 'Santhebennur' => 'STHB', - 'Saraipali' => 'SPAL', - 'Sarangarh' => 'SARH', - 'Sarangpur' => 'SARA', - 'Sardulgarh' => 'SARD', - 'Sarnath' => 'SART', - 'Sarni' => 'SARN', - 'Sasaram' => 'SARM', - 'Satara' => 'SATA', - 'Sathyamangalam' => 'STHY', - 'Satna' => 'SATN', - 'Sattenapalle' => 'SATL', - 'Secunderabad' => 'SCBD', - 'Seethanagaram' => 'SEET', - 'Sehore' => 'SEHO', - 'Semiliguda' => 'SIMI', - 'Sendhwa' => 'SEND', - 'Seoni Malwa' => 'SEMA', - 'Seoni' => 'SEON', - 'Shadnagar' => 'SHAD', - 'Shahada' => 'SHHA', - 'Shahdol' => 'SHAH', - 'Shahjahanpur' => 'SHJH', - 'Shajapur' => 'SJUR', - 'Shankarampet' => 'SHAN', - 'Shankarpally' => 'SKRP', - 'Sheorinarayan' => 'SHEO', - 'Shikaripur' => 'SHKR', - 'Shillong' => 'SHLG', - 'Shimla' => 'SMLA', - 'Shirali' => 'SHIR', - 'Shivamogga' => 'SHIA', - 'Shivpuri' => 'SHIV', - 'Shoranur' => 'SHNR', - 'Shrirampur' => 'SHUR', - 'Siddipet' => 'SDDP', - 'Sidlaghatta' => 'SIDL', - 'Sikar' => 'SIKR', - 'Silchar' => 'SIL', - 'Siliguri' => 'SILI', - 'Silvassa' => 'SILV', - 'Sindhanur' => 'SIND', - 'Sindhudurg' => 'SNDH', - 'Sinnar' => 'SINA', - 'Sircilla' => 'SIRC', - 'Sirohi' => 'SIRO', - 'Sirsi' => 'SRSI', - 'Siruguppa' => 'SPPA', - 'Sitamarhi' => 'SIMA', - 'Sitapur' => 'SITA', - 'Sivakasi' => 'SIV', - 'Sivasagar' => 'SVSG', - 'Solan' => 'SCO', - 'Solapur' => 'SOLA', - 'Sompeta' => 'SOMA', - 'Songadh' => 'SONG', - 'Sonipat' => 'RAIH', - 'Sonkatch' => 'SONH', - 'Sri Ganganagar' => 'SRIG', - 'Srikakulam' => 'SRKL', - 'Srinagar' => 'SRNG', - 'Srivaikuntam' => 'SRTA', - 'Srivilliputhur' => 'SRIV', - 'Station Ghanpur' => 'STGH', - 'Sultanpur' => 'SLUT', - 'Sulthan Bathery' => 'SULY', - 'Sundargarh' => 'SUND', - 'Surajpur' => 'SURA', - 'Surat' => 'SURT', - 'Surendranagar' => 'SRDN', - 'Suryapet' => 'SURY', - 'Tadepalligudem' => 'TADP', - 'Tallapudi' => 'TTPP', - 'Tallarevu' => 'TALL', - 'Talwandi Bhai' => 'TALW', - 'Tamluk' => 'TMLU', - 'Tanda' => 'TNDA', - 'Tandur' => 'TAND', - 'Tangutur' => 'TANG', - 'Tanuku' => 'TANK', - 'Tatipaka' => 'TATI', - 'Tenali' => 'TENA', - 'Tenkasi' => 'TENK', - 'Tezpur' => 'TEZP', - 'Thalassery' => 'THAY', - 'Thalayolaparambu' => 'THAL', - 'Thamarassery' => 'TMRY', - 'Thanipadi' => 'THPD', - 'Thanjavur' => 'TANJ', - 'Tharad' => 'THRD', - 'Theni' => 'THEN', - 'Thirubuvanai' => 'THRU', - 'Thiruthuraipoondi' => 'THND', - 'Thiruttani' => 'THTN', - 'Thiruvalla' => 'THVL', - 'Thiruvarur' => 'THVR', - 'Thodupuzha' => 'THOD', - 'Thorrur' => 'THOR', - 'Thottiyam' => 'THYM', - 'Thrissur' => 'THSR', - 'Thullur' => 'THUL', - 'Thuraiyur' => 'THYR', - 'Tilda Neora' => 'TNO', - 'Tindivanam' => 'TNVM', - 'Tinsukia' => 'TINS', - 'Tiptur' => 'TIPT', - 'Tiruchirappalli' => 'TRII', - 'Tirukoilur' => 'TRKR', - 'Tirunelveli' => 'TIRV', - 'Tirupati' => 'TIRU', - 'Tirupattur' => 'TRPR', - 'Tirupur' => 'TIRP', - 'Tirur' => 'TRUR', - 'Tiruvannamalai' => 'TVNM', - 'Titagarh' => 'TTGH', - 'Trichy' => 'TRIC', - 'Trivandrum' => 'TRIV', - 'Tumakuru (Tumkur)' => 'TUMK', - 'Tuticorin' => 'TTCN', - 'Udaipur' => 'UDAI', - 'Udaynarayanpur' => 'UDAY', - 'Udgir' => 'UDGR', - 'Udumalpet' => 'UDMP', - 'Udupi' => 'UDUP', - 'Ujjain' => 'UJJN', - 'Ulundurpet' => 'ULPT', - 'Umbergaon' => 'UMER', - 'Una' => 'BEEL', - 'Uthamapalayam' => 'UTHM', - 'Vadakara' => 'VDKR', - 'Vadakkencherry' => 'VDCY', - 'Vadalur' => 'VADA', - 'Vadanappally' => 'VADN', - 'Vadodara' => 'VAD', - 'Valigonda' => 'VALI', - 'Valluru' => 'VALL', - 'Valsad' => 'VLSD', - 'Vaniyambadi' => 'VANI', - 'Vapi' => 'VAPI', - 'Varadiyam' => 'VRYM', - 'Varanasi' => 'VAR', - 'Varkala' => 'VKAL', - 'Vatsavai' => 'VAST', - 'Vazhapadi' => 'VAZH', - 'Veeraghattam' => 'VEER', - 'Velangi' => 'VELG', - 'Velanthavalam' => 'VELM', - 'Vellakoil' => 'VELI', - 'Vellore' => 'VELL', - 'Vempalli' => 'VAIM', - 'Vemulawada' => 'VERU', - 'Venkatapuram' => 'VNKT', - 'Veraval' => 'VRAL', - 'Vetapalem' => 'VLEM', - 'Vijayapura (Bengaluru Rural)' => 'VIJP', - 'Vijayapura (Bijapur)' => 'VJPR', - 'Vijayarai' => 'VRAI', - 'Vijayawada' => 'VIJA', - 'Vikarabad' => 'VKBD', - 'Vikasnagar' => 'VKNG', - 'Vikravandi' => 'VIVI', - 'Villupuram' => 'VILL', - 'Virudhachalam' => 'VIDM', - 'Visnagar' => 'VISN', - 'Vizag (Visakhapatnam)' => 'VIZA', - 'Vizianagaram' => 'VIZI', - 'Vuyyuru' => 'VYUR', - 'Wai' => 'WAIP', - 'Wanaparthy' => 'WANA', - 'Wani' => 'WANI', - 'Warangal' => 'WAR', - 'Wardha' => 'WARD', - 'Warora' => 'WRRA', - 'Wyra' => 'WWAR', - 'Yadagirigutta' => 'YADG', - 'Yamunanagar' => 'YAMU', - 'Yavatmal' => 'YAVA', - 'Yelagiri' => 'YLGA', - 'Yelburga' => 'YELB', - 'Yellamanchili' => 'YLMN', - 'Yellandu' => 'YRLL', - 'Yemmiganur' => 'YEMM', - 'Zaheerabad' => 'ZAGE', - 'Zirakpur' => 'ZIRK', - ]; - - const PARAMETERS = [ - [ - 'city' => [ - 'name' => 'City', - 'type' => 'list', - 'defaultValue' => 'MUMBAI', - 'values' => self::CITIES, - ], - - 'category' => [ - 'name' => 'Category', - 'type' => 'list', - 'defaultValue' => self::MOVIES, - 'values' => [ - 'Plays' => self::PLAYS, - 'Events' => self::EVENTS, - 'Movies' => self::MOVIES, - ], - ], - 'language' => [ - 'name' => 'Language', - 'type' => 'list', - 'defaultValue' => 'all', - 'values' => [ - 'All' => 'all', - 'Kannada' => 'Kannada', - 'English' => 'English', - 'Hindi' => 'Hindi', - 'Telugu' => 'Telugu', - 'Tamil' => 'Tamil', - 'Malayalam' => 'Malayalam', - 'Gujarati' => 'Gujarati', - 'Assamese' => 'Assamese', - ] - ], - 'include_online' => [ - 'name' => 'Include Online Events', - 'type' => 'checkbox', - 'defaultValue' => false, - 'title' => 'Whether to include Online Events (applies only in case of "Events" category)' - ], - ] - ]; - - // Headers used in the generated table for Events/Plays - // Left is the BMS API Key, and right is the rendered version - const TABLE_HEADERS = [ - 'Genre' => 'Genre', - 'Language' => 'Language', - 'Length' => 'Length', - 'EventIsGlobal' => 'Global Event', - 'MinPrice' => 'Minimum Price', - // This doesn't seem to be used anywhere - // 'IsSuperstarExclusiveEvent' => 'SuperStar Exclusive', - 'EventSoldOut' => 'Sold Out', - ]; - - // Picked from EventGroup entry for movies - // Left is BMS API Ke, and right is the rendered version - const MOVIE_TABLE_HEADERS = [ - 'Duration' => 'Screentime', - 'EventCensor' => 'Rating', - ]; - - /* Common line that we want to edit out */ - const SYNOPSIS_REGEX = '/If you [\w\s,]+synopsis\@bookmyshow\.com/'; - - // Picked from the ChildEvents entries inside a Event Group - // for Movies - // Left is BMS API Key, right is rendered version - const INNER_MOVIE_HEADERS = [ - 'EventLanguage' => 'Language', - 'EventDimension' => 'Formats', - 'EventIsAtmosEnabled' => 'Dolby Atmos', - 'IsMovieClubEnabled' => 'Movie Club' - ]; - - // Primary URL for fetching information - // The city information is passed via a cookie - const URL_PREFIX = 'https://in.bookmyshow.com/serv/getData?cmd=QUICKBOOK&type='; - - public function collectData(){ - $city = $this->getInput('city'); - $category = $this->getInput('category'); - - $url = $this->makeUrl($category); - $headers = $this->makeHeaders($city); - - $data = json_decode(getContents($url, $headers), true); - - if ($category == self::MOVIES) { - $data = $data['moviesData']['BookMyShow']['arrEvents']; - } else { - $data = $data['data']['BookMyShow']['arrEvent']; - } - - foreach ($data as $event) { - $item = $this->generateEventData($event, $category); - if ($item and $this->matchesFilters($category, $event)) { - $this->items[] = $item; - } - } - - usort($this->items, function($a, $b) { - return $b['timestamp'] - $a['timestamp']; - }); - - $this->items = array_slice($this->items, 0, 15); - } - - private function makeUrl($category){ - return self::URL_PREFIX . $category; - } - - private function getDatesHtml($dates){ - $tz = new DateTimeZone(self::TIMEZONE); - $firstDate = DateTime::createFromFormat('Ymd', $dates[0]['ShowDateCode'], $tz) - ->format('D, d M Y'); - if (count($dates) == 1) { - return "<p>Date: $firstDate</p>"; - } - $lastDateIndex = count($dates) - 1; - $lastDate = DateTime::createFromFormat('Ymd', $dates[$lastDateIndex]['ShowDateCode']) - ->format('D, d M Y'); - return "<p>Dates: $firstDate - $lastDate</p>"; - } - - /** - * Given an event array, generates corresponding HTML entry - * @param array $event - * @see https://gist.github.com/captn3m0/6dbd539ca67579d22d6f90fab710ccd2 Sample JSON data for various events - */ - private function generateEventHtml($event, $category){ - $html = $this->getDatesHtml($event['arrDates']); - switch ($category) { - case self::MOVIES: - $html .= $this->generateMovieHtml($event); - break; - default: - $html .= $this->generateStandardHtml($event); - } - - $html .= $this->generateVenueHtml($event['arrVenues']); - return $html; - } - - /** - * Generates a simple Venue HTML, even for multiple venues - * spread across multiple dates as a description list. - */ - private function generateVenueHtml($venues){ - $html = '<h3>Venues</h3><table><thead><tr><th>Venue</th><th>Directions</th></tr></thead><tbody>'; - - foreach ($venues as $i => $venueData) { - $venueName = $venueData['VenueName']; - $address = $venueData['VenueAddress']; - $lat = $venueData['VenueLatitude']; - $lon = $venueData['VenueLongitude']; - - $directions = $this->generateDirectionsHtml($lat, $lon, $venueName); - $html .= "<tr><td>$venueName</td><td>$address<br>$directions</td></tr>"; - } - - return "$html</tbody></table>"; - } - - /** - * Generates a simple Table with event Data - * @todo Add support for jsonGenre as a tags row - */ - private function generateEventDetailsTable($event, $headers = self::TABLE_HEADERS){ - $table = ''; - foreach ($headers as $key => $header) { - if ($header == 'Language') { - $this->languages = [$event[$key]]; - } - - if ($event[$key] == 'Y') { - $value = 'Yes'; - } else if ($event[$key] == 'N') { - $value = 'No'; - } else { - $value = $event[$key]; - } - - $table .= <<<EOT + +class BookMyShowBridge extends BridgeAbstract +{ + const MAINTAINER = 'captn3m0'; + const NAME = 'BookMyShow Bridge'; + const URI = 'https://in.bookmyshow.com'; + const MOVIES_IMAGE_BASE_FORMAT = 'https://in.bmscdn.com/iedb/movies/images/mobile/thumbnail/large/%s.jpg'; + const DESCRIPTION = 'Returns the latest events on BookMyShow'; + + const TIMEZONE = 'Asia/Kolkata'; + + const PLAYS = 'PL'; + const EVENTS = 'CT'; + const MOVIES = 'MT'; + + const CATEGORIES = [ + self::PLAYS => 'Plays', + self::EVENTS => 'Events', + self::MOVIES => 'Movies', + ]; + + const CITIES = [ + // Most popular cities + 'Mumbai' => 'MUMBAI', + 'National Capital Region (NCR)' => 'NCR', + 'Bengaluru' => 'BANG', + 'Hyderabad' => 'HYD', + 'Ahmedabad' => 'AHD', + 'Chandigarh' => 'CHD', + 'Chennai' => 'CHEN', + 'Pune' => 'PUNE', + 'Kolkata' => 'KOLK', + 'Kochi' => 'KOCH', + + // Less common cities + 'Aalo' => 'AALU', + 'Abohar' => 'ABOR', + 'Abu Road' => 'ABRD', + 'Acharapakkam' => 'ACHA', + 'Adilabad' => 'ADIL', + 'Agar Malwa' => 'AGOR', + 'Agartala' => 'AGAR', + 'Agra' => 'AGRA', + 'Ahmedgarh' => 'AHMG', + 'Ahmednagar' => 'AHMED', + 'Aizawl' => 'AIZW', + 'Ajmer' => 'AJMER', + 'Akaltara' => 'AKAL', + 'Akividu' => 'AKVD', + 'Akola' => 'AKOL', + 'Alangudi' => 'ALNI', + 'Alappuzha' => 'ALPZ', + 'Alathur' => 'ALAR', + 'Alibaug' => 'ALBG', + 'Aligarh' => 'ALI', + 'Allagadda' => 'ALGD', + 'Almora' => 'ALMO', + 'Alwar' => 'ALWR', + 'Amadalavalasa' => 'ADAM', + 'Amalapuram' => 'AMAP', + 'Amaravathi' => 'AVTI', + 'Ambala' => 'AMB', + 'Ambikapur' => 'AMBI', + 'Ambur' => 'AMBR', + 'Amgaon' => 'AMGN', + 'Amravati' => 'AMRA', + 'Amritsar' => 'AMRI', + 'Anakapalle' => 'ANKP', + 'Anand' => 'AND', + 'Anantapalli' => 'ANTT', + 'Anantapur' => 'ANAN', + 'Anchal' => 'ANHL', + 'Angadipuram' => 'ANDM', + 'Angamaly' => 'ANGA', + 'Angara' => 'ANGR', + 'Angul' => 'ANGL', + 'Anjad' => 'ANJA', + 'Anjar' => 'ANJR', + 'Anklav' => 'ANKV', + 'Ankleshwar' => 'ANKL', + 'Annigeri' => 'ANGI', + 'Arakkonam' => 'ARAK', + 'Arambagh' => 'AMBH', + 'Aranthangi' => 'ARNT', + 'Ariyalur' => 'ARIY', + 'Arni' => 'ARNI', + 'Arsikere' => 'ARSI', + 'Aruppukottai' => 'ARUP', + 'Asansol' => 'ASANSOL', + 'Ashoknagar (West Bengal)' => 'ASNA', + 'Ashoknagar' => 'AKMP', + 'Aswaraopeta' => 'ASWA', + 'Atpadi' => 'ATPA', + 'Attili' => 'ATLI', + 'Aurangabad (Bihar)' => 'AUBI', + 'Aurangabad (West Bengal)' => 'AURW', + 'Aurangabad' => 'AURA', + 'Avinashi' => 'AVII', + 'Azamgarh' => 'AZMG', + 'B. Kothakota' => 'BKOT', + 'Badaun' => 'BADN', + 'Baddi' => 'BADD', + 'Badnawar' => 'BADR', + 'Bagbahara' => 'BBHA', + 'Bagha Purana' => 'BAPU', + 'Bagru' => 'BAGU', + 'Bahadurgarh' => 'BAHD', + 'Bahraich' => 'BHRH', + 'Baihar' => 'BIAH', + 'Baikunthpur' => 'BKTH', + 'Baindur' => 'BAND', + 'Bakhrahat' => 'BART', + 'Balaghat' => 'BLGT', + 'Balangir' => 'BALG', + 'Balasore' => 'BLSR', + 'Balijipeta' => 'BLIJ', + 'Balod' => 'BALD', + 'Baloda Bazar' => 'BBCH', + 'Balotra' => 'BALO', + 'Balrampur' => 'BLUR', + 'Balurghat' => 'BALU', + 'Bangarpet' => 'BAGT', + 'Banswada' => 'BNSA', + 'Banswara' => 'BANS', + 'Bantumilli' => 'BANT', + 'Barabanki' => 'BARK', + 'Baramati' => 'BARA', + 'Baraut' => 'BARL', + 'Bardoli' => 'BRDL', + 'Bareilly' => 'BARE', + 'Bargarh' => 'BARG', + 'Baripada' => 'BARI', + 'Barmer' => 'BARM', + 'Barnala' => 'BAR', + 'Barshi' => 'BRHI', + 'Barwani' => 'BRWN', + 'Basna' => 'BASN', + 'Basti' => 'BAST', + 'Bathinda' => 'BHAT', + 'Batlagundu' => 'BTGD', + 'Beawar' => 'BEAW', + 'Beed' => 'BEED', + 'Belagavi (Belgaum)' => 'BELG', + 'Bellampalli' => 'BELL', + 'Bellary' => 'BLRY', + 'Belur' => 'BELU', + 'Bemetara' => 'BMTA', + 'Berachampa' => 'BRAC', + 'Berhampore' => 'BEHA', + 'Berhampur' => 'BERP', + 'Bestavaripeta' => 'BEST', + 'Betul' => 'BETU', + 'Bhadrachalam' => 'BHDR', + 'Bhadrak' => 'BHAD', + 'Bhadravati' => 'BDVT', + 'Bhainsa' => 'BHAN', + 'Bhandara' => 'BHAA', + 'Bharamasagara' => 'BASA', + 'Bharuch' => 'BHAR', + 'Bhatapara' => 'BTAP', + 'Bhatkal' => 'BAKL', + 'Bhattiprolu' => 'BATT', + 'Bhavnagar' => 'BHNG', + 'Bhilai' => 'BHILAI', + 'Bhilwara' => 'BHIL', + 'Bhimadole' => 'BMDE', + 'Bhimavaram' => 'BHIM', + 'Bhiwadi' => 'BHWD', + 'Bhiwani' => 'BHWN', + 'Bhopal' => 'BHOP', + 'Bhubaneswar' => 'BHUB', + 'Bhuj' => 'BHUJ', + 'Bhuntar' => 'BHUN', + 'Bhupalpalle' => 'BHUP', + 'Bhusawal' => 'BHUS', + 'Biaora' => 'BIAR', + 'Bidar' => 'BIDR', + 'Bijnor' => 'BIJ', + 'Bijoynagar' => 'BIJO', + 'Bikaner' => 'BIK', + 'Bilara' => 'BILR', + 'Bilaspur (Himachal Pradesh)' => 'BIPS', + 'Bilaspur' => 'BILA', + 'Bilimora' => 'BILI', + 'Biraul' => 'BIRL', + 'Bishrampur' => 'BSRM', + 'Bodinayakanur' => 'BODI', + 'Boisar' => 'BOIS', + 'Bokaro' => 'BOKA', + 'Bolpur' => 'BLPR', + 'Bommidi' => 'BOMM', + 'Bongaigaon' => 'BONG', + 'Bongaon' => 'BONI', + 'Borsad' => 'BORM', + 'Brahmapur' => 'KHUB', + 'Brahmapuri' => 'BHMP', + 'Brajrajnagar' => 'BJNG', + 'Bulandshahr' => 'BULA', + 'Buldana' => 'BULD', + 'Bundu' => 'BUND', + 'Burdwan' => 'BURD', + 'Burhanpur' => 'BRHP', + 'Byadagi' => 'BYAD', + 'Chagallu' => 'CHAG', + 'Challakere' => 'CHLA', + 'Challapalli' => 'CHAP', + 'Champa' => 'CHAM', + 'Chanchal' => 'CCWC', + 'Chandausi' => 'CHDN', + 'Chandragiri' => 'CHAD', + 'Chandrakona' => 'CKNA', + 'Chandrapur' => 'CHAN', + 'Changanassery' => 'CNSY', + 'Channagiri' => 'CHGI', + 'Channarayapatna' => 'CHNN', + 'Chaygaon' => 'CHOG', + 'Cheepurupalli' => 'CHEE', + 'Chendrapinni' => 'CNPI', + 'Chengannur' => 'CHEG', + 'Chennur' => 'CHNU', + 'Cherial' => 'CHRY', + 'Cheyyar' => 'CHEY', + 'Chhibramau' => 'CHHI', + 'Chhindwara' => 'CHIN', + 'Chickmagaluru' => 'CHKA', + 'Chidambaram' => 'CHID', + 'Chikkaballapur' => 'CHIK', + 'Chikodi' => 'CHOK', + 'Chinturu' => 'CHTN', + 'Chirala' => 'CHIR', + 'Chitradurga' => 'CHIT', + 'Chittoor' => 'CHTT', + 'Chodavaram' => 'CDVM', + 'Chotila' => 'CHOT', + 'Coimbatore' => 'COIM', + 'Cooch Behar' => 'COBE', + 'Cuddalore' => 'CUDD', + 'Cuttack' => 'CUTT', + 'Dabra' => 'DABR', + 'Dahanu' => 'DHAU', + 'Dahegam' => 'DHGM', + 'Dahod' => 'DAHO', + 'Dakshin Barasat' => 'DAKS', + 'Dalli Rajhara' => 'DALL', + 'Daman' => 'DAMA', + 'Damoh' => 'DAMO', + 'Darjeeling' => 'DARJ', + 'Darsi' => 'DARS', + 'Dasuya' => 'DASU', + 'Dausa' => 'DAUS', + 'Davanagere' => 'DAVA', + 'Davuluru' => 'DVLR', + 'Deesa' => 'DEES', + 'Dehradun' => 'DEH', + 'Deoghar' => 'DOGH', + 'Devadurga' => 'DEVD', + 'Devarakonda' => 'DEVK', + 'Devgad' => 'DEGA', + 'Dewas' => 'DEWAS', + 'Dhampur' => 'DHPR', + 'Dhamtari' => 'DHMT', + 'Dhanbad' => 'DHAN', + 'Dhar' => 'DARH', + 'Dharamsala' => 'DMSL', + 'Dharapuram' => 'DHAR', + 'Dharmapuri' => 'DMPI', + 'Dharmavaram' => 'DDMA', + 'Dharwad' => 'DHAW', + 'Dhenkanal' => 'DNAL', + 'Dhoraji' => 'DHOR', + 'Dhule' => 'DHLE', + 'Dhuri' => 'DHRI', + 'Dibrugarh' => 'DIB', + 'Digras' => 'DIGR', + 'Dimapur' => 'DMPR', + 'Dindigul' => 'DIND', + 'Doddaballapura' => 'DDBP', + 'Domkal' => 'DMKL', + 'Dongargarh' => 'DONG', + 'Doraha' => 'DORH', + 'Durg' => 'DURG', + 'Durgapur' => 'DURGA', + 'Edappal' => 'EDPL', + 'Edlapadu' => 'EDLP', + 'Eluru' => 'ELRU', + 'Erattupetta' => 'ERAT', + 'Ernakulam' => 'ERNK', + 'Erode' => 'EROD', + 'Etawah' => 'ETWH', + 'Ettumanoor' => 'ETTU', + 'Faizabad' => 'FAZA', + 'Falna' => 'FALN', + 'Faridkot' => 'DKOT', + 'Fatehgarh Sahib' => 'FASA', + 'Fatehpur' => 'FATE', + 'Fatehpur(Rajasthan)' => 'FATR', + 'Firozpur' => 'FRZR', + 'G.Mamidada' => 'GMAD', + 'Gadag' => 'GADG', + 'Gadarwara' => 'GDWR', + 'Gadchiroli' => 'GDRO', + 'Gajendragarh' => 'GJGH', + 'Gajwel' => 'GAJW', + 'Ganapavaram' => 'GANP', + 'Gandhidham' => 'GDHAM', + 'Gandhinagar' => 'GNAGAR', + 'Gangavati' => 'GAVT', + 'Gangoh' => 'GANZ', + 'Gangtok' => 'GANG', + 'Ganjbasoda' => 'GANJ', + 'Garla' => 'GALA', + 'Gauribidanur' => 'GAUR', + 'Gaya' => 'GAYA', + 'Gingee' => 'GING', + 'Goa' => 'GOA', + 'Gobichettipalayam' => 'GOBI', + 'Godavarikhani' => 'GDVK', + 'Godhra' => 'GODH', + 'Gokak' => 'GKGK', + 'Gokavaram' => 'GOKM', + 'Golaghat' => 'GHT', + 'Gollaprolu' => 'GOLL', + 'Gonda' => 'GOND', + 'Gondia' => 'GNDA', + 'Gopalganj' => 'GOPG', + 'Gorakhpur' => 'GRKP', + 'Gorantla' => 'GORA', + 'Gotegaon' => 'GTGN', + 'Gownipalli' => 'GOWP', + 'Gudivada' => 'GUDI', + 'Gudiyatham' => 'GDTM', + 'Gudur' => 'GUDR', + 'Gulaothi' => 'GULL', + 'Guledgudda' => 'GULD', + 'Gummadidala' => 'GUMM', + 'Guna' => 'GUNA', + 'Guntakal' => 'GUNL', + 'Guntur' => 'GUNT', + 'Gurazala' => 'GURZ', + 'Guwahati' => 'GUW', + 'Gwalior' => 'GWAL', + 'Habra' => 'HARR', + 'Hagaribommanahalli' => 'HHGG', + 'Hajipur' => 'HAJI', + 'Haldia' => 'HLDI', + 'Haldwani' => 'HALD', + 'Haliya' => 'HALI', + 'Hampi' => 'HMPI', + 'Hardoi' => 'HRDI', + 'Haridwar' => 'HRDR', + 'Harihar' => 'HRRR', + 'Haripad' => 'HRPD', + 'Harugeri' => 'HARU', + 'Hasanpur' => 'HANS', + 'Hazaribagh' => 'HAZA', + 'Himmatnagar' => 'HIMM', + 'Hindaun City' => 'HIND', + 'Hisar' => 'HISR', + 'Honnali' => 'HONV', + 'Honnavara' => 'HNVR', + 'Hooghly' => 'HOOG', + 'Hoshiarpur' => 'HOSH', + 'Hoskote' => 'HOKT', + 'Hospet' => 'HOSP', + 'Hosur' => 'HSUR', + 'Howrah' => 'HWRH', + 'Hubballi (Hubli)' => 'HUBL', + 'Huvinahadagali' => 'HULI', + 'Ichalkaranji' => 'ICHL', + 'Ichchapuram' => 'ICPR', + 'Idappadi' => 'IDPI', + 'Idar' => 'IDAR', + 'Indapur' => 'INDA', + 'Indi' => 'IIND', + 'Indore' => 'IND', + 'Irinjalakuda' => 'IRNK', + 'Itanagar' => 'ITNG', + 'Itarsi' => 'ITAR', + 'Jabalpur' => 'JABL', + 'Jadcherla' => 'JADC', + 'Jagalur' => 'JAGA', + 'Jagatdal' => 'JGDL', + 'Jagdalpur' => 'JAGD', + 'Jaggampeta' => 'JAGG', + 'Jaggayyapeta' => 'JGGY', + 'Jagtial' => 'JGTL', + 'Jaipur' => 'JAIP', + 'Jaisalmer' => 'JSMR', + 'Jajpur Road' => 'JAJP', + 'Jalakandapuram' => 'JAKA', + 'Jalalabad' => 'JLAB', + 'Jalandhar' => 'JALA', + 'Jalgaon' => 'JALG', + 'Jalna' => 'JALN', + 'Jalpaiguri' => 'JPG', + 'Jami' => 'JAMI', + 'Jamkhed' => 'JAMK', + 'Jammalamadugu' => 'JAMD', + 'Jammu' => 'JAMM', + 'Jamnagar' => 'JAM', + 'Jamner' => 'JAMN', + 'Jamshedpur' => 'JMDP', + 'Jangaon' => 'JNGN', + 'Jangareddy Gudem' => 'JANG', + 'Janjgir' => 'JANR', + 'Jasdan' => 'JASD', + 'Jaunpur' => 'JANP', + 'Jehanabad' => 'JEHA', + 'Jetpur' => 'JETP', + 'Jewar' => 'JEWR', + 'Jeypore' => 'JEYP', + 'Jhabua' => 'JHAB', + 'Jhajjar' => 'JHAJ', + 'Jhansi' => 'JNSI', + 'Jharsuguda' => 'JRSG', + 'Jiaganj' => 'JAGJ', + 'Jind' => 'JIND', + 'Jodhpur' => 'JODH', + 'Jorhat' => 'JORT', + 'Junagadh' => 'JUGH', + 'Kadapa' => 'KDPA', + 'Kadi' => 'KADI', + 'Kaikaluru' => 'KAIK', + 'Kaithal' => 'KAIT', + 'Kakarapalli' => 'KAAP', + 'Kakinada' => 'KAKI', + 'Kalaburagi (Gulbarga)' => 'GULB', + 'Kalimpong' => 'KALI', + 'Kallakurichi' => 'KALL', + 'Kalol (Panchmahal)' => 'PANH', + 'Kalwakurthy' => 'KALW', + 'Kalyani' => 'KALY', + 'Kamanaickenpalayam' => 'KPLA', + 'Kamareddy' => 'KMRD', + 'Kamavarapukota' => 'KPKT', + 'Kambainallur' => 'KAMR', + 'Kamptee' => 'KAMP', + 'Kanakapura' => 'KAKP', + 'Kanchikacherla' => 'KNCH', + 'Kanchipuram' => 'KNPM', + 'Kandukur' => 'KAND', + 'Kangayam' => 'KGKM', + 'Kangra' => 'KANG', + 'Kanichar' => 'KANC', + 'Kanigiri' => 'KANI', + 'Kanipakam' => 'KAAM', + 'Kanjirappally' => 'KNNJ', + 'Kanker' => 'KANK', + 'Kannauj' => 'KANJ', + 'Kannur' => 'KANN', + 'Kanpur' => 'KANP', + 'Kanyakumari' => 'KAKM', + 'Karad' => 'KARD', + 'Karaikal' => 'KARA', + 'Karanja Lad' => 'KLAD', + 'Kareli' => 'KARE', + 'Karimangalam' => 'KARI', + 'Karimganj' => 'KRNJ', + 'Karimnagar' => 'KARIM', + 'Karjat' => 'KART', + 'Karkala' => 'KARK', + 'Karnal' => 'KARN', + 'Karunagapally' => 'KARG', + 'Karur' => 'KARU', + 'Karwar' => 'KWAR', + 'Kasdol' => 'KASD', + 'Kasgunj' => 'KASG', + 'Kashipur' => 'KASH', + 'Kasibugga' => 'KSBG', + 'Kathipudi' => 'KATP', + 'Kathua' => 'KATH', + 'Katihar' => 'KATI', + 'Kattappana' => 'AWCK', + 'Kaveripattinam' => 'KANM', + 'Kekri' => 'KEKR', + 'Keonjhar' => 'KNJH', + 'Kesinga' => 'KEGA', + 'Khachrod' => 'KHCU', + 'Khajipet' => 'KHAJ', + 'Khalilabad' => 'KHBD', + 'Khamgaon' => 'KHMG', + 'Khammam' => 'KHAM', + 'Khandwa' => 'KHDW', + 'Khanna' => 'KHAN', + 'Kharagpur' => 'KGPR', + 'Kharsia' => 'KHAS', + 'Khed' => 'KHED', + 'Khopoli' => 'KHOP', + 'Khurja' => 'KHUR', + 'Kichha' => 'KCHA', + 'Kishanganj' => 'KSGJ', + 'Kodad' => 'KODA', + 'Kodagu (Coorg)' => 'COOR', + 'Kodakara' => 'KDKR', + 'Kodungallur' => 'KODU', + 'Kokrajhar' => 'KKJR', + 'Kolar' => 'OLAR', + 'Kolhapur' => 'KOLH', + 'Kollam' => 'KOLM', + 'Kollengode' => 'KOLE', + 'Komarapalayam' => 'KOMA', + 'Kondagaon' => 'KNGN', + 'Kondlahalli' => 'KNAI', + 'Korba' => 'KRBA', + 'Kosamba' => 'KOSA', + 'Kota (AP)' => 'KOAN', + 'Kota' => 'KOTA', + 'Kothagudem' => 'KTGM', + 'Kothamangalam' => 'KTMM', + 'Kotkapura' => 'KOTK', + 'Kotpad' => 'KTPD', + 'Kotputli' => 'KPLI', + 'Kottayam' => 'KTYM', + 'Kovur (Nellore)' => 'KOVR', + 'Kovvur' => 'KOVU', + 'Koyyalagudem' => 'KOEM', + 'Kozhikode' => 'KOZH', + 'Kozhinjampara' => 'KOZA', + 'Krishnagiri' => 'KRHN', + 'Krishnanagar' => 'KNWB', + 'Krosuru' => 'KRSR', + 'Kruthivennu' => 'KRTH', + 'Kuchaman City' => 'KHCY', + 'Kukshi' => 'KUKS', + 'Kulithalai' => 'KULI', + 'Kullu' => 'KULU', + 'Kumbakonam' => 'KUMB', + 'Kunkuri' => 'KKRI', + 'Kurnool' => 'KURN', + 'Kurukshetra' => 'KURU', + 'Kutch' => 'KTCH', + 'Lakhimpur Kheri' => 'LKPK', + 'Lakhimpur' => 'LAHA', + 'Lakkavaram' => 'LRAM', + 'Lakshmeshwara' => 'LKSH', + 'Latur' => 'LAT', + 'Leh' => 'LEHL', + 'Lingasugur' => 'LING', + 'Lohardaga' => 'LOHA', + 'Lonavala' => 'LNVL', + 'Loni' => 'LONI', + 'Lucknow' => 'LUCK', + 'Ludhiana' => 'LUDH', + 'Macherla' => 'MACH', + 'Machilipatnam' => 'MAPM', + 'Madanapalle' => 'MDNP', + 'Maddur' => 'MADD', + 'Madhavaram' => 'MDHA', + 'Madhepura' => 'MHEA', + 'Madhira' => 'MADR', + 'Madurai' => 'MADU', + 'Magadi' => 'MAGA', + 'Mahabubabad' => 'MAHA', + 'Mahad' => 'MHAD', + 'Mahbubnagar' => 'MAHB', + 'Maheshwar' => 'MAHE', + 'Mahishadal' => 'MMAI', + 'Mahudha' => 'MAHU', + 'Malebennur' => 'MEBN', + 'Malegaon' => 'MALE', + 'Malerkotla' => 'MALR', + 'Mall' => 'MAAL', + 'Malout' => 'MALO', + 'Mamallapuram' => 'MMLL', + 'Manali' => 'MANA', + 'Manapparai' => 'MAPI', + 'Manawar' => 'MANW', + 'Mancherial' => 'MANC', + 'Mandapeta' => 'MAND', + 'Mandi Gobindgarh' => 'MBBH', + 'Mandla' => 'MADL', + 'Mandsaur' => 'MNDS', + 'Mandya' => 'MND', + 'Manendragarh' => 'MANE', + 'Mangalagiri' => 'MGLR', + 'Mangaldoi' => 'MANG', + 'Mangaluru (Mangalore)' => 'MLR', + 'Manikonda (AP)' => 'MNAP', + 'Manipal' => 'MANI', + 'Manjeri' => 'MAJR', + 'Mannargudi' => 'MANB', + 'Mannarkkad' => 'MKKA', + 'Mansa' => 'MNSA', + 'Manuguru' => 'MNGU', + 'Maraimalai Nagar' => 'MMNR', + 'Markapur' => 'MARK', + 'Marripeda' => 'MARR', + 'Marthandam' => 'MRDM', + 'Mathura' => 'MATH', + 'Mattannur' => 'MATT', + 'Mavellikara' => 'MVLR', + 'Medak' => 'MDAK', + 'Medarametla' => 'MDRM', + 'Meerut' => 'MERT', + 'Mehsana' => 'MEHS', + 'Memari' => 'MMRR', + 'Metpally' => 'METT', + 'Mettuppalayam' => 'MTPM', + 'Miryalaguda' => 'MRGD', + 'Mirzapur' => 'MIZP', + 'Moga' => 'MOGA', + 'Mohali' => 'MOHL', + 'Molakalmuru' => 'MOLA', + 'Moodbidri' => 'MOOD', + 'Moradabad' => 'MORA', + 'Moranhat' => 'MORH', + 'Morbi' => 'MOBI', + 'Morena' => 'MRMP', + 'Motihari' => 'MOTI', + 'Moyna' => 'MAYN', + 'Muddebihal' => 'MUDD', + 'Mudhol' => 'MUDL', + 'Mughalsarai' => 'MGSI', + 'Mukkam' => 'MUKM', + 'Muktsar' => 'MKST', + 'Mullanpur' => 'MULL', + 'Mummidivaram' => 'MUMM', + 'Mundakayam' => 'MUAM', + 'Mundra' => 'MUDA', + 'MUNNAR' => 'MUNN', + 'Muradnagar' => 'MRDG', + 'Murtizapur' => 'MUUR', + 'Musiri' => 'MUSI', + 'Mussoorie' => 'MSS', + 'Muvattupuzha' => 'MUVA', + 'Muzaffarnagar' => 'MUZ', + 'Muzaffarpur' => 'MUZA', + 'Mydukur' => 'MYDU', + 'Mysuru (Mysore)' => 'MYS', + 'Nabadwip' => 'NABB', + 'Nadiad' => 'NADI', + 'Nagaon' => 'NAAM', + 'Nagapattinam' => 'NGPT', + 'Nagari' => 'NAGI', + 'Nagarkurnool' => 'NGKL', + 'Nagda' => 'NAGD', + 'Nagercoil' => 'NAGE', + 'Nagothane' => 'NAGO', + 'Nagpur' => 'NAGP', + 'Naihati' => 'NHTA', + 'Nainital' => 'NAIN', + 'Nakhatrana' => 'NKHT', + 'Nalgonda' => 'NALK', + 'Namakkal' => 'NMKL', + 'Namchi' => 'NAMI', + 'Nanded' => 'NAND', + 'Nandigama' => 'NDGM', + 'Nandurbar' => 'NDNB', + 'Nandyal' => 'NADY', + 'Nanjanagudu' => 'NJGU', + 'Nanpara' => 'NANP', + 'Narasannapeta' => 'NRPT', + 'Narasaraopet' => 'NSPT', + 'Narayankhed' => 'NARY', + 'Narayanpur' => 'NRYA', + 'Nargund' => 'NRGD', + 'Narnaul' => 'NARN', + 'Narsampet' => 'NASP', + 'Narsapur' => 'NARP', + 'Narsipatnam' => 'NARS', + 'Nashik' => 'NASK', + 'Nathdwara' => 'NATW', + 'Navsari' => 'NVSR', + 'Nawalgarh' => 'NANA', + 'Nawanshahr' => 'NAVN', + 'Nawapara' => 'NAWA', + 'Nazira' => 'NZRA', + 'Nedumkandam' => 'NEDU', + 'Neemuch' => 'NMCH', + 'Nellimarla' => 'NLEM', + 'Ner Parsopant' => 'NERP', + 'New Tehri' => 'TEHR', + 'Neyveli' => 'NYVL', + 'Nidadavolu' => 'NDVD', + 'Nilagiri' => 'NIGA', + 'Nimbahera' => 'NIPA', + 'Nipani' => 'NIPN', + 'Nizamabad' => 'NIZA', + 'Nokha' => 'NKHA', + 'Nuzvid' => 'NZVD', + 'Nyamathi' => 'NYNT', + 'Ongole' => 'ONGL', + 'Ooty' => 'OOTY', + 'Osmanabad' => 'OSMA', + 'Ottapalam' => 'OTTP', + 'Padrauna' => 'PADR', + 'Pakala' => 'PAKA', + 'Pala' => 'PALL', + 'Palakkad' => 'PLKK', + 'Palakollu' => 'PLKL', + 'Palakonda' => 'PALK', + 'Palampur' => 'PALM', + 'Palanpur' => 'PALN', + 'Palasa' => 'PALS', + 'Palghar' => 'PALG', + 'Pali' => 'PAAL', + 'Pallipalayam' => 'PLLI', + 'Palwal' => 'PLWL', + 'Palwancha' => 'PLWA', + 'Pamarru' => 'PAMA', + 'Panchkula' => 'PNCH', + 'Pandalam' => 'PADM', + 'Pandharpur' => 'PNDH', + 'Panipat' => 'PAN', + 'Panruti' => 'PANT', + 'Papanasam' => 'PAPA', + 'Paralakhemundi' => 'PRKM', + 'Paratwada' => 'PARA', + 'Parbhani' => 'PARB', + 'Parchur' => 'PARC', + 'Parigi (Telangana)' => 'PARI', + 'Parvathipuram' => 'PRVT', + 'Patan' => 'PATA', + 'Pathalgaon' => 'PAHT', + 'Pathanamthitta' => 'PTNM', + 'Pathankot' => 'PATH', + 'Pathsala' => 'PATS', + 'Patiala' => 'PATI', + 'Patna' => 'PATN', + 'Pattambi' => 'PTMB', + 'Pattukkottai' => 'PATU', + 'Payakaraopeta' => 'PATE', + 'Payyanur' => 'PAYY', + 'Pedanandipadu' => 'PEDN', + 'Peddapalli' => 'PEDA', + 'Peddapuram' => 'PEDP', + 'Pen' => 'PEN', + 'Pendra' => 'PEND', + 'Pennagaram' => 'PENM', + 'Penuganchiprolu' => 'PENU', + 'Penugonda' => 'PDDG', + 'Perambalur' => 'PERA', + 'Peringottukurissi' => 'PERN', + 'Perinthalmanna' => 'PNTM', + 'Phagwara' => 'PHAG', + 'Phalodi' => 'PHLD', + 'Phaltan' => 'PHAL', + 'Pileru' => 'PLRU', + 'Pipariya' => 'PIPY', + 'Pithampur' => 'PITH', + 'Podili' => 'PODI', + 'Polavaram' => 'PLAB', + 'Pollachi' => 'POLL', + 'Pondicherry' => 'POND', + 'Ponduru' => 'PONU', + 'Ponnani' => 'PONN', + 'Porumamilla' => 'PORU', + 'Pratapgarh (Rajasthan)' => 'PTRT', + 'Pratapgarh (UP)' => 'PRAT', + 'Prathipadu' => 'PRTH', + 'Prayagraj (Allahabad)' => 'ALLH', + 'Proddatur' => 'PROD', + 'Pulluvila' => 'PULA', + 'Pulpally' => 'PULP', + 'Punalur' => 'PUNA', + 'Punganur' => 'PGNR', + 'Purnea' => 'PURN', + 'Purulia' => 'PURU', + 'Pusad' => 'PUSD', + 'Pusapatirega' => 'PREG', + 'Puttur' => 'PUTT', + 'Raebareli' => 'RAEB', + 'Rahimatpur' => 'RAHI', + 'Raibag' => 'RAIB', + 'Raigad' => 'RAI', + 'Raigarh' => 'RAIG', + 'Railway Koduru' => 'RLKD', + 'Raipur' => 'RAIPUR', + 'Raisinghnagar' => 'RSNG', + 'Rajamahendravaram (Rajahmundry)' => 'RJMU', + 'Rajapalayam' => 'RAYM', + 'Rajkot' => 'RAJK', + 'Rajnandgaon' => 'RAJA', + 'Rajpipla' => 'RJPA', + 'Rajpur' => 'RAJP', + 'Rajpura' => 'RARA', + 'Rajula' => 'RJLA', + 'Ramanagara' => 'RANG', + 'Ramayampet' => 'RAMP', + 'Ramgarhwa' => 'RGHA', + 'Ramnagar' => 'RAMN', + 'Rampur' => 'RAMU', + 'Ranaghat' => 'RANA', + 'Ranchi' => 'RANC', + 'Ranebennur' => 'RANE', + 'Rangia' => 'RAAA', + 'Raniganj' => 'RNGJ', + 'Ranipet' => 'RANI', + 'Ratlam' => 'RATL', + 'Ratnagiri (Odisha)' => 'RATO', + 'Ratnagiri' => 'RATN', + 'Ravulapalem' => 'RVPL', + 'Raxaul' => 'RAXA', + 'Rayachoti' => 'RYCT', + 'Rayavaram' => 'RAYA', + 'Renukoot' => 'RENU', + 'Repalle' => 'REPA', + 'Rewa' => 'RWAA', + 'Rewari' => 'REWA', + 'Rishikesh' => 'RKES', + 'Rishra' => 'RSRA', + 'Rohtak' => 'ROH', + 'Rourkela' => 'RKOR', + 'Routhulapudi' => 'ROUT', + 'Rudrapur' => 'RUDP', + 'Rupnagar' => 'RUPN', + 'Sadasivpet' => 'SADA', + 'Safidon' => 'SAFI', + 'Sagar' => 'SAMP', + 'Saharanpur' => 'SAHA', + 'Sakleshpur' => 'SASA', + 'Sakti' => 'SAKT', + 'Salem' => 'SALM', + 'Saligrama' => 'SGMA', + 'Salihundam' => 'SAHM', + 'Salur' => 'SALU', + 'Samalkota' => 'SAMA', + 'Sambalpur' => 'SAMB', + 'Sambhal' => 'SAML', + 'Samsi' => 'SAMS', + 'Sanawad' => 'SNWD', + 'Sangamner' => 'SMNE', + 'Sangareddy' => 'SARE', + 'Sangaria' => 'SAGR', + 'Sangli' => 'SANG', + 'Sangola' => 'SNGO', + 'Santhebennur' => 'STHB', + 'Saraipali' => 'SPAL', + 'Sarangarh' => 'SARH', + 'Sarangpur' => 'SARA', + 'Sardulgarh' => 'SARD', + 'Sarnath' => 'SART', + 'Sarni' => 'SARN', + 'Sasaram' => 'SARM', + 'Satara' => 'SATA', + 'Sathyamangalam' => 'STHY', + 'Satna' => 'SATN', + 'Sattenapalle' => 'SATL', + 'Secunderabad' => 'SCBD', + 'Seethanagaram' => 'SEET', + 'Sehore' => 'SEHO', + 'Semiliguda' => 'SIMI', + 'Sendhwa' => 'SEND', + 'Seoni Malwa' => 'SEMA', + 'Seoni' => 'SEON', + 'Shadnagar' => 'SHAD', + 'Shahada' => 'SHHA', + 'Shahdol' => 'SHAH', + 'Shahjahanpur' => 'SHJH', + 'Shajapur' => 'SJUR', + 'Shankarampet' => 'SHAN', + 'Shankarpally' => 'SKRP', + 'Sheorinarayan' => 'SHEO', + 'Shikaripur' => 'SHKR', + 'Shillong' => 'SHLG', + 'Shimla' => 'SMLA', + 'Shirali' => 'SHIR', + 'Shivamogga' => 'SHIA', + 'Shivpuri' => 'SHIV', + 'Shoranur' => 'SHNR', + 'Shrirampur' => 'SHUR', + 'Siddipet' => 'SDDP', + 'Sidlaghatta' => 'SIDL', + 'Sikar' => 'SIKR', + 'Silchar' => 'SIL', + 'Siliguri' => 'SILI', + 'Silvassa' => 'SILV', + 'Sindhanur' => 'SIND', + 'Sindhudurg' => 'SNDH', + 'Sinnar' => 'SINA', + 'Sircilla' => 'SIRC', + 'Sirohi' => 'SIRO', + 'Sirsi' => 'SRSI', + 'Siruguppa' => 'SPPA', + 'Sitamarhi' => 'SIMA', + 'Sitapur' => 'SITA', + 'Sivakasi' => 'SIV', + 'Sivasagar' => 'SVSG', + 'Solan' => 'SCO', + 'Solapur' => 'SOLA', + 'Sompeta' => 'SOMA', + 'Songadh' => 'SONG', + 'Sonipat' => 'RAIH', + 'Sonkatch' => 'SONH', + 'Sri Ganganagar' => 'SRIG', + 'Srikakulam' => 'SRKL', + 'Srinagar' => 'SRNG', + 'Srivaikuntam' => 'SRTA', + 'Srivilliputhur' => 'SRIV', + 'Station Ghanpur' => 'STGH', + 'Sultanpur' => 'SLUT', + 'Sulthan Bathery' => 'SULY', + 'Sundargarh' => 'SUND', + 'Surajpur' => 'SURA', + 'Surat' => 'SURT', + 'Surendranagar' => 'SRDN', + 'Suryapet' => 'SURY', + 'Tadepalligudem' => 'TADP', + 'Tallapudi' => 'TTPP', + 'Tallarevu' => 'TALL', + 'Talwandi Bhai' => 'TALW', + 'Tamluk' => 'TMLU', + 'Tanda' => 'TNDA', + 'Tandur' => 'TAND', + 'Tangutur' => 'TANG', + 'Tanuku' => 'TANK', + 'Tatipaka' => 'TATI', + 'Tenali' => 'TENA', + 'Tenkasi' => 'TENK', + 'Tezpur' => 'TEZP', + 'Thalassery' => 'THAY', + 'Thalayolaparambu' => 'THAL', + 'Thamarassery' => 'TMRY', + 'Thanipadi' => 'THPD', + 'Thanjavur' => 'TANJ', + 'Tharad' => 'THRD', + 'Theni' => 'THEN', + 'Thirubuvanai' => 'THRU', + 'Thiruthuraipoondi' => 'THND', + 'Thiruttani' => 'THTN', + 'Thiruvalla' => 'THVL', + 'Thiruvarur' => 'THVR', + 'Thodupuzha' => 'THOD', + 'Thorrur' => 'THOR', + 'Thottiyam' => 'THYM', + 'Thrissur' => 'THSR', + 'Thullur' => 'THUL', + 'Thuraiyur' => 'THYR', + 'Tilda Neora' => 'TNO', + 'Tindivanam' => 'TNVM', + 'Tinsukia' => 'TINS', + 'Tiptur' => 'TIPT', + 'Tiruchirappalli' => 'TRII', + 'Tirukoilur' => 'TRKR', + 'Tirunelveli' => 'TIRV', + 'Tirupati' => 'TIRU', + 'Tirupattur' => 'TRPR', + 'Tirupur' => 'TIRP', + 'Tirur' => 'TRUR', + 'Tiruvannamalai' => 'TVNM', + 'Titagarh' => 'TTGH', + 'Trichy' => 'TRIC', + 'Trivandrum' => 'TRIV', + 'Tumakuru (Tumkur)' => 'TUMK', + 'Tuticorin' => 'TTCN', + 'Udaipur' => 'UDAI', + 'Udaynarayanpur' => 'UDAY', + 'Udgir' => 'UDGR', + 'Udumalpet' => 'UDMP', + 'Udupi' => 'UDUP', + 'Ujjain' => 'UJJN', + 'Ulundurpet' => 'ULPT', + 'Umbergaon' => 'UMER', + 'Una' => 'BEEL', + 'Uthamapalayam' => 'UTHM', + 'Vadakara' => 'VDKR', + 'Vadakkencherry' => 'VDCY', + 'Vadalur' => 'VADA', + 'Vadanappally' => 'VADN', + 'Vadodara' => 'VAD', + 'Valigonda' => 'VALI', + 'Valluru' => 'VALL', + 'Valsad' => 'VLSD', + 'Vaniyambadi' => 'VANI', + 'Vapi' => 'VAPI', + 'Varadiyam' => 'VRYM', + 'Varanasi' => 'VAR', + 'Varkala' => 'VKAL', + 'Vatsavai' => 'VAST', + 'Vazhapadi' => 'VAZH', + 'Veeraghattam' => 'VEER', + 'Velangi' => 'VELG', + 'Velanthavalam' => 'VELM', + 'Vellakoil' => 'VELI', + 'Vellore' => 'VELL', + 'Vempalli' => 'VAIM', + 'Vemulawada' => 'VERU', + 'Venkatapuram' => 'VNKT', + 'Veraval' => 'VRAL', + 'Vetapalem' => 'VLEM', + 'Vijayapura (Bengaluru Rural)' => 'VIJP', + 'Vijayapura (Bijapur)' => 'VJPR', + 'Vijayarai' => 'VRAI', + 'Vijayawada' => 'VIJA', + 'Vikarabad' => 'VKBD', + 'Vikasnagar' => 'VKNG', + 'Vikravandi' => 'VIVI', + 'Villupuram' => 'VILL', + 'Virudhachalam' => 'VIDM', + 'Visnagar' => 'VISN', + 'Vizag (Visakhapatnam)' => 'VIZA', + 'Vizianagaram' => 'VIZI', + 'Vuyyuru' => 'VYUR', + 'Wai' => 'WAIP', + 'Wanaparthy' => 'WANA', + 'Wani' => 'WANI', + 'Warangal' => 'WAR', + 'Wardha' => 'WARD', + 'Warora' => 'WRRA', + 'Wyra' => 'WWAR', + 'Yadagirigutta' => 'YADG', + 'Yamunanagar' => 'YAMU', + 'Yavatmal' => 'YAVA', + 'Yelagiri' => 'YLGA', + 'Yelburga' => 'YELB', + 'Yellamanchili' => 'YLMN', + 'Yellandu' => 'YRLL', + 'Yemmiganur' => 'YEMM', + 'Zaheerabad' => 'ZAGE', + 'Zirakpur' => 'ZIRK', + ]; + + const PARAMETERS = [ + [ + 'city' => [ + 'name' => 'City', + 'type' => 'list', + 'defaultValue' => 'MUMBAI', + 'values' => self::CITIES, + ], + + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'defaultValue' => self::MOVIES, + 'values' => [ + 'Plays' => self::PLAYS, + 'Events' => self::EVENTS, + 'Movies' => self::MOVIES, + ], + ], + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'defaultValue' => 'all', + 'values' => [ + 'All' => 'all', + 'Kannada' => 'Kannada', + 'English' => 'English', + 'Hindi' => 'Hindi', + 'Telugu' => 'Telugu', + 'Tamil' => 'Tamil', + 'Malayalam' => 'Malayalam', + 'Gujarati' => 'Gujarati', + 'Assamese' => 'Assamese', + ] + ], + 'include_online' => [ + 'name' => 'Include Online Events', + 'type' => 'checkbox', + 'defaultValue' => false, + 'title' => 'Whether to include Online Events (applies only in case of "Events" category)' + ], + ] + ]; + + // Headers used in the generated table for Events/Plays + // Left is the BMS API Key, and right is the rendered version + const TABLE_HEADERS = [ + 'Genre' => 'Genre', + 'Language' => 'Language', + 'Length' => 'Length', + 'EventIsGlobal' => 'Global Event', + 'MinPrice' => 'Minimum Price', + // This doesn't seem to be used anywhere + // 'IsSuperstarExclusiveEvent' => 'SuperStar Exclusive', + 'EventSoldOut' => 'Sold Out', + ]; + + // Picked from EventGroup entry for movies + // Left is BMS API Ke, and right is the rendered version + const MOVIE_TABLE_HEADERS = [ + 'Duration' => 'Screentime', + 'EventCensor' => 'Rating', + ]; + + /* Common line that we want to edit out */ + const SYNOPSIS_REGEX = '/If you [\w\s,]+synopsis\@bookmyshow\.com/'; + + // Picked from the ChildEvents entries inside a Event Group + // for Movies + // Left is BMS API Key, right is rendered version + const INNER_MOVIE_HEADERS = [ + 'EventLanguage' => 'Language', + 'EventDimension' => 'Formats', + 'EventIsAtmosEnabled' => 'Dolby Atmos', + 'IsMovieClubEnabled' => 'Movie Club' + ]; + + // Primary URL for fetching information + // The city information is passed via a cookie + const URL_PREFIX = 'https://in.bookmyshow.com/serv/getData?cmd=QUICKBOOK&type='; + + public function collectData() + { + $city = $this->getInput('city'); + $category = $this->getInput('category'); + + $url = $this->makeUrl($category); + $headers = $this->makeHeaders($city); + + $data = json_decode(getContents($url, $headers), true); + + if ($category == self::MOVIES) { + $data = $data['moviesData']['BookMyShow']['arrEvents']; + } else { + $data = $data['data']['BookMyShow']['arrEvent']; + } + + foreach ($data as $event) { + $item = $this->generateEventData($event, $category); + if ($item and $this->matchesFilters($category, $event)) { + $this->items[] = $item; + } + } + + usort($this->items, function ($a, $b) { + return $b['timestamp'] - $a['timestamp']; + }); + + $this->items = array_slice($this->items, 0, 15); + } + + private function makeUrl($category) + { + return self::URL_PREFIX . $category; + } + + private function getDatesHtml($dates) + { + $tz = new DateTimeZone(self::TIMEZONE); + $firstDate = DateTime::createFromFormat('Ymd', $dates[0]['ShowDateCode'], $tz) + ->format('D, d M Y'); + if (count($dates) == 1) { + return "<p>Date: $firstDate</p>"; + } + $lastDateIndex = count($dates) - 1; + $lastDate = DateTime::createFromFormat('Ymd', $dates[$lastDateIndex]['ShowDateCode']) + ->format('D, d M Y'); + return "<p>Dates: $firstDate - $lastDate</p>"; + } + + /** + * Given an event array, generates corresponding HTML entry + * @param array $event + * @see https://gist.github.com/captn3m0/6dbd539ca67579d22d6f90fab710ccd2 Sample JSON data for various events + */ + private function generateEventHtml($event, $category) + { + $html = $this->getDatesHtml($event['arrDates']); + switch ($category) { + case self::MOVIES: + $html .= $this->generateMovieHtml($event); + break; + default: + $html .= $this->generateStandardHtml($event); + } + + $html .= $this->generateVenueHtml($event['arrVenues']); + return $html; + } + + /** + * Generates a simple Venue HTML, even for multiple venues + * spread across multiple dates as a description list. + */ + private function generateVenueHtml($venues) + { + $html = '<h3>Venues</h3><table><thead><tr><th>Venue</th><th>Directions</th></tr></thead><tbody>'; + + foreach ($venues as $i => $venueData) { + $venueName = $venueData['VenueName']; + $address = $venueData['VenueAddress']; + $lat = $venueData['VenueLatitude']; + $lon = $venueData['VenueLongitude']; + + $directions = $this->generateDirectionsHtml($lat, $lon, $venueName); + $html .= "<tr><td>$venueName</td><td>$address<br>$directions</td></tr>"; + } + + return "$html</tbody></table>"; + } + + /** + * Generates a simple Table with event Data + * @todo Add support for jsonGenre as a tags row + */ + private function generateEventDetailsTable($event, $headers = self::TABLE_HEADERS) + { + $table = ''; + foreach ($headers as $key => $header) { + if ($header == 'Language') { + $this->languages = [$event[$key]]; + } + + if ($event[$key] == 'Y') { + $value = 'Yes'; + } elseif ($event[$key] == 'N') { + $value = 'No'; + } else { + $value = $event[$key]; + } + + $table .= <<<EOT <tr> <th>$header</th> <td>$value</td> </tr> EOT; - } + } - return "<table>$table</table>"; - } + return "<table>$table</table>"; + } - private function generateStandardHtml($event){ - $table = $this->generateEventDetailsTable($event); + private function generateStandardHtml($event) + { + $table = $this->generateEventDetailsTable($event); - $imgsrc = $event['BannerURL']; + $imgsrc = $event['BannerURL']; - return <<<EOT + return <<<EOT <img title="Event Banner URL" src="$imgsrc"></img> <br> $table <br> More Details are available on the <a href="${event['FShareURL']}">BookMyShow website</a>. EOT; - } - - /** - * Converts some movie details from child entries, such as language - * into a single row item, either as a list, or as a Y/N - */ - private function generateInnerMovieDetails($data){ - // Show list of languages and list of formats - $headers = ['EventLanguage', 'EventDimension']; - // if any of these has a Y for any of the screenings, mark it as YES - $booleanHeaders = [ - 'EventIsAtmosEnabled', 'IsMovieClubEnabled' - ]; - - $items = []; - - // Throw values inside $items[$headerName] - foreach ($data as $row) { - foreach ($headers as $header) { - $items[$header][] = $row[$header]; - } - foreach ($booleanHeaders as $header) { - $items[$header][] = $row[$header]; - } - } - - // Remove duplicate values (if all screenings are 2D for eg) - foreach ($headers as $header) { - $items[$header] = array_unique($items[$header]); - - if ($header == 'EventLanguage') { - $this->languages = $items[$header]; - } - } - - $html = ''; - - // Generate a list for first kind of entries - foreach ($headers as $header) { - $html .= self::INNER_MOVIE_HEADERS[$header] . ': ' . join(', ', $items[$header]) . '<br>'; - } - - // Put a yes for the boolean entries - foreach ($booleanHeaders as $header) { - if(in_array('Y', $items[$header])) { - $html .= self::INNER_MOVIE_HEADERS[$header] . ': Yes<br>'; - } - } - - return $html; - } - - private function generateMovieHtml($eventGroup){ - $data = $eventGroup['ChildEvents'][0]; - $table = $this->generateEventDetailsTable($data, self::MOVIE_TABLE_HEADERS); - - $imgsrc = sprintf(self::MOVIES_IMAGE_BASE_FORMAT, $data['EventImageCode']); - - $url = $this->generateMovieUrl($eventGroup); - - $innerHtml = $this->generateInnerMovieDetails($eventGroup['ChildEvents']); - - $synopsis = preg_replace(self::SYNOPSIS_REGEX, '', $data['EventSynopsis']); - - return <<<EOT + } + + /** + * Converts some movie details from child entries, such as language + * into a single row item, either as a list, or as a Y/N + */ + private function generateInnerMovieDetails($data) + { + // Show list of languages and list of formats + $headers = ['EventLanguage', 'EventDimension']; + // if any of these has a Y for any of the screenings, mark it as YES + $booleanHeaders = [ + 'EventIsAtmosEnabled', 'IsMovieClubEnabled' + ]; + + $items = []; + + // Throw values inside $items[$headerName] + foreach ($data as $row) { + foreach ($headers as $header) { + $items[$header][] = $row[$header]; + } + foreach ($booleanHeaders as $header) { + $items[$header][] = $row[$header]; + } + } + + // Remove duplicate values (if all screenings are 2D for eg) + foreach ($headers as $header) { + $items[$header] = array_unique($items[$header]); + + if ($header == 'EventLanguage') { + $this->languages = $items[$header]; + } + } + + $html = ''; + + // Generate a list for first kind of entries + foreach ($headers as $header) { + $html .= self::INNER_MOVIE_HEADERS[$header] . ': ' . join(', ', $items[$header]) . '<br>'; + } + + // Put a yes for the boolean entries + foreach ($booleanHeaders as $header) { + if (in_array('Y', $items[$header])) { + $html .= self::INNER_MOVIE_HEADERS[$header] . ': Yes<br>'; + } + } + + return $html; + } + + private function generateMovieHtml($eventGroup) + { + $data = $eventGroup['ChildEvents'][0]; + $table = $this->generateEventDetailsTable($data, self::MOVIE_TABLE_HEADERS); + + $imgsrc = sprintf(self::MOVIES_IMAGE_BASE_FORMAT, $data['EventImageCode']); + + $url = $this->generateMovieUrl($eventGroup); + + $innerHtml = $this->generateInnerMovieDetails($eventGroup['ChildEvents']); + + $synopsis = preg_replace(self::SYNOPSIS_REGEX, '', $data['EventSynopsis']); + + return <<<EOT <img title="Movie Poster" src="$imgsrc"></img> <div>$table</div> <p>$innerHtml</p> @@ -1290,169 +1300,179 @@ EOT; More Details are available on the <a href="$url">BookMyShow website</a> and a trailer is available <a href="${data['EventTrailerURL']}" title="Trailer URL">here</a> EOT; - - } - - /** - * Generates a canonical movie URL - */ - private function generateMovieUrl($eventGroup){ - return self::URI . '/movies/' . $eventGroup['EventURLTitle'] . '/' . $eventGroup['EventCode']; - } - - private function generateMoviesData($eventGroup){ - // Additional data picked up from the first Child Event - $data = $eventGroup['ChildEvents'][0]; - $date = new DateTime($data['EventDate']); - - return [ - 'uri' => $this->generateMovieUrl($eventGroup), - 'title' => $eventGroup['EventTitle'], - 'timestamp' => $date->format('U'), - 'author' => 'BookMyShow', - 'content' => $this->generateMovieHtml($eventGroup), - 'enclosures' => [ - sprintf(self::MOVIES_IMAGE_BASE_FORMAT, $data['EventImageCode']), - ], - // Sample Input = |ADVENTURE|ANIMATION|COMEDY| - // Sample Output = ['Adventure', 'Animation', 'Comedy'] - 'categories' => array_filter( - explode('|', ucwords(strtolower($eventGroup['EventGrpGenre']), '|')) - ), - 'uid' => $eventGroup['EventGroup'] - ]; - } - - private function generateEventData($event, $category){ - if($category == self::MOVIES) { - return $this->generateMoviesData($event); - } - - return $this->generateGenericEventData($event, $category); - } - - /** - * Takes an event data as array and returns data for RSS Post - */ - private function generateGenericEventData($event, $category){ - $datetime = $event['Event_dtmCreated']; - if (empty($datetime)) { - return null; - } - $date = new DateTime($event['Event_dtmCreated']); - - return [ - 'uri' => $event['FShareURL'], - 'title' => $event['EventTitle'], - 'timestamp' => $date->format('U'), - 'author' => 'BookMyShow', - 'content' => $this->generateEventHtml($event, $category), - 'enclosures' => [ - $event['BannerURL'], - ], - 'categories' => array_merge( - [self::CATEGORIES[$category]], - $event['GenreArray'] - ), - 'uid' => $event['EventGroupCode'], - ]; - } - - /** - * Check if this is an online event. We can't rely on - * EventIsWebView, since that is set to Y for everything - */ - private function isEventOnline($event){ - if (isset($event['arrVenues']) && count($event['arrVenues']) === 1) { - if (preg_match('/(Online|Zoom)/i', $event['arrVenues'][0]['VenueName'])) { - return true; - } - } - - return false; - } - - private function matchesLanguage(){ - if ($this->getInput('language') !== 'all') { - $language = $this->getInput('language'); - return in_array($language, $this->languages); - } - return true; - } - - private function matchesOnline($event){ - if ($this->getInput('include_online')) { - return true; - } - return (!$this->isEventOnline($event)); - } - - /** - * Currently only checks if the language filter matches - */ - private function matchesFilters($category, $event){ - return $this->matchesLanguage() and $this->matchesOnline($event); - } - - /** - * Generates the RSS Feed title - */ - public function getName(){ - $city = $this->getInput('city'); - $category = $this->getInput('category'); - if(!is_null($city) and !is_null($category)) { - $categoryName = self::CATEGORIES[$category]; - $cityNames = array_flip(self::CITIES); - $cityName = $cityNames[$city]; - if ($this->getInput('language') !== 'null') { - $l = ucwords($this->getInput('language')); - // Sample: English Movies in Delhi - return "BookMyShow: $l $categoryName in $cityName"; - } - return "BookMyShow: $categoryName in $cityName"; - } - - return parent::getName(); - } - - /** - * Returns - * @param string $city City Code - * @return array list of headers - */ - private function makeHeaders($city){ - $uniqid = uniqid(); - $rgn = urlencode("|Code=$city|"); - return [ - "Cookie: bmsId=$uniqid; Rgn=$rgn;" - ]; - } - - /** - * Generates various URLs as per https://tools.ietf.org/html/rfc5870 - * and other standards - */ - private function generateDirectionsHtml($lat, $long, $address = ''){ - $address = urlencode($address); - - $links = [ - 'Apple Maps' => 'http://maps.apple.com/maps?q=%s,%s"', - 'Google Maps' => 'http://maps.google.com/maps?ll=%s,%s', - // 'Google Maps (Android)' => 'geo:%s,%s?q=%s', - // 'Google Maps (iOS)' => 'comgooglemaps://?center=%s,%s&zoom=12&views=traffic', - 'OpenStreetMap' => 'https://www.openstreetmap.org/?mlat=%s&mlon=%s&zoom=12', - 'GeoURI' => 'geo:%s,%s?q=%s', - ]; - - $html = ''; - - foreach ($links as $app => $str) { - $url = sprintf($str, $lat, $long, $address); - $locations[] = "<a href='$url' title='$app'>$app</a>"; - } - - $html .= implode(', ', $locations) . '</span>'; - - return $html; - } + } + + /** + * Generates a canonical movie URL + */ + private function generateMovieUrl($eventGroup) + { + return self::URI . '/movies/' . $eventGroup['EventURLTitle'] . '/' . $eventGroup['EventCode']; + } + + private function generateMoviesData($eventGroup) + { + // Additional data picked up from the first Child Event + $data = $eventGroup['ChildEvents'][0]; + $date = new DateTime($data['EventDate']); + + return [ + 'uri' => $this->generateMovieUrl($eventGroup), + 'title' => $eventGroup['EventTitle'], + 'timestamp' => $date->format('U'), + 'author' => 'BookMyShow', + 'content' => $this->generateMovieHtml($eventGroup), + 'enclosures' => [ + sprintf(self::MOVIES_IMAGE_BASE_FORMAT, $data['EventImageCode']), + ], + // Sample Input = |ADVENTURE|ANIMATION|COMEDY| + // Sample Output = ['Adventure', 'Animation', 'Comedy'] + 'categories' => array_filter( + explode('|', ucwords(strtolower($eventGroup['EventGrpGenre']), '|')) + ), + 'uid' => $eventGroup['EventGroup'] + ]; + } + + private function generateEventData($event, $category) + { + if ($category == self::MOVIES) { + return $this->generateMoviesData($event); + } + + return $this->generateGenericEventData($event, $category); + } + + /** + * Takes an event data as array and returns data for RSS Post + */ + private function generateGenericEventData($event, $category) + { + $datetime = $event['Event_dtmCreated']; + if (empty($datetime)) { + return null; + } + $date = new DateTime($event['Event_dtmCreated']); + + return [ + 'uri' => $event['FShareURL'], + 'title' => $event['EventTitle'], + 'timestamp' => $date->format('U'), + 'author' => 'BookMyShow', + 'content' => $this->generateEventHtml($event, $category), + 'enclosures' => [ + $event['BannerURL'], + ], + 'categories' => array_merge( + [self::CATEGORIES[$category]], + $event['GenreArray'] + ), + 'uid' => $event['EventGroupCode'], + ]; + } + + /** + * Check if this is an online event. We can't rely on + * EventIsWebView, since that is set to Y for everything + */ + private function isEventOnline($event) + { + if (isset($event['arrVenues']) && count($event['arrVenues']) === 1) { + if (preg_match('/(Online|Zoom)/i', $event['arrVenues'][0]['VenueName'])) { + return true; + } + } + + return false; + } + + private function matchesLanguage() + { + if ($this->getInput('language') !== 'all') { + $language = $this->getInput('language'); + return in_array($language, $this->languages); + } + return true; + } + + private function matchesOnline($event) + { + if ($this->getInput('include_online')) { + return true; + } + return (!$this->isEventOnline($event)); + } + + /** + * Currently only checks if the language filter matches + */ + private function matchesFilters($category, $event) + { + return $this->matchesLanguage() and $this->matchesOnline($event); + } + + /** + * Generates the RSS Feed title + */ + public function getName() + { + $city = $this->getInput('city'); + $category = $this->getInput('category'); + if (!is_null($city) and !is_null($category)) { + $categoryName = self::CATEGORIES[$category]; + $cityNames = array_flip(self::CITIES); + $cityName = $cityNames[$city]; + if ($this->getInput('language') !== 'null') { + $l = ucwords($this->getInput('language')); + // Sample: English Movies in Delhi + return "BookMyShow: $l $categoryName in $cityName"; + } + return "BookMyShow: $categoryName in $cityName"; + } + + return parent::getName(); + } + + /** + * Returns + * @param string $city City Code + * @return array list of headers + */ + private function makeHeaders($city) + { + $uniqid = uniqid(); + $rgn = urlencode("|Code=$city|"); + return [ + "Cookie: bmsId=$uniqid; Rgn=$rgn;" + ]; + } + + /** + * Generates various URLs as per https://tools.ietf.org/html/rfc5870 + * and other standards + */ + private function generateDirectionsHtml($lat, $long, $address = '') + { + $address = urlencode($address); + + $links = [ + 'Apple Maps' => 'http://maps.apple.com/maps?q=%s,%s"', + 'Google Maps' => 'http://maps.google.com/maps?ll=%s,%s', + // 'Google Maps (Android)' => 'geo:%s,%s?q=%s', + // 'Google Maps (iOS)' => 'comgooglemaps://?center=%s,%s&zoom=12&views=traffic', + 'OpenStreetMap' => 'https://www.openstreetmap.org/?mlat=%s&mlon=%s&zoom=12', + 'GeoURI' => 'geo:%s,%s?q=%s', + ]; + + $html = ''; + + foreach ($links as $app => $str) { + $url = sprintf($str, $lat, $long, $address); + $locations[] = "<a href='$url' title='$app'>$app</a>"; + } + + $html .= implode(', ', $locations) . '</span>'; + + return $html; + } } diff --git a/bridges/BooruprojectBridge.php b/bridges/BooruprojectBridge.php index 9917da7e..761fd084 100644 --- a/bridges/BooruprojectBridge.php +++ b/bridges/BooruprojectBridge.php @@ -1,71 +1,77 @@ <?php -class BooruprojectBridge extends DanbooruBridge { +class BooruprojectBridge extends DanbooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Booruproject'; + const URI = 'https://booru.org/'; + const DESCRIPTION = 'Returns images from given page of booruproject'; + const PARAMETERS = [ + 'global' => [ + 'p' => [ + 'name' => 'page', + 'defaultValue' => 0, + 'type' => 'number' + ], + 't' => [ + 'name' => 'tags', + 'required' => true, + 'exampleValue' => 'tagme', + 'title' => 'Use "all" to get all posts' + ] + ], + 'Booru subdomain (subdomain.booru.org)' => [ + 'i' => [ + 'name' => 'Subdomain', + 'required' => true, + 'exampleValue' => 'rm' + ] + ] + ]; - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Booruproject'; - const URI = 'https://booru.org/'; - const DESCRIPTION = 'Returns images from given page of booruproject'; - const PARAMETERS = array( - 'global' => array( - 'p' => array( - 'name' => 'page', - 'defaultValue' => 0, - 'type' => 'number' - ), - 't' => array( - 'name' => 'tags', - 'required' => true, - 'exampleValue' => 'tagme', - 'title' => 'Use "all" to get all posts' - ) - ), - 'Booru subdomain (subdomain.booru.org)' => array( - 'i' => array( - 'name' => 'Subdomain', - 'required' => true, - 'exampleValue' => 'rm' - ) - ) - ); + const PATHTODATA = '.thumb'; + const IDATTRIBUTE = 'id'; + const TAGATTRIBUTE = 'title'; + const PIDBYPAGE = 20; - const PATHTODATA = '.thumb'; - const IDATTRIBUTE = 'id'; - const TAGATTRIBUTE = 'title'; - const PIDBYPAGE = 20; + protected function getFullURI() + { + return $this->getURI() + . 'index.php?page=post&s=list&pid=' + . ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '') + . '&tags=' . urlencode($this->getInput('t')); + } - protected function getFullURI(){ - return $this->getURI() - . 'index.php?page=post&s=list&pid=' - . ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '') - . '&tags=' . urlencode($this->getInput('t')); - } + protected function getTags($element) + { + $tags = parent::getTags($element); + $tags = explode(' ', $tags); - protected function getTags($element){ - $tags = parent::getTags($element); - $tags = explode(' ', $tags); + // Remove statistics from the tags list (identified by colon) + foreach ($tags as $key => $tag) { + if (strpos($tag, ':') !== false) { + unset($tags[$key]); + } + } - // Remove statistics from the tags list (identified by colon) - foreach($tags as $key => $tag) { - if(strpos($tag, ':') !== false) unset($tags[$key]); - } + return implode(' ', $tags); + } - return implode(' ', $tags); - } + public function getURI() + { + if (!is_null($this->getInput('i'))) { + return 'https://' . $this->getInput('i') . '.booru.org/'; + } - public function getURI(){ - if(!is_null($this->getInput('i'))) { - return 'https://' . $this->getInput('i') . '.booru.org/'; - } + return parent::getURI(); + } - return parent::getURI(); - } + public function getName() + { + if (!is_null($this->getInput('i'))) { + return static::NAME . ' ' . $this->getInput('i'); + } - public function getName(){ - if(!is_null($this->getInput('i'))) { - return static::NAME . ' ' . $this->getInput('i'); - } - - return parent::getName(); - } + return parent::getName(); + } } diff --git a/bridges/BrutBridge.php b/bridges/BrutBridge.php index d53b5c6d..c482c247 100644 --- a/bridges/BrutBridge.php +++ b/bridges/BrutBridge.php @@ -1,73 +1,75 @@ <?php -class BrutBridge extends BridgeAbstract { - const NAME = 'Brut Bridge'; - const URI = 'https://www.brut.media'; - const DESCRIPTION = 'Returns 10 newest videos by category and edition'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'News' => 'news', - 'International' => 'international', - 'Economy' => 'economy', - 'Science and Technology' => 'science-and-technology', - 'Entertainment' => 'entertainment', - 'Sports' => 'sport', - 'Nature' => 'nature', - 'Health' => 'health', - ), - 'defaultValue' => 'news', - ), - 'edition' => array( - 'name' => ' Edition', - 'type' => 'list', - 'values' => array( - 'United States' => 'us', - 'United Kingdom' => 'uk', - 'France' => 'fr', - 'Spain' => 'es', - 'India' => 'in', - 'Mexico' => 'mx', - ), - 'defaultValue' => 'us', - ) - ) - ); - - const CACHE_TIMEOUT = 1800; // 30 mins - - private $jsonRegex = '/window\.__PRELOADED_STATE__ = ((?:.*)});/'; - - public function collectData() { - - $html = getSimpleHTMLDOM($this->getURI()); - - $results = $html->find('div.results', 0); - - foreach($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $li) { - $item = array(); - - $videoPath = self::URI . $li->children(0)->href; - $videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600); - - $json = $this->extractJson($videoPageHtml); - $id = array_keys((array) $json->media->index)[0]; - - $item['uri'] = $videoPath; - $item['title'] = $json->media->index->$id->title; - $item['timestamp'] = $json->media->index->$id->published_at; - $item['enclosures'][] = $json->media->index->$id->media->thumbnail; - - $description = $json->media->index->$id->description; - $article = ''; - - if (is_null($json->media->index->$id->media->seo_article) === false) { - $article = markdownToHtml($json->media->index->$id->media->seo_article); - } - - $item['content'] = <<<EOD + +class BrutBridge extends BridgeAbstract +{ + const NAME = 'Brut Bridge'; + const URI = 'https://www.brut.media'; + const DESCRIPTION = 'Returns 10 newest videos by category and edition'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [[ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'News' => 'news', + 'International' => 'international', + 'Economy' => 'economy', + 'Science and Technology' => 'science-and-technology', + 'Entertainment' => 'entertainment', + 'Sports' => 'sport', + 'Nature' => 'nature', + 'Health' => 'health', + ], + 'defaultValue' => 'news', + ], + 'edition' => [ + 'name' => ' Edition', + 'type' => 'list', + 'values' => [ + 'United States' => 'us', + 'United Kingdom' => 'uk', + 'France' => 'fr', + 'Spain' => 'es', + 'India' => 'in', + 'Mexico' => 'mx', + ], + 'defaultValue' => 'us', + ] + ] + ]; + + const CACHE_TIMEOUT = 1800; // 30 mins + + private $jsonRegex = '/window\.__PRELOADED_STATE__ = ((?:.*)});/'; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $results = $html->find('div.results', 0); + + foreach ($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $li) { + $item = []; + + $videoPath = self::URI . $li->children(0)->href; + $videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600); + + $json = $this->extractJson($videoPageHtml); + $id = array_keys((array) $json->media->index)[0]; + + $item['uri'] = $videoPath; + $item['title'] = $json->media->index->$id->title; + $item['timestamp'] = $json->media->index->$id->published_at; + $item['enclosures'][] = $json->media->index->$id->media->thumbnail; + + $description = $json->media->index->$id->description; + $article = ''; + + if (is_null($json->media->index->$id->media->seo_article) === false) { + $article = markdownToHtml($json->media->index->$id->media->seo_article); + } + + $item['content'] = <<<EOD <video controls poster="{$json->media->index->$id->media->thumbnail}" preload="none"> <source src="{$json->media->index->$id->media->mp4_url}" type="video/mp4"> </video> @@ -75,53 +77,53 @@ class BrutBridge extends BridgeAbstract { {$article} EOD; - $this->items[] = $item; - - if (count($this->items) >= 10) { - break; - } - } - } - - public function getURI() { - - if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) { - return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category'); - } + $this->items[] = $item; - return parent::getURI(); - } + if (count($this->items) >= 10) { + break; + } + } + } - public function getName() { + public function getURI() + { + if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) { + return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category'); + } - if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) { - $parameters = $this->getParameters(); + return parent::getURI(); + } - $editionValues = array_flip($parameters[0]['edition']['values']); - $categoryValues = array_flip($parameters[0]['category']['values']); + public function getName() + { + if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) { + $parameters = $this->getParameters(); - return $categoryValues[$this->getInput('category')] . ' - ' . - $editionValues[$this->getInput('edition')] . ' - Brut.'; - } + $editionValues = array_flip($parameters[0]['edition']['values']); + $categoryValues = array_flip($parameters[0]['category']['values']); - return parent::getName(); - } + return $categoryValues[$this->getInput('category')] . ' - ' . + $editionValues[$this->getInput('edition')] . ' - Brut.'; + } - /** - * Extract JSON from page - */ - private function extractJson($html) { + return parent::getName(); + } - if (!preg_match($this->jsonRegex, $html, $parts)) { - returnServerError('Failed to extract data from page'); - } + /** + * Extract JSON from page + */ + private function extractJson($html) + { + if (!preg_match($this->jsonRegex, $html, $parts)) { + returnServerError('Failed to extract data from page'); + } - $data = json_decode($parts[1]); + $data = json_decode($parts[1]); - if ($data === false) { - returnServerError('Failed to decode extracted data'); - } + if ($data === false) { + returnServerError('Failed to decode extracted data'); + } - return $data; - } + return $data; + } } diff --git a/bridges/BugzillaBridge.php b/bridges/BugzillaBridge.php index 16ce4a28..a7fc75b8 100644 --- a/bridges/BugzillaBridge.php +++ b/bridges/BugzillaBridge.php @@ -1,180 +1,190 @@ <?php -class BugzillaBridge extends BridgeAbstract { - const NAME = 'Bugzilla Bridge'; - const URI = 'https://www.bugzilla.org/'; - const DESCRIPTION = 'Bridge for any Bugzilla instance'; - const MAINTAINER = 'Yaman Qalieh'; - const PARAMETERS = array( - 'global' => array( - 'instance' => array( - 'name' => 'Instance URL', - 'required' => true, - 'exampleValue' => 'https://bugzilla.mozilla.org' - ) - ), - 'Bug comments' => array( - 'id' => array( - 'name' => 'Bug tracking ID', - 'type' => 'number', - 'required' => true, - 'title' => 'Insert bug tracking ID', - 'exampleValue' => 121241 - ), - 'limit' => array( - 'name' => 'Number of comments to return', - 'type' => 'number', - 'required' => false, - 'title' => 'Specify number of comments to return', - 'defaultValue' => -1 - ), - 'skiptags' => array( - 'name' => 'Skip offtopic comments', - 'type' => 'checkbox', - 'title' => 'Excludes comments tagged as advocacy, metoo, or offtopic from the feed' - ) - ) - ); - - const SKIPPED_ACTIVITY = array( - 'cc' => true, - 'comment_tag' => true - ); - - const SKIPPED_TAGS = array('advocacy', 'metoo', 'offtopic'); - - private $instance; - private $bugid; - private $buguri; - private $title; - - public function getName() { - if (!is_null($this->title)) { - return $this->title; - } - return parent::getName(); - } - - public function getURI() { - return $this->buguri ?? parent::getURI(); - } - - public function collectData() { - $this->instance = rtrim($this->getInput('instance'), '/'); - $this->bugid = $this->getInput('id'); - $this->buguri = $this->instance . '/show_bug.cgi?id=' . $this->bugid; - - $url = $this->instance . '/rest/bug/' . $this->bugid; - $this->getTitle($url); - $this->collectComments($url . '/comment'); - $this->collectUpdates($url . '/history'); - - usort($this->items, function($a, $b) { - return $b['timestamp'] <=> $a['timestamp']; - }); - - if ($this->getInput('limit') > 0) { - $this->items = array_slice($this->items, 0, $this->getInput('limit')); - } - } - - protected function getTitle($url) { - // Only request the summary for a faster request - $json = json_decode(getContents($url . '?include_fields=summary'), true); - $this->title = 'Bug ' . $this->bugid . ' - ' . - $json['bugs'][0]['summary'] . ' - ' . - // Remove https:// - substr($this->instance, 8); - } - - protected function collectComments($url) { - $json = json_decode(getContents($url), true); - - // Array of comments is here - if (!isset($json['bugs'][$this->bugid]['comments'])) { - returnClientError('Cannot find REST endpoint'); - } - - foreach($json['bugs'][$this->bugid]['comments'] as $comment) { - $item = array(); - if ($this->getInput('skiptags') and - array_intersect(self::SKIPPED_TAGS, $comment['tags'])) { - continue; - } - $item['categories'] = $comment['tags']; - $item['uri'] = $this->buguri . '#c' . $comment['count']; - $item['title'] = 'Comment ' . $comment['count']; - $item['timestamp'] = $comment['creation_time']; - $item['author'] = $this->getUser($comment['creator']); - $item['content'] = $comment['text']; - if (isset($comment['is_markdown']) and $comment['is_markdown']) { - $item['content'] = markdownToHtml($item['content']); - } - if (!is_null($comment['attachment_id'])) { - $item['enclosures'] = array($this->instance . '/attachment.cgi?id=' . $comment['attachment_id']); - } - $this->items[] = $item; - } - } - - protected function collectUpdates($url) { - $json = json_decode(getContents($url), true); - - // Array of changesets which contain an array of changes - if (!isset($json['bugs']['0']['history'])) { - returnClientError('Cannot find REST endpoint'); - } - - foreach($json['bugs']['0']['history'] as $changeset) { - $author = $this->getUser($changeset['who']); - $timestamp = $changeset['when']; - foreach($changeset['changes'] as $change) { - // Skip updates to the cc list and comment tagging - if (isset(self::SKIPPED_ACTIVITY[$change['field_name']])) { - continue; - } - - $item = array(); - $item['uri'] = $this->buguri; - $item['title'] = 'Updated'; - $item['timestamp'] = $timestamp; - $item['author'] = $author; - $item['content'] = ucfirst($change['field_name']) . ': ' . - ($change['removed'] === '' ? '[nothing]' : $change['removed']) . ' -> ' . - ($change['added'] === '' ? '[nothing]' : $change['added']); - $this->items[] = $item; - } - } - } - - protected function getUser($user) { - // Check if the user endpoint is available - if ($this->loadCacheValue($this->instance . 'userEndpointClosed')) { - return $user; - } - - $cache = $this->loadCacheValue($this->instance . $user); - if (!is_null($cache)) { - return $cache; - } - - $url = $this->instance . '/rest/user/' . $user . '?include_fields=real_name'; - try { - $json = json_decode(getContents($url), true); - if (isset($json['error']) and $json['error']) { - throw new Exception; - } - } catch (Exception $e) { - $this->saveCacheValue($this->instance . 'userEndpointClosed', true); - return $user; - } - - $username = $json['users']['0']['real_name']; - - if (empty($username)) { - $username = $user; - } - $this->saveCacheValue($this->instance . $user, $username); - return $username; - } +class BugzillaBridge extends BridgeAbstract +{ + const NAME = 'Bugzilla Bridge'; + const URI = 'https://www.bugzilla.org/'; + const DESCRIPTION = 'Bridge for any Bugzilla instance'; + const MAINTAINER = 'Yaman Qalieh'; + const PARAMETERS = [ + 'global' => [ + 'instance' => [ + 'name' => 'Instance URL', + 'required' => true, + 'exampleValue' => 'https://bugzilla.mozilla.org' + ] + ], + 'Bug comments' => [ + 'id' => [ + 'name' => 'Bug tracking ID', + 'type' => 'number', + 'required' => true, + 'title' => 'Insert bug tracking ID', + 'exampleValue' => 121241 + ], + 'limit' => [ + 'name' => 'Number of comments to return', + 'type' => 'number', + 'required' => false, + 'title' => 'Specify number of comments to return', + 'defaultValue' => -1 + ], + 'skiptags' => [ + 'name' => 'Skip offtopic comments', + 'type' => 'checkbox', + 'title' => 'Excludes comments tagged as advocacy, metoo, or offtopic from the feed' + ] + ] + ]; + + const SKIPPED_ACTIVITY = [ + 'cc' => true, + 'comment_tag' => true + ]; + + const SKIPPED_TAGS = ['advocacy', 'metoo', 'offtopic']; + + private $instance; + private $bugid; + private $buguri; + private $title; + + public function getName() + { + if (!is_null($this->title)) { + return $this->title; + } + return parent::getName(); + } + + public function getURI() + { + return $this->buguri ?? parent::getURI(); + } + + public function collectData() + { + $this->instance = rtrim($this->getInput('instance'), '/'); + $this->bugid = $this->getInput('id'); + $this->buguri = $this->instance . '/show_bug.cgi?id=' . $this->bugid; + + $url = $this->instance . '/rest/bug/' . $this->bugid; + $this->getTitle($url); + $this->collectComments($url . '/comment'); + $this->collectUpdates($url . '/history'); + + usort($this->items, function ($a, $b) { + return $b['timestamp'] <=> $a['timestamp']; + }); + + if ($this->getInput('limit') > 0) { + $this->items = array_slice($this->items, 0, $this->getInput('limit')); + } + } + + protected function getTitle($url) + { + // Only request the summary for a faster request + $json = json_decode(getContents($url . '?include_fields=summary'), true); + $this->title = 'Bug ' . $this->bugid . ' - ' . + $json['bugs'][0]['summary'] . ' - ' . + // Remove https:// + substr($this->instance, 8); + } + + protected function collectComments($url) + { + $json = json_decode(getContents($url), true); + + // Array of comments is here + if (!isset($json['bugs'][$this->bugid]['comments'])) { + returnClientError('Cannot find REST endpoint'); + } + + foreach ($json['bugs'][$this->bugid]['comments'] as $comment) { + $item = []; + if ( + $this->getInput('skiptags') and + array_intersect(self::SKIPPED_TAGS, $comment['tags']) + ) { + continue; + } + $item['categories'] = $comment['tags']; + $item['uri'] = $this->buguri . '#c' . $comment['count']; + $item['title'] = 'Comment ' . $comment['count']; + $item['timestamp'] = $comment['creation_time']; + $item['author'] = $this->getUser($comment['creator']); + $item['content'] = $comment['text']; + if (isset($comment['is_markdown']) and $comment['is_markdown']) { + $item['content'] = markdownToHtml($item['content']); + } + if (!is_null($comment['attachment_id'])) { + $item['enclosures'] = [$this->instance . '/attachment.cgi?id=' . $comment['attachment_id']]; + } + $this->items[] = $item; + } + } + + protected function collectUpdates($url) + { + $json = json_decode(getContents($url), true); + + // Array of changesets which contain an array of changes + if (!isset($json['bugs']['0']['history'])) { + returnClientError('Cannot find REST endpoint'); + } + + foreach ($json['bugs']['0']['history'] as $changeset) { + $author = $this->getUser($changeset['who']); + $timestamp = $changeset['when']; + foreach ($changeset['changes'] as $change) { + // Skip updates to the cc list and comment tagging + if (isset(self::SKIPPED_ACTIVITY[$change['field_name']])) { + continue; + } + + $item = []; + $item['uri'] = $this->buguri; + $item['title'] = 'Updated'; + $item['timestamp'] = $timestamp; + $item['author'] = $author; + $item['content'] = ucfirst($change['field_name']) . ': ' . + ($change['removed'] === '' ? '[nothing]' : $change['removed']) . ' -> ' . + ($change['added'] === '' ? '[nothing]' : $change['added']); + $this->items[] = $item; + } + } + } + + protected function getUser($user) + { + // Check if the user endpoint is available + if ($this->loadCacheValue($this->instance . 'userEndpointClosed')) { + return $user; + } + + $cache = $this->loadCacheValue($this->instance . $user); + if (!is_null($cache)) { + return $cache; + } + + $url = $this->instance . '/rest/user/' . $user . '?include_fields=real_name'; + try { + $json = json_decode(getContents($url), true); + if (isset($json['error']) and $json['error']) { + throw new Exception(); + } + } catch (Exception $e) { + $this->saveCacheValue($this->instance . 'userEndpointClosed', true); + return $user; + } + + $username = $json['users']['0']['real_name']; + + if (empty($username)) { + $username = $user; + } + $this->saveCacheValue($this->instance . $user, $username); + return $username; + } } diff --git a/bridges/BukowskisBridge.php b/bridges/BukowskisBridge.php index 7b7c36bf..14889889 100644 --- a/bridges/BukowskisBridge.php +++ b/bridges/BukowskisBridge.php @@ -2,217 +2,219 @@ class BukowskisBridge extends BridgeAbstract { - const NAME = 'Bukowskis'; - const URI = 'https://www.bukowskis.com'; - const DESCRIPTION = 'Fetches info about auction objects from Bukowskis auction house'; - const MAINTAINER = 'Qluxzz'; - const PARAMETERS = array(array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'All categories' => '', - 'Art' => array( - 'All' => 'art', - 'Classic Art' => 'art.classic-art', - 'Classic Finnish Art' => 'art.classic-finnish-art', - 'Classic Swedish Art' => 'art.classic-swedish-art', - 'Contemporary' => 'art.contemporary', - 'Modern Finnish Art' => 'art.modern-finnish-art', - 'Modern International Art' => 'art.modern-international-art', - 'Modern Swedish Art' => 'art.modern-swedish-art', - 'Old Masters' => 'art.old-masters', - 'Other' => 'art.other', - 'Photographs' => 'art.photographs', - 'Prints' => 'art.prints', - 'Sculpture' => 'art.sculpture', - 'Swedish Old Masters' => 'art.swedish-old-masters', - ), - 'Asian Ceramics & Works of Art' => array( - 'All' => 'asian-ceramics-works-of-art', - 'Other' => 'asian-ceramics-works-of-art.other', - 'Porcelain' => 'asian-ceramics-works-of-art.porcelain', - ), - 'Books & Manuscripts' => array( - 'All' => 'books-manuscripts', - 'Books' => 'books-manuscripts.books', - ), - 'Carpets, rugs & textiles' => array( - 'All' => 'carpets-rugs-textiles', - 'European' => 'carpets-rugs-textiles.european', - 'Oriental' => 'carpets-rugs-textiles.oriental', - 'Rest of the world' => 'carpets-rugs-textiles.rest-of-the-world', - 'Scandinavian' => 'carpets-rugs-textiles.scandinavian', - ), - 'Ceramics & porcelain' => array( - 'All' => 'ceramics-porcelain', - 'Ceramic ware' => 'ceramics-porcelain.ceramic-ware', - 'European' => 'ceramics-porcelain.european', - 'Rest of the world' => 'ceramics-porcelain.rest-of-the-world', - 'Scandinavian' => 'ceramics-porcelain.scandinavian', - ), - 'Collectibles' => array( - 'All' => 'collectibles', - 'Advertising & Retail' => 'collectibles.advertising-retail', - 'Memorabilia' => 'collectibles.memorabilia', - 'Movies & music' => 'collectibles.movies-music', - 'Other' => 'collectibles.other', - 'Retro & Popular Culture' => 'collectibles.retro-popular-culture', - 'Technica & Nautica' => 'collectibles.technica-nautica', - 'Toys' => 'collectibles.toys', - ), - 'Design' => array( - 'All' => 'design', - 'Art glass' => 'design.art-glass', - 'Furniture' => 'design.furniture', - 'Other' => 'design.other', - ), - 'Folk art' => array( - 'All' => 'folk-art', - 'All categories' => 'lots', - ), - 'Furniture' => array( - 'All' => 'furniture', - 'Armchairs & Sofas' => 'furniture.armchairs-sofas', - 'Cabinets & Bureaus' => 'furniture.cabinets-bureaus', - 'Chairs' => 'furniture.chairs', - 'Garden furniture' => 'furniture.garden-furniture', - 'Mirrors' => 'furniture.mirrors', - 'Other' => 'furniture.other', - 'Shelves & Book cases' => 'furniture.shelves-book-cases', - 'Tables' => 'furniture.tables', - ), - 'Glassware' => array( - 'All' => 'glassware', - 'Glassware' => 'glassware.glassware', - 'Other' => 'glassware.other', - ), - 'Jewellery' => array( - 'All' => 'jewellery', - 'Bracelets' => 'jewellery.bracelets', - 'Brooches' => 'jewellery.brooches', - 'Earrings' => 'jewellery.earrings', - 'Necklaces & Pendants' => 'jewellery.necklaces-pendants', - 'Other' => 'jewellery.other', - 'Rings' => 'jewellery.rings', - ), - 'Lighting' => array( - 'All' => 'lighting', - 'Candle sticks & Candelabras' => 'lighting.candle-sticks-candelabras', - 'Ceiling lights' => 'lighting.ceiling-lights', - 'Chandeliers' => 'lighting.chandeliers', - 'Floor lights' => 'lighting.floor-lights', - 'Other' => 'lighting.other', - 'Table lights' => 'lighting.table-lights', - 'Wall lights' => 'lighting.wall-lights', - ), - 'Militaria' => array( - 'All' => 'militaria', - 'Honors & Medals' => 'militaria.honors-medals', - 'Other militaria' => 'militaria.other-militaria', - 'Weaponry' => 'militaria.weaponry', - ), - 'Miscellaneous' => array( - 'All' => 'miscellaneous', - 'Brass, Copper & Pewter' => 'miscellaneous.brass-copper-pewter', - 'Nickel silver' => 'miscellaneous.nickel-silver', - 'Oriental' => 'miscellaneous.oriental', - 'Other' => 'miscellaneous.other', - ), - 'Silver' => array( - 'All' => 'silver', - 'Candle sticks' => 'silver.candle-sticks', - 'Cups & Bowls' => 'silver.cups-bowls', - 'Cutlery' => 'silver.cutlery', - 'Other' => 'silver.other', - ), - 'Timepieces' => array( - 'All' => 'timepieces', - 'Other' => 'timepieces.other', - 'Pocket watches' => 'timepieces.pocket-watches', - 'Table clocks' => 'timepieces.table-clocks', - 'Wrist watches' => 'timepieces.wrist-watches', - ), - 'Vintage & Fashion' => array( - 'All' => 'vintage-fashion', - 'Accessories' => 'vintage-fashion.accessories', - 'Bags & Trunks' => 'vintage-fashion.bags-trunks', - 'Clothes' => 'vintage-fashion.clothes', - ), - ) - ), - 'sort_order' => array( - 'name' => 'Sort order', - 'type' => 'list', - 'values' => array( - 'Ending soon' => 'ending', - 'Most recent' => 'recent', - 'Most bids' => 'most', - 'Fewest bids' => 'fewest', - 'Lowest price' => 'lowest', - 'Highest price' => 'highest', - 'Lowest estimate' => 'low', - 'Highest estimate' => 'high', - 'Alphabetical' => 'alphabetical', - ), - ), - 'language' => array( - 'name' => 'Language', - 'type' => 'list', - 'values' => array( - 'English' => 'en', - 'Swedish' => 'sv', - 'Finnish' => 'fi' - ), - ), - )); + const NAME = 'Bukowskis'; + const URI = 'https://www.bukowskis.com'; + const DESCRIPTION = 'Fetches info about auction objects from Bukowskis auction house'; + const MAINTAINER = 'Qluxzz'; + const PARAMETERS = [[ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'All categories' => '', + 'Art' => [ + 'All' => 'art', + 'Classic Art' => 'art.classic-art', + 'Classic Finnish Art' => 'art.classic-finnish-art', + 'Classic Swedish Art' => 'art.classic-swedish-art', + 'Contemporary' => 'art.contemporary', + 'Modern Finnish Art' => 'art.modern-finnish-art', + 'Modern International Art' => 'art.modern-international-art', + 'Modern Swedish Art' => 'art.modern-swedish-art', + 'Old Masters' => 'art.old-masters', + 'Other' => 'art.other', + 'Photographs' => 'art.photographs', + 'Prints' => 'art.prints', + 'Sculpture' => 'art.sculpture', + 'Swedish Old Masters' => 'art.swedish-old-masters', + ], + 'Asian Ceramics & Works of Art' => [ + 'All' => 'asian-ceramics-works-of-art', + 'Other' => 'asian-ceramics-works-of-art.other', + 'Porcelain' => 'asian-ceramics-works-of-art.porcelain', + ], + 'Books & Manuscripts' => [ + 'All' => 'books-manuscripts', + 'Books' => 'books-manuscripts.books', + ], + 'Carpets, rugs & textiles' => [ + 'All' => 'carpets-rugs-textiles', + 'European' => 'carpets-rugs-textiles.european', + 'Oriental' => 'carpets-rugs-textiles.oriental', + 'Rest of the world' => 'carpets-rugs-textiles.rest-of-the-world', + 'Scandinavian' => 'carpets-rugs-textiles.scandinavian', + ], + 'Ceramics & porcelain' => [ + 'All' => 'ceramics-porcelain', + 'Ceramic ware' => 'ceramics-porcelain.ceramic-ware', + 'European' => 'ceramics-porcelain.european', + 'Rest of the world' => 'ceramics-porcelain.rest-of-the-world', + 'Scandinavian' => 'ceramics-porcelain.scandinavian', + ], + 'Collectibles' => [ + 'All' => 'collectibles', + 'Advertising & Retail' => 'collectibles.advertising-retail', + 'Memorabilia' => 'collectibles.memorabilia', + 'Movies & music' => 'collectibles.movies-music', + 'Other' => 'collectibles.other', + 'Retro & Popular Culture' => 'collectibles.retro-popular-culture', + 'Technica & Nautica' => 'collectibles.technica-nautica', + 'Toys' => 'collectibles.toys', + ], + 'Design' => [ + 'All' => 'design', + 'Art glass' => 'design.art-glass', + 'Furniture' => 'design.furniture', + 'Other' => 'design.other', + ], + 'Folk art' => [ + 'All' => 'folk-art', + 'All categories' => 'lots', + ], + 'Furniture' => [ + 'All' => 'furniture', + 'Armchairs & Sofas' => 'furniture.armchairs-sofas', + 'Cabinets & Bureaus' => 'furniture.cabinets-bureaus', + 'Chairs' => 'furniture.chairs', + 'Garden furniture' => 'furniture.garden-furniture', + 'Mirrors' => 'furniture.mirrors', + 'Other' => 'furniture.other', + 'Shelves & Book cases' => 'furniture.shelves-book-cases', + 'Tables' => 'furniture.tables', + ], + 'Glassware' => [ + 'All' => 'glassware', + 'Glassware' => 'glassware.glassware', + 'Other' => 'glassware.other', + ], + 'Jewellery' => [ + 'All' => 'jewellery', + 'Bracelets' => 'jewellery.bracelets', + 'Brooches' => 'jewellery.brooches', + 'Earrings' => 'jewellery.earrings', + 'Necklaces & Pendants' => 'jewellery.necklaces-pendants', + 'Other' => 'jewellery.other', + 'Rings' => 'jewellery.rings', + ], + 'Lighting' => [ + 'All' => 'lighting', + 'Candle sticks & Candelabras' => 'lighting.candle-sticks-candelabras', + 'Ceiling lights' => 'lighting.ceiling-lights', + 'Chandeliers' => 'lighting.chandeliers', + 'Floor lights' => 'lighting.floor-lights', + 'Other' => 'lighting.other', + 'Table lights' => 'lighting.table-lights', + 'Wall lights' => 'lighting.wall-lights', + ], + 'Militaria' => [ + 'All' => 'militaria', + 'Honors & Medals' => 'militaria.honors-medals', + 'Other militaria' => 'militaria.other-militaria', + 'Weaponry' => 'militaria.weaponry', + ], + 'Miscellaneous' => [ + 'All' => 'miscellaneous', + 'Brass, Copper & Pewter' => 'miscellaneous.brass-copper-pewter', + 'Nickel silver' => 'miscellaneous.nickel-silver', + 'Oriental' => 'miscellaneous.oriental', + 'Other' => 'miscellaneous.other', + ], + 'Silver' => [ + 'All' => 'silver', + 'Candle sticks' => 'silver.candle-sticks', + 'Cups & Bowls' => 'silver.cups-bowls', + 'Cutlery' => 'silver.cutlery', + 'Other' => 'silver.other', + ], + 'Timepieces' => [ + 'All' => 'timepieces', + 'Other' => 'timepieces.other', + 'Pocket watches' => 'timepieces.pocket-watches', + 'Table clocks' => 'timepieces.table-clocks', + 'Wrist watches' => 'timepieces.wrist-watches', + ], + 'Vintage & Fashion' => [ + 'All' => 'vintage-fashion', + 'Accessories' => 'vintage-fashion.accessories', + 'Bags & Trunks' => 'vintage-fashion.bags-trunks', + 'Clothes' => 'vintage-fashion.clothes', + ], + ] + ], + 'sort_order' => [ + 'name' => 'Sort order', + 'type' => 'list', + 'values' => [ + 'Ending soon' => 'ending', + 'Most recent' => 'recent', + 'Most bids' => 'most', + 'Fewest bids' => 'fewest', + 'Lowest price' => 'lowest', + 'Highest price' => 'highest', + 'Lowest estimate' => 'low', + 'Highest estimate' => 'high', + 'Alphabetical' => 'alphabetical', + ], + ], + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'values' => [ + 'English' => 'en', + 'Swedish' => 'sv', + 'Finnish' => 'fi' + ], + ], + ]]; - const CACHE_TIMEOUT = 3600; // 1 hour + const CACHE_TIMEOUT = 3600; // 1 hour - private $title; + private $title; - public function collectData() - { - $baseUrl = 'https://www.bukowskis.com'; - $category = $this->getInput('category'); - $language = $this->getInput('language'); - $sort_order = $this->getInput('sort_order'); + public function collectData() + { + $baseUrl = 'https://www.bukowskis.com'; + $category = $this->getInput('category'); + $language = $this->getInput('language'); + $sort_order = $this->getInput('sort_order'); - $url = $baseUrl . '/' . $language . '/lots'; + $url = $baseUrl . '/' . $language . '/lots'; - if ($category) - $url = $url . '/category/' . $category; + if ($category) { + $url = $url . '/category/' . $category; + } - if ($sort_order) - $url = $url . '/sort/' . $sort_order; + if ($sort_order) { + $url = $url . '/sort/' . $sort_order; + } - $html = getSimpleHTMLDOM($url); + $html = getSimpleHTMLDOM($url); - $this->title = htmlspecialchars_decode($html->find('title', 0)->innertext); + $this->title = htmlspecialchars_decode($html->find('title', 0)->innertext); - foreach ($html->find('div.c-lot-index-lot') as $lot) { - $title = $lot->find('a.c-lot-index-lot__title', 0)->plaintext; - $relative_url = $lot->find('a.c-lot-index-lot__link', 0)->href; - $images = json_decode( - htmlspecialchars_decode( - $lot - ->find('img.o-aspect-ratio__image', 0) - ->getAttribute('data-thumbnails') - ) - ); + foreach ($html->find('div.c-lot-index-lot') as $lot) { + $title = $lot->find('a.c-lot-index-lot__title', 0)->plaintext; + $relative_url = $lot->find('a.c-lot-index-lot__link', 0)->href; + $images = json_decode( + htmlspecialchars_decode( + $lot + ->find('img.o-aspect-ratio__image', 0) + ->getAttribute('data-thumbnails') + ) + ); - $this->items[] = array( - 'title' => $title, - 'uri' => $baseUrl . $relative_url, - 'uid' => $lot->getAttribute('data-lot-id'), - 'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title, - 'enclosures' => array_slice($images, 1), - ); - } - } + $this->items[] = [ + 'title' => $title, + 'uri' => $baseUrl . $relative_url, + 'uid' => $lot->getAttribute('data-lot-id'), + 'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title, + 'enclosures' => array_slice($images, 1), + ]; + } + } - public function getName() - { - return $this->title ?: parent::getName(); - } + public function getName() + { + return $this->title ?: parent::getName(); + } } diff --git a/bridges/BundesbankBridge.php b/bridges/BundesbankBridge.php index dab7893c..4335cb69 100644 --- a/bridges/BundesbankBridge.php +++ b/bridges/BundesbankBridge.php @@ -1,84 +1,88 @@ <?php -class BundesbankBridge extends BridgeAbstract { - const PARAM_LANG = 'lang'; - - const LANG_EN = 'en'; - const LANG_DE = 'de'; - - const NAME = 'Bundesbank Bridge'; - const URI = 'https://www.bundesbank.de/'; - const DESCRIPTION = 'Returns the latest studies of the Bundesbank (Germany)'; - const MAINTAINER = 'logmanoriginal'; - const CACHE_TIMEOUT = 86400; // 24 hours - - const PARAMETERS = array( - array( - self::PARAM_LANG => array( - 'name' => 'Language', - 'type' => 'list', - 'defaultValue' => self::LANG_DE, - 'values' => array( - 'English' => self::LANG_EN, - 'Deutsch' => self::LANG_DE - ) - ) - ) - ); - - public function getIcon() { - return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico'; - } - - public function getURI() { - switch($this->getInput(self::PARAM_LANG)) { - case self::LANG_EN: return self::URI . 'en/publications/reports/studies'; - case self::LANG_DE: return self::URI . 'de/publikationen/berichte/studien'; - } - - return parent::getURI(); - } - - public function collectData() { - - $html = getSimpleHTMLDOM($this->getURI()); - - $html = defaultLinkTo($html, $this->getURI()); - - foreach($html->find('ul.resultlist li') as $study) { - $item = array(); - - $item['uri'] = $study->find('.teasable__link', 0)->href; - - // Get title without child elements (i.e. subtitle) - $title = $study->find('.teasable__title div.h2', 0); - - foreach($title->children as &$child) { - $child->outertext = ''; - } - - $item['title'] = $title->innertext; - - // Add subtitle to the content if it exists - $item['content'] = ''; - - if($subtitle = $study->find('.teasable__subtitle', 0)) { - $item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>'; - } - - $item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>'; - - $item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext); - - // Downloads and older studies don't have images - if($study->find('.teasable__image', 0)) { - $item['enclosures'] = array( - $study->find('.teasable__image img', 0)->src - ); - } - - $this->items[] = $item; - } - - } +class BundesbankBridge extends BridgeAbstract +{ + const PARAM_LANG = 'lang'; + + const LANG_EN = 'en'; + const LANG_DE = 'de'; + + const NAME = 'Bundesbank Bridge'; + const URI = 'https://www.bundesbank.de/'; + const DESCRIPTION = 'Returns the latest studies of the Bundesbank (Germany)'; + const MAINTAINER = 'logmanoriginal'; + const CACHE_TIMEOUT = 86400; // 24 hours + + const PARAMETERS = [ + [ + self::PARAM_LANG => [ + 'name' => 'Language', + 'type' => 'list', + 'defaultValue' => self::LANG_DE, + 'values' => [ + 'English' => self::LANG_EN, + 'Deutsch' => self::LANG_DE + ] + ] + ] + ]; + + public function getIcon() + { + return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico'; + } + + public function getURI() + { + switch ($this->getInput(self::PARAM_LANG)) { + case self::LANG_EN: + return self::URI . 'en/publications/reports/studies'; + case self::LANG_DE: + return self::URI . 'de/publikationen/berichte/studien'; + } + + return parent::getURI(); + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $html = defaultLinkTo($html, $this->getURI()); + + foreach ($html->find('ul.resultlist li') as $study) { + $item = []; + + $item['uri'] = $study->find('.teasable__link', 0)->href; + + // Get title without child elements (i.e. subtitle) + $title = $study->find('.teasable__title div.h2', 0); + + foreach ($title->children as &$child) { + $child->outertext = ''; + } + + $item['title'] = $title->innertext; + + // Add subtitle to the content if it exists + $item['content'] = ''; + + if ($subtitle = $study->find('.teasable__subtitle', 0)) { + $item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>'; + } + + $item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>'; + + $item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext); + + // Downloads and older studies don't have images + if ($study->find('.teasable__image', 0)) { + $item['enclosures'] = [ + $study->find('.teasable__image img', 0)->src + ]; + } + + $this->items[] = $item; + } + } } diff --git a/bridges/BundestagParteispendenBridge.php b/bridges/BundestagParteispendenBridge.php index 1af24e01..cdf398e8 100644 --- a/bridges/BundestagParteispendenBridge.php +++ b/bridges/BundestagParteispendenBridge.php @@ -1,89 +1,94 @@ <?php -class BundestagParteispendenBridge extends BridgeAbstract { - const MAINTAINER = 'mibe'; - const NAME = 'Deutscher Bundestag - Parteispenden'; - const URI = 'https://www.bundestag.de/parlament/praesidium/parteienfinanzierung/fundstellen50000'; - - const CACHE_TIMEOUT = 86400; // 24h - const DESCRIPTION = 'Returns the latest "soft money" donations to parties represented in the German Bundestag.'; - const CONTENT_TEMPLATE = <<<TMPL + +class BundestagParteispendenBridge extends BridgeAbstract +{ + const MAINTAINER = 'mibe'; + const NAME = 'Deutscher Bundestag - Parteispenden'; + const URI = 'https://www.bundestag.de/parlament/praesidium/parteienfinanzierung/fundstellen50000'; + + const CACHE_TIMEOUT = 86400; // 24h + const DESCRIPTION = 'Returns the latest "soft money" donations to parties represented in the German Bundestag.'; + const CONTENT_TEMPLATE = <<<TMPL <p><b>Partei:</b><br>%s</p> <p><b>Spendenbetrag:</b><br>%s</p> <p><b>Spender:</b><br>%s</p> <p><b>Eingang der Spende:</b><br>%s</p> TMPL; - public function getIcon() - { - return 'https://www.bundestag.de/static/appdata/includes/images/layout/favicon.ico'; - } + public function getIcon() + { + return 'https://www.bundestag.de/static/appdata/includes/images/layout/favicon.ico'; + } - public function collectData() - { - $ajaxUri = <<<URI + public function collectData() + { + $ajaxUri = <<<URI https://www.bundestag.de/ajax/filterlist/de/parlament/praesidium/parteienfinanzierung/fundstellen50000/462002-462002 URI; - // Get the main page - $html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT) - or returnServerError('Could not request AJAX list.'); - - // Build the URL from the first anchor element. The list is sorted by year, descending, so the first element is the current year. - $firstAnchor = $html->find('a', 0) - or returnServerError('Could not find the proper HTML element.'); - - $url = 'https://www.bundestag.de' . $firstAnchor->href; - - // Get the actual page with the soft money donations - $html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT) - or returnServerError('Could not request ' . $url); - - $rows = $html->find('table.table > tbody > tr') - or returnServerError('Could not find the proper HTML elements.'); - - foreach($rows as $row) { - $item = $this->generateItemFromRow($row); - if (is_array($item)) { - $item['uri'] = $url; - $this->items[] = $item; - } - } - } - - private function generateItemFromRow(simple_html_dom_node $row) - { - // The row must have 5 columns. There are monthly header rows, which are ignored here. - if(count($row->children) != 5) - return null; - - $item = array(); - - // | column | paragraph inside column - $party = $row->children[0]->children[0]->innertext; - $amount = $row->children[1]->children[0]->innertext . ' €'; - $donor = $row->children[2]->children[0]->innertext; - $date = $row->children[3]->children[0]->innertext; - $dip = $row->children[4]->children[0]->find('a.dipLink', 0); - - // Strip whitespace from date string. - $date = str_replace(' ', '', $date); - - $content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date); - - $item = array( - 'title' => $party . ': ' . $amount, - 'content' => $content, - 'uid' => sha1($content), - ); - - // Try to get the link to the official document - if ($dip != null) - $item['enclosures'] = array($dip->href); - - // Try to parse the date - $dateTime = DateTime::createFromFormat('d.m.Y', $date); - if ($dateTime !== false) - $item['timestamp'] = $dateTime->getTimestamp(); - - return $item; - } + // Get the main page + $html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT) + or returnServerError('Could not request AJAX list.'); + + // Build the URL from the first anchor element. The list is sorted by year, descending, so the first element is the current year. + $firstAnchor = $html->find('a', 0) + or returnServerError('Could not find the proper HTML element.'); + + $url = 'https://www.bundestag.de' . $firstAnchor->href; + + // Get the actual page with the soft money donations + $html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT) + or returnServerError('Could not request ' . $url); + + $rows = $html->find('table.table > tbody > tr') + or returnServerError('Could not find the proper HTML elements.'); + + foreach ($rows as $row) { + $item = $this->generateItemFromRow($row); + if (is_array($item)) { + $item['uri'] = $url; + $this->items[] = $item; + } + } + } + + private function generateItemFromRow(simple_html_dom_node $row) + { + // The row must have 5 columns. There are monthly header rows, which are ignored here. + if (count($row->children) != 5) { + return null; + } + + $item = []; + + // | column | paragraph inside column + $party = $row->children[0]->children[0]->innertext; + $amount = $row->children[1]->children[0]->innertext . ' €'; + $donor = $row->children[2]->children[0]->innertext; + $date = $row->children[3]->children[0]->innertext; + $dip = $row->children[4]->children[0]->find('a.dipLink', 0); + + // Strip whitespace from date string. + $date = str_replace(' ', '', $date); + + $content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date); + + $item = [ + 'title' => $party . ': ' . $amount, + 'content' => $content, + 'uid' => sha1($content), + ]; + + // Try to get the link to the official document + if ($dip != null) { + $item['enclosures'] = [$dip->href]; + } + + // Try to parse the date + $dateTime = DateTime::createFromFormat('d.m.Y', $date); + if ($dateTime !== false) { + $item['timestamp'] = $dateTime->getTimestamp(); + } + + return $item; + } } diff --git a/bridges/CBCEditorsBlogBridge.php b/bridges/CBCEditorsBlogBridge.php index c7feb344..a9c0a4dc 100644 --- a/bridges/CBCEditorsBlogBridge.php +++ b/bridges/CBCEditorsBlogBridge.php @@ -1,36 +1,38 @@ <?php -class CBCEditorsBlogBridge extends BridgeAbstract { - const MAINTAINER = 'quickwick'; - const NAME = 'CBC Editors Blog'; - const URI = 'https://www.cbc.ca/news/editorsblog'; - const DESCRIPTION = 'Recent CBC Editor\'s Blog posts'; +class CBCEditorsBlogBridge extends BridgeAbstract +{ + const MAINTAINER = 'quickwick'; + const NAME = 'CBC Editors Blog'; + const URI = 'https://www.cbc.ca/news/editorsblog'; + const DESCRIPTION = 'Recent CBC Editor\'s Blog posts'; - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - // Loop on each blog post entry - foreach($html->find('div.contentListCards', 0)->find('a[data-test=type-story]') as $element) { - $headline = ($element->find('.headline', 0))->innertext; - $timestamp = ($element->find('time', 0))->datetime; - $articleUri = 'https://www.cbc.ca' . $element->href; - $summary = ($element->find('div.description', 0))->innertext; - $thumbnailUris = ($element->find('img[loading=lazy]', 0))->srcset; - $thumbnailUri = rtrim(explode(',', $thumbnailUris)[0], ' 300w'); + // Loop on each blog post entry + foreach ($html->find('div.contentListCards', 0)->find('a[data-test=type-story]') as $element) { + $headline = ($element->find('.headline', 0))->innertext; + $timestamp = ($element->find('time', 0))->datetime; + $articleUri = 'https://www.cbc.ca' . $element->href; + $summary = ($element->find('div.description', 0))->innertext; + $thumbnailUris = ($element->find('img[loading=lazy]', 0))->srcset; + $thumbnailUri = rtrim(explode(',', $thumbnailUris)[0], ' 300w'); - // Fill item - $item = array(); - $item['uri'] = $articleUri; - $item['id'] = $item['uri']; - $item['timestamp'] = $timestamp; - $item['title'] = $headline; - $item['content'] = '<img src="' - . $thumbnailUri . '" /><br>' . $summary; - $item['author'] = 'Editor\'s Blog'; + // Fill item + $item = []; + $item['uri'] = $articleUri; + $item['id'] = $item['uri']; + $item['timestamp'] = $timestamp; + $item['title'] = $headline; + $item['content'] = '<img src="' + . $thumbnailUri . '" /><br>' . $summary; + $item['author'] = 'Editor\'s Blog'; - if(isset($item['title'])) { - $this->items[] = $item; - } - } - } + if (isset($item['title'])) { + $this->items[] = $item; + } + } + } } diff --git a/bridges/CNETBridge.php b/bridges/CNETBridge.php index 27946f25..34442abd 100644 --- a/bridges/CNETBridge.php +++ b/bridges/CNETBridge.php @@ -1,108 +1,114 @@ <?php -class CNETBridge extends BridgeAbstract { - const MAINTAINER = 'ORelio'; - const NAME = 'CNET News'; - const URI = 'https://www.cnet.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns the newest articles.'; - const PARAMETERS = array( - array( - 'topic' => array( - 'name' => 'Topic', - 'type' => 'list', - 'values' => array( - 'All articles' => '', - 'Apple' => 'apple', - 'Google' => 'google', - 'Microsoft' => 'tags-microsoft', - 'Computers' => 'topics-computers', - 'Mobile' => 'topics-mobile', - 'Sci-Tech' => 'topics-sci-tech', - 'Security' => 'topics-security', - 'Internet' => 'topics-internet', - 'Tech Industry' => 'topics-tech-industry' - ) - ) - ) - ); +class CNETBridge extends BridgeAbstract +{ + const MAINTAINER = 'ORelio'; + const NAME = 'CNET News'; + const URI = 'https://www.cnet.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns the newest articles.'; + const PARAMETERS = [ + [ + 'topic' => [ + 'name' => 'Topic', + 'type' => 'list', + 'values' => [ + 'All articles' => '', + 'Apple' => 'apple', + 'Google' => 'google', + 'Microsoft' => 'tags-microsoft', + 'Computers' => 'topics-computers', + 'Mobile' => 'topics-mobile', + 'Sci-Tech' => 'topics-sci-tech', + 'Security' => 'topics-security', + 'Internet' => 'topics-internet', + 'Tech Industry' => 'topics-tech-industry' + ] + ] + ] + ]; - private function cleanArticle($article_html) { - $offset_p = strpos($article_html, '<p>'); - $offset_figure = strpos($article_html, '<figure'); - $offset = ($offset_figure < $offset_p ? $offset_figure : $offset_p); - $article_html = substr($article_html, $offset); - $article_html = str_replace('href="/', 'href="' . self::URI, $article_html); - $article_html = str_replace(' height="0"', '', $article_html); - $article_html = str_replace('<noscript>', '', $article_html); - $article_html = str_replace('</noscript>', '', $article_html); - $article_html = StripWithDelimiters($article_html, '<a class="clickToEnlarge', '</a>'); - $article_html = stripWithDelimiters($article_html, '<span class="nowPlaying', '</span>'); - $article_html = stripWithDelimiters($article_html, '<span class="duration', '</span>'); - $article_html = stripWithDelimiters($article_html, '<script', '</script>'); - $article_html = stripWithDelimiters($article_html, '<svg', '</svg>'); - return $article_html; - } + private function cleanArticle($article_html) + { + $offset_p = strpos($article_html, '<p>'); + $offset_figure = strpos($article_html, '<figure'); + $offset = ($offset_figure < $offset_p ? $offset_figure : $offset_p); + $article_html = substr($article_html, $offset); + $article_html = str_replace('href="/', 'href="' . self::URI, $article_html); + $article_html = str_replace(' height="0"', '', $article_html); + $article_html = str_replace('<noscript>', '', $article_html); + $article_html = str_replace('</noscript>', '', $article_html); + $article_html = StripWithDelimiters($article_html, '<a class="clickToEnlarge', '</a>'); + $article_html = stripWithDelimiters($article_html, '<span class="nowPlaying', '</span>'); + $article_html = stripWithDelimiters($article_html, '<span class="duration', '</span>'); + $article_html = stripWithDelimiters($article_html, '<script', '</script>'); + $article_html = stripWithDelimiters($article_html, '<svg', '</svg>'); + return $article_html; + } - public function collectData() { + public function collectData() + { + // Retrieve and check user input + $topic = str_replace('-', '/', $this->getInput('topic')); + if (!empty($topic) && (substr_count($topic, '/') > 1 || !ctype_alpha(str_replace('/', '', $topic)))) { + returnClientError('Invalid topic: ' . $topic); + } - // Retrieve and check user input - $topic = str_replace('-', '/', $this->getInput('topic')); - if (!empty($topic) && (substr_count($topic, '/') > 1 || !ctype_alpha(str_replace('/', '', $topic)))) - returnClientError('Invalid topic: ' . $topic); + // Retrieve webpage + $pageUrl = self::URI . (empty($topic) ? 'news/' : $topic . '/'); + $html = getSimpleHTMLDOM($pageUrl); - // Retrieve webpage - $pageUrl = self::URI . (empty($topic) ? 'news/' : $topic . '/'); - $html = getSimpleHTMLDOM($pageUrl); + // Process articles + foreach ($html->find('div.assetBody, div.riverPost') as $element) { + if (count($this->items) >= 10) { + break; + } - // Process articles - foreach($html->find('div.assetBody, div.riverPost') as $element) { + $article_title = trim($element->find('h2, h3', 0)->plaintext); + $article_uri = self::URI . substr($element->find('a', 0)->href, 1); + $article_thumbnail = $element->parent()->find('img[src]', 0)->src; + $article_timestamp = strtotime($element->find('time.assetTime, div.timeAgo', 0)->plaintext); + $article_author = trim($element->find('a[rel=author], a.name', 0)->plaintext); + $article_content = '<p><b>' . trim($element->find('p.dek', 0)->plaintext) . '</b></p>'; - if(count($this->items) >= 10) { - break; - } + if (is_null($article_thumbnail)) { + $article_thumbnail = extractFromDelimiters($element->innertext, '<img src="', '"'); + } - $article_title = trim($element->find('h2, h3', 0)->plaintext); - $article_uri = self::URI . substr($element->find('a', 0)->href, 1); - $article_thumbnail = $element->parent()->find('img[src]', 0)->src; - $article_timestamp = strtotime($element->find('time.assetTime, div.timeAgo', 0)->plaintext); - $article_author = trim($element->find('a[rel=author], a.name', 0)->plaintext); - $article_content = '<p><b>' . trim($element->find('p.dek', 0)->plaintext) . '</b></p>'; + if (!empty($article_title) && !empty($article_uri) && strpos($article_uri, self::URI . 'news/') !== false) { + $article_html = getSimpleHTMLDOMCached($article_uri) or $article_html = null; - if (is_null($article_thumbnail)) - $article_thumbnail = extractFromDelimiters($element->innertext, '<img src="', '"'); + if (!is_null($article_html)) { + if (empty($article_thumbnail)) { + $article_thumbnail = $article_html->find('div.originalImage', 0); + } + if (empty($article_thumbnail)) { + $article_thumbnail = $article_html->find('span.imageContainer', 0); + } + if (is_object($article_thumbnail)) { + $article_thumbnail = $article_thumbnail->find('img', 0)->src; + } - if (!empty($article_title) && !empty($article_uri) && strpos($article_uri, self::URI . 'news/') !== false) { + $article_content .= trim( + $this->cleanArticle( + extractFromDelimiters( + $article_html, + '<article', + '<footer' + ) + ) + ); + } - $article_html = getSimpleHTMLDOMCached($article_uri) or $article_html = null; - - if (!is_null($article_html)) { - - if (empty($article_thumbnail)) - $article_thumbnail = $article_html->find('div.originalImage', 0); - if (empty($article_thumbnail)) - $article_thumbnail = $article_html->find('span.imageContainer', 0); - if (is_object($article_thumbnail)) - $article_thumbnail = $article_thumbnail->find('img', 0)->src; - - $article_content .= trim( - $this->cleanArticle( - extractFromDelimiters( - $article_html, '<article', '<footer' - ) - ) - ); - } - - $item = array(); - $item['uri'] = $article_uri; - $item['title'] = $article_title; - $item['author'] = $article_author; - $item['timestamp'] = $article_timestamp; - $item['enclosures'] = array($article_thumbnail); - $item['content'] = $article_content; - $this->items[] = $item; - } - } - } + $item = []; + $item['uri'] = $article_uri; + $item['title'] = $article_title; + $item['author'] = $article_author; + $item['timestamp'] = $article_timestamp; + $item['enclosures'] = [$article_thumbnail]; + $item['content'] = $article_content; + $this->items[] = $item; + } + } + } } diff --git a/bridges/CNETFranceBridge.php b/bridges/CNETFranceBridge.php index 9195d1b4..724564fa 100644 --- a/bridges/CNETFranceBridge.php +++ b/bridges/CNETFranceBridge.php @@ -1,63 +1,64 @@ <?php + class CNETFranceBridge extends FeedExpander { - const MAINTAINER = 'leomaradan'; - const NAME = 'CNET France'; - const URI = 'https://www.cnetfrance.fr/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'CNET France RSS with filters'; - const PARAMETERS = array( - 'filters' => array( - 'title' => array( - 'name' => 'Exclude by title', - 'required' => false, - 'title' => 'Title term, separated by semicolon (;)', - 'exampleValue' => 'bon plan;bons plans;au meilleur prix;des meilleures offres;Amazon Prime Day;RED by SFR ou B&You' - ), - 'url' => array( - 'name' => 'Exclude by url', - 'required' => false, - 'title' => 'URL term, separated by semicolon (;)', - 'exampleValue' => 'bon-plan;bons-plans' - ) - ) - ); - - private $bannedTitle = array(); - private $bannedURL = array(); - - public function collectData() - { - $title = $this->getInput('title'); - $url = $this->getInput('url'); - - if ($title !== null) { - $this->bannedTitle = explode(';', $title); - } - - if ($url !== null) { - $this->bannedURL = explode(';', $url); - } - - $this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/'); - } - - protected function parseItem($feedItem) - { - $item = parent::parseItem($feedItem); - - foreach ($this->bannedTitle as $term) { - if (preg_match('/' . $term . '/mi', $item['title']) === 1) { - return null; - } - } - - foreach ($this->bannedURL as $term) { - if (preg_match('/' . $term . '/mi', $item['uri']) === 1) { - return null; - } - } - - return $item; - } + const MAINTAINER = 'leomaradan'; + const NAME = 'CNET France'; + const URI = 'https://www.cnetfrance.fr/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'CNET France RSS with filters'; + const PARAMETERS = [ + 'filters' => [ + 'title' => [ + 'name' => 'Exclude by title', + 'required' => false, + 'title' => 'Title term, separated by semicolon (;)', + 'exampleValue' => 'bon plan;bons plans;au meilleur prix;des meilleures offres;Amazon Prime Day;RED by SFR ou B&You' + ], + 'url' => [ + 'name' => 'Exclude by url', + 'required' => false, + 'title' => 'URL term, separated by semicolon (;)', + 'exampleValue' => 'bon-plan;bons-plans' + ] + ] + ]; + + private $bannedTitle = []; + private $bannedURL = []; + + public function collectData() + { + $title = $this->getInput('title'); + $url = $this->getInput('url'); + + if ($title !== null) { + $this->bannedTitle = explode(';', $title); + } + + if ($url !== null) { + $this->bannedURL = explode(';', $url); + } + + $this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/'); + } + + protected function parseItem($feedItem) + { + $item = parent::parseItem($feedItem); + + foreach ($this->bannedTitle as $term) { + if (preg_match('/' . $term . '/mi', $item['title']) === 1) { + return null; + } + } + + foreach ($this->bannedURL as $term) { + if (preg_match('/' . $term . '/mi', $item['uri']) === 1) { + return null; + } + } + + return $item; + } } diff --git a/bridges/CVEDetailsBridge.php b/bridges/CVEDetailsBridge.php index 18da49bd..38b37bb7 100644 --- a/bridges/CVEDetailsBridge.php +++ b/bridges/CVEDetailsBridge.php @@ -7,134 +7,139 @@ // it is not reliable and contain no useful information. This bridge create a // sane feed with additional information like tags and a link to the CWE // a description of the vulnerability. -class CVEDetailsBridge extends BridgeAbstract { - const MAINTAINER = 'Aaron Fischer'; - const NAME = 'CVE Details'; - const CACHE_TIMEOUT = 60 * 60 * 6; // 6 hours - const DESCRIPTION = 'Report new CVE vulnerabilities for a given vendor (and product)'; - const URI = 'https://www.cvedetails.com'; - - const PARAMETERS = array(array( - // The Vendor ID can be taken from the URL - 'vendor_id' => array( - 'name' => 'Vendor ID', - 'type' => 'number', - 'required' => true, - 'exampleValue' => 74, // PHP - ), - // The optional Product ID can be taken from the URL as well - 'product_id' => array( - 'name' => 'Product ID', - 'type' => 'number', - 'required' => false, - 'exampleValue' => 128, // PHP - ), - )); - - private $html = null; - private $vendor = ''; - private $product = ''; - - // Return the URL to query. - // Because of the optional product ID, we need to attach it if it is - // set. The search result page has the exact same structure (with and - // without the product ID). - private function buildUrl() { - $url = self::URI . '/vulnerability-list/vendor_id-' . $this->getInput('vendor_id'); - if ($this->getInput('product_id') !== '') { - $url .= '/product_id-' . $this->getInput('product_id'); - } - // Sadly, there is no way (prove me wrong please) to sort the search - // result by publish date. So the nearest alternative is the CVE - // number, which should be mostly accurate. - $url .= '?order=1'; // Order by CVE number DESC - - return $url; - } - - // Make the actual request to cvedetails.com and stores the response - // (HTML) for later use and extract vendor and product from it. - private function fetchContent() { - $html = getSimpleHTMLDOM($this->buildUrl()); - $this->html = defaultLinkTo($html, self::URI); - - $vendor = $html->find('#contentdiv > h1 > a', 0); - if ($vendor == null) { - returnServerError('Invalid Vendor ID ' . - $this->getInput('vendor_id') . - ' or Product ID ' . - $this->getInput('product_id')); - } - $this->vendor = $vendor->innertext; - - $product = $html->find('#contentdiv > h1 > a', 1); - if ($product != null) { - $this->product = $product->innertext; - } - } - - // Build the name of the feed. - public function getName() { - if ($this->getInput('vendor_id') == '') { - return self::NAME; - } - - if ($this->html == null) { - $this->fetchContent(); - } - - $name = 'CVE Vulnerabilities for ' . $this->vendor; - if ($this->product != '') { - $name .= '/' . $this->product; - } - - return $name; - } - - // Pull the data from the HTML response and fill the items.. - public function collectData() { - if ($this->html == null) { - $this->fetchContent(); - } - - foreach ($this->html->find('#vulnslisttable .srrowns') as $i => $tr) { - // There are some optional vulnerability types, which will be - // added to the categories as well as the CWE number -- which is - // always given. - $categories = array($this->vendor); - $enclosures = array(); - - $cwe = $tr->find('td', 2)->find('a', 0); - if ($cwe != null) { - $cwe = $cwe->innertext; - $categories[] = 'CWE-' . $cwe; - $enclosures[] = 'https://cwe.mitre.org/data/definitions/' . $cwe . '.html'; - } - $c = $tr->find('td', 4)->innertext; - if (trim($c) != '') { - $categories[] = $c; - } - if ($this->product != '') { - $categories[] = $this->product; - } - - // The CVE number itself - $title = $tr->find('td', 1)->find('a', 0)->innertext; - - $this->items[] = array( - 'uri' => $tr->find('td', 1)->find('a', 0)->href, - 'title' => $title, - 'timestamp' => $tr->find('td', 5)->innertext, - 'content' => $tr->next_sibling()->innertext, - 'categories' => $categories, - 'enclosures' => $enclosures, - 'uid' => $tr->find('td', 1)->find('a', 0)->innertext, - ); - - // We only want to fetch the latest 10 CVEs - if (count($this->items) >= 10) { - break; - } - } - } +class CVEDetailsBridge extends BridgeAbstract +{ + const MAINTAINER = 'Aaron Fischer'; + const NAME = 'CVE Details'; + const CACHE_TIMEOUT = 60 * 60 * 6; // 6 hours + const DESCRIPTION = 'Report new CVE vulnerabilities for a given vendor (and product)'; + const URI = 'https://www.cvedetails.com'; + + const PARAMETERS = [[ + // The Vendor ID can be taken from the URL + 'vendor_id' => [ + 'name' => 'Vendor ID', + 'type' => 'number', + 'required' => true, + 'exampleValue' => 74, // PHP + ], + // The optional Product ID can be taken from the URL as well + 'product_id' => [ + 'name' => 'Product ID', + 'type' => 'number', + 'required' => false, + 'exampleValue' => 128, // PHP + ], + ]]; + + private $html = null; + private $vendor = ''; + private $product = ''; + + // Return the URL to query. + // Because of the optional product ID, we need to attach it if it is + // set. The search result page has the exact same structure (with and + // without the product ID). + private function buildUrl() + { + $url = self::URI . '/vulnerability-list/vendor_id-' . $this->getInput('vendor_id'); + if ($this->getInput('product_id') !== '') { + $url .= '/product_id-' . $this->getInput('product_id'); + } + // Sadly, there is no way (prove me wrong please) to sort the search + // result by publish date. So the nearest alternative is the CVE + // number, which should be mostly accurate. + $url .= '?order=1'; // Order by CVE number DESC + + return $url; + } + + // Make the actual request to cvedetails.com and stores the response + // (HTML) for later use and extract vendor and product from it. + private function fetchContent() + { + $html = getSimpleHTMLDOM($this->buildUrl()); + $this->html = defaultLinkTo($html, self::URI); + + $vendor = $html->find('#contentdiv > h1 > a', 0); + if ($vendor == null) { + returnServerError('Invalid Vendor ID ' . + $this->getInput('vendor_id') . + ' or Product ID ' . + $this->getInput('product_id')); + } + $this->vendor = $vendor->innertext; + + $product = $html->find('#contentdiv > h1 > a', 1); + if ($product != null) { + $this->product = $product->innertext; + } + } + + // Build the name of the feed. + public function getName() + { + if ($this->getInput('vendor_id') == '') { + return self::NAME; + } + + if ($this->html == null) { + $this->fetchContent(); + } + + $name = 'CVE Vulnerabilities for ' . $this->vendor; + if ($this->product != '') { + $name .= '/' . $this->product; + } + + return $name; + } + + // Pull the data from the HTML response and fill the items.. + public function collectData() + { + if ($this->html == null) { + $this->fetchContent(); + } + + foreach ($this->html->find('#vulnslisttable .srrowns') as $i => $tr) { + // There are some optional vulnerability types, which will be + // added to the categories as well as the CWE number -- which is + // always given. + $categories = [$this->vendor]; + $enclosures = []; + + $cwe = $tr->find('td', 2)->find('a', 0); + if ($cwe != null) { + $cwe = $cwe->innertext; + $categories[] = 'CWE-' . $cwe; + $enclosures[] = 'https://cwe.mitre.org/data/definitions/' . $cwe . '.html'; + } + $c = $tr->find('td', 4)->innertext; + if (trim($c) != '') { + $categories[] = $c; + } + if ($this->product != '') { + $categories[] = $this->product; + } + + // The CVE number itself + $title = $tr->find('td', 1)->find('a', 0)->innertext; + + $this->items[] = [ + 'uri' => $tr->find('td', 1)->find('a', 0)->href, + 'title' => $title, + 'timestamp' => $tr->find('td', 5)->innertext, + 'content' => $tr->next_sibling()->innertext, + 'categories' => $categories, + 'enclosures' => $enclosures, + 'uid' => $tr->find('td', 1)->find('a', 0)->innertext, + ]; + + // We only want to fetch the latest 10 CVEs + if (count($this->items) >= 10) { + break; + } + } + } } diff --git a/bridges/CachetBridge.php b/bridges/CachetBridge.php index 75b18017..355e7926 100644 --- a/bridges/CachetBridge.php +++ b/bridges/CachetBridge.php @@ -1,134 +1,138 @@ <?php -class CachetBridge extends BridgeAbstract { - const NAME = 'Cachet Bridge'; - const URI = 'https://cachethq.io/'; - const DESCRIPTION = 'Returns status updates from any Cachet installation'; - const MAINTAINER = 'klimplant'; - const PARAMETERS = array( - array( - 'host' => array( - 'name' => 'Cachet installation', - 'type' => 'text', - 'required' => true, - 'title' => 'The URL of the Cachet installation', - 'exampleValue' => 'https://demo.cachethq.io/', - ), 'additional_info' => array( - 'name' => 'Additional Timestamps', - 'type' => 'checkbox', - 'title' => 'Whether to include the given timestamps' - ) - ) - ); - const CACHE_TIMEOUT = 300; +class CachetBridge extends BridgeAbstract +{ + const NAME = 'Cachet Bridge'; + const URI = 'https://cachethq.io/'; + const DESCRIPTION = 'Returns status updates from any Cachet installation'; + const MAINTAINER = 'klimplant'; + const PARAMETERS = [ + [ + 'host' => [ + 'name' => 'Cachet installation', + 'type' => 'text', + 'required' => true, + 'title' => 'The URL of the Cachet installation', + 'exampleValue' => 'https://demo.cachethq.io/', + ], 'additional_info' => [ + 'name' => 'Additional Timestamps', + 'type' => 'checkbox', + 'title' => 'Whether to include the given timestamps' + ] + ] + ]; + const CACHE_TIMEOUT = 300; - private $componentCache = array(); + private $componentCache = []; - public function getURI() { - return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host'); - } + public function getURI() + { + return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host'); + } - /** - * Validates the ping request to the cache API - * - * @param string $ping - * @return boolean - */ - private function validatePing($ping) { - $ping = json_decode($ping); - if ($ping === null) { - return false; - } - return $ping->data === 'Pong!'; - } + /** + * Validates the ping request to the cache API + * + * @param string $ping + * @return boolean + */ + private function validatePing($ping) + { + $ping = json_decode($ping); + if ($ping === null) { + return false; + } + return $ping->data === 'Pong!'; + } - /** - * Returns the component name of a cachat component - * - * @param integer $id - * @return string - */ - private function getComponentName($id) { - if ($id === 0) { - return ''; - } - if (array_key_exists($id, $this->componentCache)) { - return $this->componentCache[$id]; - } + /** + * Returns the component name of a cachat component + * + * @param integer $id + * @return string + */ + private function getComponentName($id) + { + if ($id === 0) { + return ''; + } + if (array_key_exists($id, $this->componentCache)) { + return $this->componentCache[$id]; + } - $component = getContents($this->getURI() . '/api/v1/components/' . $id); - $component = json_decode($component); - if ($component === null) { - return ''; - } - return $component->data->name; - } + $component = getContents($this->getURI() . '/api/v1/components/' . $id); + $component = json_decode($component); + if ($component === null) { + return ''; + } + return $component->data->name; + } - public function collectData() { - $ping = getContents(urljoin($this->getURI(), '/api/v1/ping')); - if (!$this->validatePing($ping)) { - returnClientError('Provided URI is invalid!'); - } + public function collectData() + { + $ping = getContents(urljoin($this->getURI(), '/api/v1/ping')); + if (!$this->validatePing($ping)) { + returnClientError('Provided URI is invalid!'); + } - $url = urljoin($this->getURI(), '/api/v1/incidents?sort=id&order=desc'); - $incidents = getContents($url); - $incidents = json_decode($incidents); - if ($incidents === null) { - returnClientError('/api/v1/incidents returned no valid json'); - } + $url = urljoin($this->getURI(), '/api/v1/incidents?sort=id&order=desc'); + $incidents = getContents($url); + $incidents = json_decode($incidents); + if ($incidents === null) { + returnClientError('/api/v1/incidents returned no valid json'); + } - usort($incidents->data, function ($a, $b) { - $timeA = strtotime($a->updated_at); - $timeB = strtotime($b->updated_at); - return $timeA > $timeB ? -1 : 1; - }); + usort($incidents->data, function ($a, $b) { + $timeA = strtotime($a->updated_at); + $timeB = strtotime($b->updated_at); + return $timeA > $timeB ? -1 : 1; + }); - foreach ($incidents->data as $incident) { + foreach ($incidents->data as $incident) { + if (isset($incident->permalink)) { + $permalink = $incident->permalink; + } else { + $permalink = urljoin($this->getURI(), '/incident/' . $incident->id); + } - if (isset($incident->permalink)) { - $permalink = $incident->permalink; - } else { - $permalink = urljoin($this->getURI(), '/incident/' . $incident->id); - } + $title = $incident->human_status . ': ' . $incident->name; + $message = ''; + if ($this->getInput('additional_info')) { + if (isset($incident->occurred_at)) { + $message .= 'Occurred at: ' . $incident->occurred_at . "\r\n"; + } + if (isset($incident->scheduled_at)) { + $message .= 'Scheduled at: ' . $incident->scheduled_at . "\r\n"; + } + if (isset($incident->created_at)) { + $message .= 'Created at: ' . $incident->created_at . "\r\n"; + } + if (isset($incident->updated_at)) { + $message .= 'Updated at: ' . $incident->updated_at . "\r\n\r\n"; + } + } - $title = $incident->human_status . ': ' . $incident->name; - $message = ''; - if ($this->getInput('additional_info')) { - if (isset($incident->occurred_at)) { - $message .= 'Occurred at: ' . $incident->occurred_at . "\r\n"; - } - if (isset($incident->scheduled_at)) { - $message .= 'Scheduled at: ' . $incident->scheduled_at . "\r\n"; - } - if (isset($incident->created_at)) { - $message .= 'Created at: ' . $incident->created_at . "\r\n"; - } - if (isset($incident->updated_at)) { - $message .= 'Updated at: ' . $incident->updated_at . "\r\n\r\n"; - } - } + $message .= $incident->message; + $content = nl2br($message); + $componentName = $this->getComponentName($incident->component_id); + $uidOrig = $permalink . $incident->created_at; + $uid = hash('sha512', $uidOrig); + $timestamp = strtotime($incident->created_at); + $categories = []; + $categories[] = $incident->human_status; + if ($componentName !== '') { + $categories[] = $componentName; + } - $message .= $incident->message; - $content = nl2br($message); - $componentName = $this->getComponentName($incident->component_id); - $uidOrig = $permalink . $incident->created_at; - $uid = hash('sha512', $uidOrig); - $timestamp = strtotime($incident->created_at); - $categories = array(); - $categories[] = $incident->human_status; - if ($componentName !== '') { - $categories[] = $componentName; - } + $item = []; + $item['uri'] = $permalink; + $item['title'] = $title; + $item['timestamp'] = $timestamp; + $item['content'] = $content; + $item['uid'] = $uid; + $item['categories'] = $categories; - $item = array(); - $item['uri'] = $permalink; - $item['title'] = $title; - $item['timestamp'] = $timestamp; - $item['content'] = $content; - $item['uid'] = $uid; - $item['categories'] = $categories; - - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/CarThrottleBridge.php b/bridges/CarThrottleBridge.php index 86dafd9e..95641573 100644 --- a/bridges/CarThrottleBridge.php +++ b/bridges/CarThrottleBridge.php @@ -1,41 +1,44 @@ <?php -class CarThrottleBridge extends FeedExpander { - const NAME = 'Car Throttle '; - const URI = 'https://www.carthrottle.com'; - const DESCRIPTION = 'Get the latest car-related news from Car Throttle.'; - const MAINTAINER = 't0stiman'; - public function collectData() { - $this->collectExpandableDatas('https://www.carthrottle.com/rss', 10); - } +class CarThrottleBridge extends FeedExpander +{ + const NAME = 'Car Throttle '; + const URI = 'https://www.carthrottle.com'; + const DESCRIPTION = 'Get the latest car-related news from Car Throttle.'; + const MAINTAINER = 't0stiman'; - protected function parseItem($feedItem) { - $item = parent::parseItem($feedItem); + public function collectData() + { + $this->collectExpandableDatas('https://www.carthrottle.com/rss', 10); + } - //fetch page - $articlePage = getSimpleHTMLDOMCached($feedItem->link) - or returnServerError('Could not retrieve ' . $feedItem->link); + protected function parseItem($feedItem) + { + $item = parent::parseItem($feedItem); - $subtitle = $articlePage->find('p.standfirst', 0); - $article = $articlePage->find('div.content_field', 0); + //fetch page + $articlePage = getSimpleHTMLDOMCached($feedItem->link) + or returnServerError('Could not retrieve ' . $feedItem->link); - $item['content'] = str_get_html($subtitle . $article); + $subtitle = $articlePage->find('p.standfirst', 0); + $article = $articlePage->find('div.content_field', 0); - //convert <iframe>s to <a>s. meant for embedded videos. - foreach($item['content']->find('iframe') as $found) { + $item['content'] = str_get_html($subtitle . $article); - $iframeUrl = $found->getAttribute('src'); + //convert <iframe>s to <a>s. meant for embedded videos. + foreach ($item['content']->find('iframe') as $found) { + $iframeUrl = $found->getAttribute('src'); - if ($iframeUrl) { - $found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>'; - } - } + if ($iframeUrl) { + $found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>'; + } + } - //remove scripts from the text - foreach ($item['content']->find('script') as $remove) { - $remove->outertext = ''; - } + //remove scripts from the text + foreach ($item['content']->find('script') as $remove) { + $remove->outertext = ''; + } - return $item; - } + return $item; + } } diff --git a/bridges/CastorusBridge.php b/bridges/CastorusBridge.php index 9dc878f7..a0a1454e 100644 --- a/bridges/CastorusBridge.php +++ b/bridges/CastorusBridge.php @@ -1,118 +1,135 @@ <?php -class CastorusBridge extends BridgeAbstract { - const MAINTAINER = 'logmanoriginal'; - const NAME = 'Castorus Bridge'; - const URI = 'https://www.castorus.com'; - const CACHE_TIMEOUT = 600; // 10min - const DESCRIPTION = 'Returns the latest changes'; - - const PARAMETERS = array( - 'Get latest changes' => array(), - 'Get latest changes via ZIP code' => array( - 'zip' => array( - 'name' => 'ZIP code', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '7', - 'title' => 'Insert ZIP code (complete or partial). e.g: 78125 OR 781 OR 7' - ) - ), - 'Get latest changes via city name' => array( - 'city' => array( - 'name' => 'City name', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'Paris', - 'title' => 'Insert city name (complete or partial). e.g: Paris OR Par OR P' - ) - ) - ); - - // Extracts the title from an actitiy - private function extractActivityTitle($activity){ - $title = $activity->find('a', 0); - - if(!$title) - returnServerError('Cannot find title!'); - - return trim($title->plaintext); - } - - // Extracts the url from an actitiy - private function extractActivityUrl($activity){ - $url = $activity->find('a', 0); - - if(!$url) - returnServerError('Cannot find url!'); - - return self::URI . $url->href; - } - - // Extracts the time from an activity - private function extractActivityTime($activity){ - // Unfortunately the time is part of the parent node, - // so we have to clear all child nodes first - $nodes = $activity->find('*'); - - if(!$nodes) - returnServerError('Cannot find nodes!'); - - foreach($nodes as $node) { - $node->outertext = ''; - } - - return strtotime($activity->innertext); - } - - // Extracts the price change - private function extractActivityPrice($activity){ - $price = $activity->find('span', 1); - - if(!$price) - returnServerError('Cannot find price!'); - - return $price->innertext; - } - - public function collectData(){ - $zip_filter = trim($this->getInput('zip')); - $city_filter = trim($this->getInput('city')); - - $html = getSimpleHTMLDOM(self::URI); - - if(!$html) - returnServerError('Could not load data from ' . self::URI . '!'); - - $activities = $html->find('div#activite > li'); - - if(!$activities) - returnServerError('Failed to find activities!'); - - foreach($activities as $activity) { - $item = array(); - - $item['title'] = $this->extractActivityTitle($activity); - $item['uri'] = $this->extractActivityUrl($activity); - $item['timestamp'] = $this->extractActivityTime($activity); - $item['content'] = '<a href="' - . $item['uri'] - . '">' - . $item['title'] - . '</a><br><p>' - . $this->extractActivityPrice($activity) - . '</p>'; - - if(isset($zip_filter) - && !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter)) { - continue; // Skip this item - } - - if(isset($city_filter) - && !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter)) { - continue; // Skip this item - } - - $this->items[] = $item; - } - } + +class CastorusBridge extends BridgeAbstract +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'Castorus Bridge'; + const URI = 'https://www.castorus.com'; + const CACHE_TIMEOUT = 600; // 10min + const DESCRIPTION = 'Returns the latest changes'; + + const PARAMETERS = [ + 'Get latest changes' => [], + 'Get latest changes via ZIP code' => [ + 'zip' => [ + 'name' => 'ZIP code', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '7', + 'title' => 'Insert ZIP code (complete or partial). e.g: 78125 OR 781 OR 7' + ] + ], + 'Get latest changes via city name' => [ + 'city' => [ + 'name' => 'City name', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'Paris', + 'title' => 'Insert city name (complete or partial). e.g: Paris OR Par OR P' + ] + ] + ]; + + // Extracts the title from an actitiy + private function extractActivityTitle($activity) + { + $title = $activity->find('a', 0); + + if (!$title) { + returnServerError('Cannot find title!'); + } + + return trim($title->plaintext); + } + + // Extracts the url from an actitiy + private function extractActivityUrl($activity) + { + $url = $activity->find('a', 0); + + if (!$url) { + returnServerError('Cannot find url!'); + } + + return self::URI . $url->href; + } + + // Extracts the time from an activity + private function extractActivityTime($activity) + { + // Unfortunately the time is part of the parent node, + // so we have to clear all child nodes first + $nodes = $activity->find('*'); + + if (!$nodes) { + returnServerError('Cannot find nodes!'); + } + + foreach ($nodes as $node) { + $node->outertext = ''; + } + + return strtotime($activity->innertext); + } + + // Extracts the price change + private function extractActivityPrice($activity) + { + $price = $activity->find('span', 1); + + if (!$price) { + returnServerError('Cannot find price!'); + } + + return $price->innertext; + } + + public function collectData() + { + $zip_filter = trim($this->getInput('zip')); + $city_filter = trim($this->getInput('city')); + + $html = getSimpleHTMLDOM(self::URI); + + if (!$html) { + returnServerError('Could not load data from ' . self::URI . '!'); + } + + $activities = $html->find('div#activite > li'); + + if (!$activities) { + returnServerError('Failed to find activities!'); + } + + foreach ($activities as $activity) { + $item = []; + + $item['title'] = $this->extractActivityTitle($activity); + $item['uri'] = $this->extractActivityUrl($activity); + $item['timestamp'] = $this->extractActivityTime($activity); + $item['content'] = '<a href="' + . $item['uri'] + . '">' + . $item['title'] + . '</a><br><p>' + . $this->extractActivityPrice($activity) + . '</p>'; + + if ( + isset($zip_filter) + && !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter) + ) { + continue; // Skip this item + } + + if ( + isset($city_filter) + && !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter) + ) { + continue; // Skip this item + } + + $this->items[] = $item; + } + } } diff --git a/bridges/CdactionBridge.php b/bridges/CdactionBridge.php index 6712faf6..a73a1b4f 100644 --- a/bridges/CdactionBridge.php +++ b/bridges/CdactionBridge.php @@ -1,60 +1,62 @@ <?php -class CdactionBridge extends BridgeAbstract { - const NAME = 'CD-ACTION bridge'; - const URI = 'https://cdaction.pl'; - const DESCRIPTION = 'Fetches the latest posts from given category.'; - const MAINTAINER = 'tomaszkane'; - const PARAMETERS = array( array( - 'category' => array( - 'name' => 'Kategoria', - 'type' => 'list', - 'values' => array( - 'Najnowsze (wszystkie)' => 'najnowsze', - 'Newsy' => 'newsy', - 'Recenzje' => 'recenzje', - 'Teksty' => array( - 'Publicystyka' => 'publicystyka', - 'Zapowiedzi' => 'zapowiedzi', - 'Już graliśmy' => 'juz-gralismy', - 'Poradniki' => 'poradniki', - ), - 'Kultura' => 'kultura', - 'Wideo' => 'wideo', - 'Czasopismo' => 'czasopismo', - 'Technologie' => array( - 'Artykuły' => 'artykuly', - 'Testy' => 'testy', - ), - 'Na luzie' => array( - 'Konkursy' => 'konkursy', - 'Nadgodziny' => 'nadgodziny', - ) - ) - )) - ); +class CdactionBridge extends BridgeAbstract +{ + const NAME = 'CD-ACTION bridge'; + const URI = 'https://cdaction.pl'; + const DESCRIPTION = 'Fetches the latest posts from given category.'; + const MAINTAINER = 'tomaszkane'; + const PARAMETERS = [ [ + 'category' => [ + 'name' => 'Kategoria', + 'type' => 'list', + 'values' => [ + 'Najnowsze (wszystkie)' => 'najnowsze', + 'Newsy' => 'newsy', + 'Recenzje' => 'recenzje', + 'Teksty' => [ + 'Publicystyka' => 'publicystyka', + 'Zapowiedzi' => 'zapowiedzi', + 'Już graliśmy' => 'juz-gralismy', + 'Poradniki' => 'poradniki', + ], + 'Kultura' => 'kultura', + 'Wideo' => 'wideo', + 'Czasopismo' => 'czasopismo', + 'Technologie' => [ + 'Artykuły' => 'artykuly', + 'Testy' => 'testy', + ], + 'Na luzie' => [ + 'Konkursy' => 'konkursy', + 'Nadgodziny' => 'nadgodziny', + ] + ] + ]] + ]; - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI() . '/' . $this->getInput('category')); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI() . '/' . $this->getInput('category')); - $newsJson = $html->find('script#__NEXT_DATA__', 0)->innertext; - if (!$newsJson = json_decode($newsJson)) { - return; - } + $newsJson = $html->find('script#__NEXT_DATA__', 0)->innertext; + if (!$newsJson = json_decode($newsJson)) { + return; + } - $queriesIndex = $this->getInput('category') === 'najnowsze' ? 0 : 1; - foreach ($newsJson->props->pageProps->dehydratedState->queries[$queriesIndex]->state->data->results as $news) { - $item = array(); - $item['uri'] = $this->getURI() . '/' . $news->category->slug . '/' . $news->slug; - $item['title'] = $news->title; - $item['timestamp'] = $news->publishedAt; - $item['author'] = $news->editor->fullName; - $item['content'] = $news->lead; - $item['enclosures'][] = $news->bannerUrl; - $item['categories'] = array_column($news->tags, 'name'); - $item['uid'] = $news->id; + $queriesIndex = $this->getInput('category') === 'najnowsze' ? 0 : 1; + foreach ($newsJson->props->pageProps->dehydratedState->queries[$queriesIndex]->state->data->results as $news) { + $item = []; + $item['uri'] = $this->getURI() . '/' . $news->category->slug . '/' . $news->slug; + $item['title'] = $news->title; + $item['timestamp'] = $news->publishedAt; + $item['author'] = $news->editor->fullName; + $item['content'] = $news->lead; + $item['enclosures'][] = $news->bannerUrl; + $item['categories'] = array_column($news->tags, 'name'); + $item['uid'] = $news->id; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/CeskaTelevizeBridge.php b/bridges/CeskaTelevizeBridge.php index c3be1dc3..99b9b868 100644 --- a/bridges/CeskaTelevizeBridge.php +++ b/bridges/CeskaTelevizeBridge.php @@ -1,83 +1,88 @@ <?php -class CeskaTelevizeBridge extends BridgeAbstract { - - const NAME = 'Česká televize Bridge'; - const URI = 'https://www.ceskatelevize.cz'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'Return newest videos'; - const MAINTAINER = 'kolarcz'; - - const PARAMETERS = array( - array( - 'url' => array( - 'name' => 'url to the show', - 'required' => true, - 'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/' - ) - ) - ); - - private function fixChars($text) { - return html_entity_decode($text, ENT_QUOTES, 'UTF-8'); - } - - private function getUploadTimeFromString($string) { - if (strpos($string, 'dnes') !== false) { - return strtotime('today'); - } elseif (strpos($string, 'včera') !== false) { - return strtotime('yesterday'); - } elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) { - returnServerError('Could not get date from Česká televize string'); - } - - $date = sprintf('%04d-%02d-%02d', isset($match[3]) ? $match[3] : date('Y'), $match[2], $match[1]); - return strtotime($date); - } - - public function collectData() { - $url = $this->getInput('url'); - - $validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(bonus\/)?$/'; - if (!preg_match($validUrl, $url, $match)) { - returnServerError('Invalid url'); - } - - $category = isset($match[4]) ? $match[4] : 'nove'; - $fixedUrl = "{$match[1]}dily/{$category}/"; - - $html = getSimpleHTMLDOM($fixedUrl); - - $this->feedUri = $fixedUrl; - $this->feedName = str_replace('Přehled dílů — ', '', $this->fixChars($html->find('title', 0)->plaintext)); - if ($category !== 'nove') { - $this->feedName .= " ({$category})"; - } - - foreach ($html->find('#episodeListSection a[data-testid=next-link]') as $element) { - $itemTitle = $element->find('h3', 0); - $itemContent = $element->find('div[class^=content-]', 0); - $itemDate = $element->find('div[class^=playTime-] span', 0); - $itemThumbnail = $element->find('img', 0); - $itemUri = self::URI . $element->getAttribute('href'); - - $item = array( - 'title' => $this->fixChars($itemTitle->plaintext), - 'uri' => $itemUri, - 'content' => '<img src="' . $itemThumbnail->getAttribute('src') . '" /><br />' - . $this->fixChars($itemContent->plaintext), - 'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext) - ); - - $this->items[] = $item; - } - } - - public function getURI() { - return isset($this->feedUri) ? $this->feedUri : parent::getURI(); - } - - public function getName() { - return isset($this->feedName) ? $this->feedName : parent::getName(); - } +class CeskaTelevizeBridge extends BridgeAbstract +{ + const NAME = 'Česká televize Bridge'; + const URI = 'https://www.ceskatelevize.cz'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'Return newest videos'; + const MAINTAINER = 'kolarcz'; + + const PARAMETERS = [ + [ + 'url' => [ + 'name' => 'url to the show', + 'required' => true, + 'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/' + ] + ] + ]; + + private function fixChars($text) + { + return html_entity_decode($text, ENT_QUOTES, 'UTF-8'); + } + + private function getUploadTimeFromString($string) + { + if (strpos($string, 'dnes') !== false) { + return strtotime('today'); + } elseif (strpos($string, 'včera') !== false) { + return strtotime('yesterday'); + } elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) { + returnServerError('Could not get date from Česká televize string'); + } + + $date = sprintf('%04d-%02d-%02d', isset($match[3]) ? $match[3] : date('Y'), $match[2], $match[1]); + return strtotime($date); + } + + public function collectData() + { + $url = $this->getInput('url'); + + $validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(bonus\/)?$/'; + if (!preg_match($validUrl, $url, $match)) { + returnServerError('Invalid url'); + } + + $category = isset($match[4]) ? $match[4] : 'nove'; + $fixedUrl = "{$match[1]}dily/{$category}/"; + + $html = getSimpleHTMLDOM($fixedUrl); + + $this->feedUri = $fixedUrl; + $this->feedName = str_replace('Přehled dílů — ', '', $this->fixChars($html->find('title', 0)->plaintext)); + if ($category !== 'nove') { + $this->feedName .= " ({$category})"; + } + + foreach ($html->find('#episodeListSection a[data-testid=next-link]') as $element) { + $itemTitle = $element->find('h3', 0); + $itemContent = $element->find('div[class^=content-]', 0); + $itemDate = $element->find('div[class^=playTime-] span', 0); + $itemThumbnail = $element->find('img', 0); + $itemUri = self::URI . $element->getAttribute('href'); + + $item = [ + 'title' => $this->fixChars($itemTitle->plaintext), + 'uri' => $itemUri, + 'content' => '<img src="' . $itemThumbnail->getAttribute('src') . '" /><br />' + . $this->fixChars($itemContent->plaintext), + 'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext) + ]; + + $this->items[] = $item; + } + } + + public function getURI() + { + return isset($this->feedUri) ? $this->feedUri : parent::getURI(); + } + + public function getName() + { + return isset($this->feedName) ? $this->feedName : parent::getName(); + } } diff --git a/bridges/CodebergBridge.php b/bridges/CodebergBridge.php index d8a40525..b9a2b4c9 100644 --- a/bridges/CodebergBridge.php +++ b/bridges/CodebergBridge.php @@ -1,346 +1,359 @@ <?php -class CodebergBridge extends BridgeAbstract { - const NAME = 'Codeberg Bridge'; - const URI = 'https://codeberg.org/'; - const DESCRIPTION = 'Returns commits, issues, pull requests or releases for a repository.'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - 'Commits' => array( - 'branch' => array( - 'name' => 'branch', - 'type' => 'text', - 'exampleValue' => 'main', - 'required' => false, - 'title' => 'Optional, main branch is used by default.', - ), - ), - 'Issues' => array(), - 'Issue Comments' => array( - 'issueId' => array( - 'name' => 'Issue ID', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '513', - ) - ), - 'Pull Requests' => array(), - 'Releases' => array(), - 'global' => array( - 'username' => array( - 'name' => 'Username', - 'type' => 'text', - 'exampleValue' => 'Codeberg', - 'title' => 'Username of account that the repository belongs to.', - 'required' => true, - ), - 'repo' => array( - 'name' => 'Repository', - 'type' => 'text', - 'exampleValue' => 'Community', - 'required' => true, - ) - ) - ); - - const CACHE_TIMEOUT = 1800; - - const TEST_DETECT_PARAMETERS = array( - 'https://codeberg.org/Codeberg/Community/issues/507' => array( - 'context' => 'Issue Comments', 'username' => 'Codeberg', 'repo' => 'Community', 'issueId' => '507' - ), - 'https://codeberg.org/Codeberg/Community/issues' => array( - 'context' => 'Issues', 'username' => 'Codeberg', 'repo' => 'Community' - ), - 'https://codeberg.org/Codeberg/Community/pulls' => array( - 'context' => 'Pull Requests', 'username' => 'Codeberg', 'repo' => 'Community' - ), - 'https://codeberg.org/Codeberg/Community/releases' => array( - 'context' => 'Releases', 'username' => 'Codeberg', 'repo' => 'Community' - ), - 'https://codeberg.org/Codeberg/Community/commits/branch/master' => array( - 'context' => 'Commits', 'username' => 'Codeberg', 'repo' => 'Community', 'branch' => 'master' - ), - 'https://codeberg.org/Codeberg/Community/commits' => array( - 'context' => 'Commits', 'username' => 'Codeberg', 'repo' => 'Community' - ) - ); - - private $defaultBranch = 'main'; - private $issueTitle = ''; - - private $urlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)(?:\/commits\/branch\/([\w]+))?/'; - private $issuesUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/issues/'; - private $pullsUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/pulls/'; - private $releasesUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/releases/'; - private $issueCommentsUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/issues\/([0-9]+)/'; - - public function detectParameters($url) { - $params = array(); - - // Issue Comments - if(preg_match($this->issueCommentsUrlRegex, $url, $matches)) { - $params['context'] = 'Issue Comments'; - $params['username'] = $matches[1]; - $params['repo'] = $matches[2]; - $params['issueId'] = $matches[3]; - - return $params; - } - - // Issues - if(preg_match($this->issuesUrlRegex, $url, $matches)) { - $params['context'] = 'Issues'; - $params['username'] = $matches[1]; - $params['repo'] = $matches[2]; - - return $params; - } - - // Pull Requests - if(preg_match($this->pullsUrlRegex, $url, $matches)) { - $params['context'] = 'Pull Requests'; - $params['username'] = $matches[1]; - $params['repo'] = $matches[2]; - - return $params; - } - - // Releases - if(preg_match($this->releasesUrlRegex, $url, $matches)) { - $params['context'] = 'Releases'; - $params['username'] = $matches[1]; - $params['repo'] = $matches[2]; - - return $params; - } - - // Commits - if(preg_match($this->urlRegex, $url, $matches)) { - $params['context'] = 'Commits'; - $params['username'] = $matches[1]; - $params['repo'] = $matches[2]; - - if (isset($matches[3])) { - $params['branch'] = $matches[3]; - } - - return $params; - } - - return null; - } - - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); - - $html = defaultLinkTo($html, $this->getURI()); - - switch($this->queriedContext) { - case 'Commits': - $this->extractCommits($html); - break; - case 'Issues': - $this->extractIssues($html); - break; - case 'Issue Comments': - $this->extractIssueComments($html); - break; - case 'Pull Requests': - $this->extractPulls($html); - break; - case 'Releases': - $this->extractReleases($html); - break; - default: - returnClientError('Invalid context: ' . $this->queriedContext); - } - } - - public function getName() { - switch($this->queriedContext) { - case 'Commits': - if ($this->getBranch() === $this->defaultBranch) { - return $this->getRepo() . ' Commits'; - } - - return $this->getRepo() . ' Commits (' . $this->getBranch() . ' branch) - ' . self::NAME; - case 'Issues': - return $this->getRepo() . ' Issues - ' . self::NAME; - case 'Issue Comments': - return $this->issueTitle . ' - Issue Comments - ' . self::NAME; - case 'Pull Requests': - return $this->getRepo() . ' Pull Requests - ' . self::NAME; - case 'Releases': - return $this->getRepo() . ' Releases - ' . self::NAME; - default: - return parent::getName(); - } - } - - public function getURI() { - switch($this->queriedContext) { - case 'Commits': - return self::URI . $this->getRepo() . '/commits/branch/' . $this->getBranch(); - case 'Issues': - return self::URI . $this->getRepo() . '/issues/'; - case 'Issue Comments': - return self::URI . $this->getRepo() . '/issues/' . $this->getInput('issueId'); - case 'Pull Requests': - return self::URI . $this->getRepo() . '/pulls'; - case 'Releases': - return self::URI . $this->getRepo() . '/releases'; - default: - return parent::getURI(); - } - } - - private function getBranch() { - if ($this->getInput('branch')) { - return $this->getInput('branch'); - } - - return $this->defaultBranch; - } - - private function getRepo() { - return $this->getInput('username') . '/' . $this->getInput('repo'); - } - - /** - * Extract commits - */ - private function extractCommits($html) { - $table = $html->find('table#commits-table', 0); - $tbody = $table->find('tbody.commit-list', 0); - - foreach ($tbody->find('tr') as $tr) { - $item = array(); - - $message = $tr->find('td.message', 0); - - $item['title'] = $message->find('span.message-wrapper', 0)->plaintext; - $item['uri'] = $tr->find('td.sha', 0)->find('a', 0)->href; - $item['author'] = $tr->find('td.author', 0)->plaintext; - $item['timestamp'] = $tr->find('td', 3)->find('span', 0)->title; - - if ($message->find('pre.commit-body', 0)) { - $message->find('pre.commit-body', 0)->style = ''; - - $item['content'] = $message->find('pre.commit-body', 0); - } else { - $item['content'] = '<blockquote>' . $item['title'] . '</blockquote>'; - } - - $this->items[] = $item; - } - } - - /** - * Extract issues - */ - private function extractIssues($html) { - $div = $html->find('div.issue.list', 0); - - foreach ($div->find('li.item') as $li) { - $item = array(); - - $number = trim($li->find('a.index,ml-0.mr-2', 0)->plaintext); - - $item['title'] = $li->find('a.title', 0)->plaintext . ' (' . $number . ')'; - $item['uri'] = $li->find('a.title', 0)->href; - $item['timestamp'] = $li->find('span.time-since', 0)->title; - $item['author'] = $li->find('div.desc', 0)->find('a', 1)->plaintext; - - // Fetch issue page - $issuePage = getSimpleHTMLDOMCached($item['uri'], 3600); - $issuePage = defaultLinkTo($issuePage, self::URI); - - $item['content'] = $issuePage->find('div.timeline-item.comment.first', 0)->find('div.render-content.markup', 0); - - foreach ($li->find('a.ui.label') as $label) { - $item['categories'][] = $label->plaintext; - } - - $this->items[] = $item; - } - } - - /** - * Extract issue comments - */ - private function extractIssueComments($html) { - $this->issueTitle = $html->find('span#issue-title', 0)->plaintext - . ' (' . $html->find('span.index', 0)->plaintext . ')'; - - foreach ($html->find('div.timeline-item.comment') as $div) { - $item = array(); - - if ($div->class === 'timeline-item comment merge box') { - continue; - } - - $item['title'] = $this->ellipsisTitle($div->find('div.render-content.markup', 0)->plaintext); - $item['uri'] = $div->find('span.text.grey', 0)->find('a', 1)->href; - $item['content'] = $div->find('div.render-content.markup', 0); - - if ($div->find('div.dropzone-attachments', 0)) { - $item['content'] .= $div->find('div.dropzone-attachments', 0); - } - - $item['author'] = $div->find('a.author', 0)->innertext; - $item['timestamp'] = $div->find('span.time-since', 0)->title; - - $this->items[] = $item; - } - } - /** - * Extract pulls - */ - private function extractPulls($html) { - $div = $html->find('div.issue.list', 0); +class CodebergBridge extends BridgeAbstract +{ + const NAME = 'Codeberg Bridge'; + const URI = 'https://codeberg.org/'; + const DESCRIPTION = 'Returns commits, issues, pull requests or releases for a repository.'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + 'Commits' => [ + 'branch' => [ + 'name' => 'branch', + 'type' => 'text', + 'exampleValue' => 'main', + 'required' => false, + 'title' => 'Optional, main branch is used by default.', + ], + ], + 'Issues' => [], + 'Issue Comments' => [ + 'issueId' => [ + 'name' => 'Issue ID', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '513', + ] + ], + 'Pull Requests' => [], + 'Releases' => [], + 'global' => [ + 'username' => [ + 'name' => 'Username', + 'type' => 'text', + 'exampleValue' => 'Codeberg', + 'title' => 'Username of account that the repository belongs to.', + 'required' => true, + ], + 'repo' => [ + 'name' => 'Repository', + 'type' => 'text', + 'exampleValue' => 'Community', + 'required' => true, + ] + ] + ]; + + const CACHE_TIMEOUT = 1800; + + const TEST_DETECT_PARAMETERS = [ + 'https://codeberg.org/Codeberg/Community/issues/507' => [ + 'context' => 'Issue Comments', 'username' => 'Codeberg', 'repo' => 'Community', 'issueId' => '507' + ], + 'https://codeberg.org/Codeberg/Community/issues' => [ + 'context' => 'Issues', 'username' => 'Codeberg', 'repo' => 'Community' + ], + 'https://codeberg.org/Codeberg/Community/pulls' => [ + 'context' => 'Pull Requests', 'username' => 'Codeberg', 'repo' => 'Community' + ], + 'https://codeberg.org/Codeberg/Community/releases' => [ + 'context' => 'Releases', 'username' => 'Codeberg', 'repo' => 'Community' + ], + 'https://codeberg.org/Codeberg/Community/commits/branch/master' => [ + 'context' => 'Commits', 'username' => 'Codeberg', 'repo' => 'Community', 'branch' => 'master' + ], + 'https://codeberg.org/Codeberg/Community/commits' => [ + 'context' => 'Commits', 'username' => 'Codeberg', 'repo' => 'Community' + ] + ]; + + private $defaultBranch = 'main'; + private $issueTitle = ''; + + private $urlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)(?:\/commits\/branch\/([\w]+))?/'; + private $issuesUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/issues/'; + private $pullsUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/pulls/'; + private $releasesUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/releases/'; + private $issueCommentsUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/issues\/([0-9]+)/'; + + public function detectParameters($url) + { + $params = []; + + // Issue Comments + if (preg_match($this->issueCommentsUrlRegex, $url, $matches)) { + $params['context'] = 'Issue Comments'; + $params['username'] = $matches[1]; + $params['repo'] = $matches[2]; + $params['issueId'] = $matches[3]; + + return $params; + } + + // Issues + if (preg_match($this->issuesUrlRegex, $url, $matches)) { + $params['context'] = 'Issues'; + $params['username'] = $matches[1]; + $params['repo'] = $matches[2]; + + return $params; + } + + // Pull Requests + if (preg_match($this->pullsUrlRegex, $url, $matches)) { + $params['context'] = 'Pull Requests'; + $params['username'] = $matches[1]; + $params['repo'] = $matches[2]; + + return $params; + } + + // Releases + if (preg_match($this->releasesUrlRegex, $url, $matches)) { + $params['context'] = 'Releases'; + $params['username'] = $matches[1]; + $params['repo'] = $matches[2]; + + return $params; + } + + // Commits + if (preg_match($this->urlRegex, $url, $matches)) { + $params['context'] = 'Commits'; + $params['username'] = $matches[1]; + $params['repo'] = $matches[2]; + + if (isset($matches[3])) { + $params['branch'] = $matches[3]; + } + + return $params; + } + + return null; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $html = defaultLinkTo($html, $this->getURI()); + + switch ($this->queriedContext) { + case 'Commits': + $this->extractCommits($html); + break; + case 'Issues': + $this->extractIssues($html); + break; + case 'Issue Comments': + $this->extractIssueComments($html); + break; + case 'Pull Requests': + $this->extractPulls($html); + break; + case 'Releases': + $this->extractReleases($html); + break; + default: + returnClientError('Invalid context: ' . $this->queriedContext); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Commits': + if ($this->getBranch() === $this->defaultBranch) { + return $this->getRepo() . ' Commits'; + } + + return $this->getRepo() . ' Commits (' . $this->getBranch() . ' branch) - ' . self::NAME; + case 'Issues': + return $this->getRepo() . ' Issues - ' . self::NAME; + case 'Issue Comments': + return $this->issueTitle . ' - Issue Comments - ' . self::NAME; + case 'Pull Requests': + return $this->getRepo() . ' Pull Requests - ' . self::NAME; + case 'Releases': + return $this->getRepo() . ' Releases - ' . self::NAME; + default: + return parent::getName(); + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Commits': + return self::URI . $this->getRepo() . '/commits/branch/' . $this->getBranch(); + case 'Issues': + return self::URI . $this->getRepo() . '/issues/'; + case 'Issue Comments': + return self::URI . $this->getRepo() . '/issues/' . $this->getInput('issueId'); + case 'Pull Requests': + return self::URI . $this->getRepo() . '/pulls'; + case 'Releases': + return self::URI . $this->getRepo() . '/releases'; + default: + return parent::getURI(); + } + } + + private function getBranch() + { + if ($this->getInput('branch')) { + return $this->getInput('branch'); + } + + return $this->defaultBranch; + } + + private function getRepo() + { + return $this->getInput('username') . '/' . $this->getInput('repo'); + } + + /** + * Extract commits + */ + private function extractCommits($html) + { + $table = $html->find('table#commits-table', 0); + $tbody = $table->find('tbody.commit-list', 0); + + foreach ($tbody->find('tr') as $tr) { + $item = []; + + $message = $tr->find('td.message', 0); + + $item['title'] = $message->find('span.message-wrapper', 0)->plaintext; + $item['uri'] = $tr->find('td.sha', 0)->find('a', 0)->href; + $item['author'] = $tr->find('td.author', 0)->plaintext; + $item['timestamp'] = $tr->find('td', 3)->find('span', 0)->title; + + if ($message->find('pre.commit-body', 0)) { + $message->find('pre.commit-body', 0)->style = ''; + + $item['content'] = $message->find('pre.commit-body', 0); + } else { + $item['content'] = '<blockquote>' . $item['title'] . '</blockquote>'; + } + + $this->items[] = $item; + } + } + + /** + * Extract issues + */ + private function extractIssues($html) + { + $div = $html->find('div.issue.list', 0); + + foreach ($div->find('li.item') as $li) { + $item = []; + + $number = trim($li->find('a.index,ml-0.mr-2', 0)->plaintext); + + $item['title'] = $li->find('a.title', 0)->plaintext . ' (' . $number . ')'; + $item['uri'] = $li->find('a.title', 0)->href; + $item['timestamp'] = $li->find('span.time-since', 0)->title; + $item['author'] = $li->find('div.desc', 0)->find('a', 1)->plaintext; + + // Fetch issue page + $issuePage = getSimpleHTMLDOMCached($item['uri'], 3600); + $issuePage = defaultLinkTo($issuePage, self::URI); + + $item['content'] = $issuePage->find('div.timeline-item.comment.first', 0)->find('div.render-content.markup', 0); + + foreach ($li->find('a.ui.label') as $label) { + $item['categories'][] = $label->plaintext; + } + + $this->items[] = $item; + } + } + + /** + * Extract issue comments + */ + private function extractIssueComments($html) + { + $this->issueTitle = $html->find('span#issue-title', 0)->plaintext + . ' (' . $html->find('span.index', 0)->plaintext . ')'; + + foreach ($html->find('div.timeline-item.comment') as $div) { + $item = []; + + if ($div->class === 'timeline-item comment merge box') { + continue; + } + + $item['title'] = $this->ellipsisTitle($div->find('div.render-content.markup', 0)->plaintext); + $item['uri'] = $div->find('span.text.grey', 0)->find('a', 1)->href; + $item['content'] = $div->find('div.render-content.markup', 0); + + if ($div->find('div.dropzone-attachments', 0)) { + $item['content'] .= $div->find('div.dropzone-attachments', 0); + } + + $item['author'] = $div->find('a.author', 0)->innertext; + $item['timestamp'] = $div->find('span.time-since', 0)->title; + + $this->items[] = $item; + } + } + + /** + * Extract pulls + */ + private function extractPulls($html) + { + $div = $html->find('div.issue.list', 0); + + foreach ($div->find('li.item') as $li) { + $item = []; + + $number = trim($li->find('a.index,ml-0.mr-2', 0)->plaintext); + + $item['title'] = $li->find('a.title', 0)->plaintext . ' (' . $number . ')'; + $item['uri'] = $li->find('a.title', 0)->href; + $item['timestamp'] = $li->find('span.time-since', 0)->title; + $item['author'] = $li->find('div.desc', 0)->find('a', 1)->plaintext; - foreach ($div->find('li.item') as $li) { - $item = array(); + // Fetch pull request page + $pullRequestPage = getSimpleHTMLDOMCached($item['uri'], 3600); + $pullRequestPage = defaultLinkTo($pullRequestPage, self::URI); + + $item['content'] = $pullRequestPage->find('ui.timeline', 0)->find('div.render-content.markup', 0); + + foreach ($li->find('a.ui.label') as $label) { + $item['categories'][] = $label->plaintext; + } - $number = trim($li->find('a.index,ml-0.mr-2', 0)->plaintext); + $this->items[] = $item; + } + } + + /** + * Extract releases + */ + private function extractReleases($html) + { + $ul = $html->find('ul#release-list', 0); + + foreach ($ul->find('li.ui.grid') as $li) { + $item = []; + $item['title'] = $li->find('h4', 0)->plaintext; + $item['uri'] = $li->find('h4', 0)->find('a', 0)->href; - $item['title'] = $li->find('a.title', 0)->plaintext . ' (' . $number . ')'; - $item['uri'] = $li->find('a.title', 0)->href; - $item['timestamp'] = $li->find('span.time-since', 0)->title; - $item['author'] = $li->find('div.desc', 0)->find('a', 1)->plaintext; + $tag = $this->stripSvg($li->find('span.tag', 0)); + $commit = $this->stripSvg($li->find('span.commit', 0)); + $downloads = $this->extractDownloads($li->find('details.download', 0)); - // Fetch pull request page - $pullRequestPage = getSimpleHTMLDOMCached($item['uri'], 3600); - $pullRequestPage = defaultLinkTo($pullRequestPage, self::URI); - - $item['content'] = $pullRequestPage->find('ui.timeline', 0)->find('div.render-content.markup', 0); - - foreach ($li->find('a.ui.label') as $label) { - $item['categories'][] = $label->plaintext; - } - - $this->items[] = $item; - } - } - - /** - * Extract releases - */ - private function extractReleases($html) { - $ul = $html->find('ul#release-list', 0); - - foreach ($ul->find('li.ui.grid') as $li) { - $item = array(); - $item['title'] = $li->find('h4', 0)->plaintext; - $item['uri'] = $li->find('h4', 0)->find('a', 0)->href; - - $tag = $this->stripSvg($li->find('span.tag', 0)); - $commit = $this->stripSvg($li->find('span.commit', 0)); - $downloads = $this->extractDownloads($li->find('details.download', 0)); - - $item['content'] = $li->find('div.markup.desc', 0); - $item['content'] .= <<<HTML + $item['content'] = $li->find('div.markup.desc', 0); + $item['content'] .= <<<HTML <strong>Tag</strong> <p>{$tag}</p> <strong>Commit</strong> @@ -348,56 +361,59 @@ class CodebergBridge extends BridgeAbstract { {$downloads} HTML; - $item['timestamp'] = $li->find('span.time', 0)->find('span', 0)->title; - $item['author'] = $li->find('span.author', 0)->find('a', 0)->plaintext; + $item['timestamp'] = $li->find('span.time', 0)->find('span', 0)->title; + $item['author'] = $li->find('span.author', 0)->find('a', 0)->plaintext; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - /** - * Extract downloads for a releases - */ - private function extractDownloads($html, $skipFirst = false) { - $downloads = ''; + /** + * Extract downloads for a releases + */ + private function extractDownloads($html, $skipFirst = false) + { + $downloads = ''; - foreach ($html->find('a') as $index => $a) { - if ($skipFirst === true && $index === 0) { - continue; - } + foreach ($html->find('a') as $index => $a) { + if ($skipFirst === true && $index === 0) { + continue; + } - $downloads .= <<<HTML + $downloads .= <<<HTML <a href="{$a->herf}">{$a->plaintext}</a><br> HTML; - } + } - return <<<EOD + return <<<EOD <strong>Downloads</strong> <p>{$downloads}</p> EOD; - } - - /** - * Ellipsis title to first 100 characters - */ - private function ellipsisTitle($text) { - $length = 100; - - if (strlen($text) > $length) { - $text = explode('<br>', wordwrap($text, $length, '<br>')); - return $text[0] . '...'; - } - return $text; - } - - /** - * Strip SVG tag - */ - private function stripSvg($html) { - if ($html->find('svg', 0)) { - $html->find('svg', 0)->outertext = ''; - } - - return $html; - } + } + + /** + * Ellipsis title to first 100 characters + */ + private function ellipsisTitle($text) + { + $length = 100; + + if (strlen($text) > $length) { + $text = explode('<br>', wordwrap($text, $length, '<br>')); + return $text[0] . '...'; + } + return $text; + } + + /** + * Strip SVG tag + */ + private function stripSvg($html) + { + if ($html->find('svg', 0)) { + $html->find('svg', 0)->outertext = ''; + } + + return $html; + } } diff --git a/bridges/CollegeDeFranceBridge.php b/bridges/CollegeDeFranceBridge.php index 3c241834..2a1b33d4 100644 --- a/bridges/CollegeDeFranceBridge.php +++ b/bridges/CollegeDeFranceBridge.php @@ -1,83 +1,85 @@ <?php -class CollegeDeFranceBridge extends BridgeAbstract { - const MAINTAINER = 'pit-fgfjiudghdf'; - const NAME = 'CollegeDeFrance'; - const URI = 'https://www.college-de-france.fr/'; - const CACHE_TIMEOUT = 10800; // 3h - const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance'; +class CollegeDeFranceBridge extends BridgeAbstract +{ + const MAINTAINER = 'pit-fgfjiudghdf'; + const NAME = 'CollegeDeFrance'; + const URI = 'https://www.college-de-france.fr/'; + const CACHE_TIMEOUT = 10800; // 3h + const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance'; - public function collectData(){ - $months = array( - '01' => 'janv.', - '02' => 'févr.', - '03' => 'mars', - '04' => 'avr.', - '05' => 'mai', - '06' => 'juin', - '07' => 'juil.', - '08' => 'août', - '09' => 'sept.', - '10' => 'oct.', - '11' => 'nov.', - '12' => 'déc.' - ); + public function collectData() + { + $months = [ + '01' => 'janv.', + '02' => 'févr.', + '03' => 'mars', + '04' => 'avr.', + '05' => 'mai', + '06' => 'juin', + '07' => 'juil.', + '08' => 'août', + '09' => 'sept.', + '10' => 'oct.', + '11' => 'nov.', + '12' => 'déc.' + ]; - // The "API" used by the site returns a list of partial HTML in this form - /* <li> - * <a href="/site/thomas-romer/guestlecturer-2016-04-15-14h30.htm" data-target="after"> - * <span class="date"><span class="list-icon list-icon-video"></span> - * <span class="list-icon list-icon-audio"></span>15 avr. 2016</span> - * <span class="lecturer">Christopher Hays</span> - * <span class='title'>Imagery of Divine Suckling in the Hebrew Bible and the Ancient Near East</span> - * </a> - * </li> - */ - $html = getSimpleHTMLDOM(self::URI - . 'components/search-audiovideo.jsp?fulltext=&siteid=1156951719600&lang=FR&type=all'); + // The "API" used by the site returns a list of partial HTML in this form + /* <li> + * <a href="/site/thomas-romer/guestlecturer-2016-04-15-14h30.htm" data-target="after"> + * <span class="date"><span class="list-icon list-icon-video"></span> + * <span class="list-icon list-icon-audio"></span>15 avr. 2016</span> + * <span class="lecturer">Christopher Hays</span> + * <span class='title'>Imagery of Divine Suckling in the Hebrew Bible and the Ancient Near East</span> + * </a> + * </li> + */ + $html = getSimpleHTMLDOM(self::URI + . 'components/search-audiovideo.jsp?fulltext=&siteid=1156951719600&lang=FR&type=all'); - foreach($html->find('a[data-target]') as $element) { - $item = array(); - $item['title'] = $element->find('.title', 0)->plaintext; + foreach ($html->find('a[data-target]') as $element) { + $item = []; + $item['title'] = $element->find('.title', 0)->plaintext; - // Most relative URLs contains an hour in addition to the date, so let's use it - // <a href="/site/yann-lecun/course-2016-04-08-11h00.htm" data-target="after"> - // - // Sometimes there's an __1, perhaps it signifies an update - // "/site/patrick-boucheron/seminar-2016-05-03-18h00__1.htm" - // - // But unfortunately some don't have any hours info - // <a href="/site/institut-physique/ - // The-Mysteries-of-Decoherence-Sebastien-Gleyzes-[Video-3-35].htm" data-target="after"> - $timezone = new DateTimeZone('Europe/Paris'); + // Most relative URLs contains an hour in addition to the date, so let's use it + // <a href="/site/yann-lecun/course-2016-04-08-11h00.htm" data-target="after"> + // + // Sometimes there's an __1, perhaps it signifies an update + // "/site/patrick-boucheron/seminar-2016-05-03-18h00__1.htm" + // + // But unfortunately some don't have any hours info + // <a href="/site/institut-physique/ + // The-Mysteries-of-Decoherence-Sebastien-Gleyzes-[Video-3-35].htm" data-target="after"> + $timezone = new DateTimeZone('Europe/Paris'); - // strpos($element->href, '201') will break in 2020 but it'll - // probably break prior to then due to site changes anyway - $d = DateTime::createFromFormat( - '!Y-m-d-H\hi', - substr($element->href, strpos($element->href, '201'), 16), - $timezone - ); + // strpos($element->href, '201') will break in 2020 but it'll + // probably break prior to then due to site changes anyway + $d = DateTime::createFromFormat( + '!Y-m-d-H\hi', + substr($element->href, strpos($element->href, '201'), 16), + $timezone + ); - if(!$d) { - $d = DateTime::createFromFormat( - '!d m Y', - trim(str_replace( - array_values($months), - array_keys($months), - $element->find('.date', 0)->plaintext - )), - $timezone - ); - } + if (!$d) { + $d = DateTime::createFromFormat( + '!d m Y', + trim(str_replace( + array_values($months), + array_keys($months), + $element->find('.date', 0)->plaintext + )), + $timezone + ); + } - $item['timestamp'] = $d->format('U'); - $item['content'] = $element->find('.lecturer', 0)->innertext - . ' - ' - . $element->find('.title', 0)->innertext; + $item['timestamp'] = $d->format('U'); + $item['content'] = $element->find('.lecturer', 0)->innertext + . ' - ' + . $element->find('.title', 0)->innertext; - $item['uri'] = self::URI . $element->href; - $this->items[] = $item; - } - } + $item['uri'] = self::URI . $element->href; + $this->items[] = $item; + } + } } diff --git a/bridges/ComboiosDePortugalBridge.php b/bridges/ComboiosDePortugalBridge.php index 652ba601..7b2381b9 100644 --- a/bridges/ComboiosDePortugalBridge.php +++ b/bridges/ComboiosDePortugalBridge.php @@ -1,25 +1,28 @@ <?php -class ComboiosDePortugalBridge extends BridgeAbstract { - const NAME = 'CP | Avisos'; - const BASE_URI = 'https://www.cp.pt'; - const URI = self::BASE_URI . '/passageiros/pt'; - const DESCRIPTION = 'Comboios de Portugal | Avisos'; - const MAINTAINER = 'somini'; - public function collectData() { - # Do not verify SSL certificate (the server doesn't send the intermediate) - # https://github.com/RSS-Bridge/rss-bridge/issues/2397 - $html = getSimpleHTMLDOM($this->getURI() . '/consultar-horarios/avisos', array(), array( - CURLOPT_SSL_VERIFYPEER => 0, - )); +class ComboiosDePortugalBridge extends BridgeAbstract +{ + const NAME = 'CP | Avisos'; + const BASE_URI = 'https://www.cp.pt'; + const URI = self::BASE_URI . '/passageiros/pt'; + const DESCRIPTION = 'Comboios de Portugal | Avisos'; + const MAINTAINER = 'somini'; - foreach($html->find('.warnings-table a') as $element) { - $item = array(); + public function collectData() + { + # Do not verify SSL certificate (the server doesn't send the intermediate) + # https://github.com/RSS-Bridge/rss-bridge/issues/2397 + $html = getSimpleHTMLDOM($this->getURI() . '/consultar-horarios/avisos', [], [ + CURLOPT_SSL_VERIFYPEER => 0, + ]); - $item['title'] = $element->innertext; - $item['uri'] = self::BASE_URI . implode('/', array_map('urlencode', explode('/', $element->href))); + foreach ($html->find('.warnings-table a') as $element) { + $item = []; - $this->items[] = $item; - } - } + $item['title'] = $element->innertext; + $item['uri'] = self::BASE_URI . implode('/', array_map('urlencode', explode('/', $element->href))); + + $this->items[] = $item; + } + } } diff --git a/bridges/ComicsKingdomBridge.php b/bridges/ComicsKingdomBridge.php index 402403e0..8baf7511 100644 --- a/bridges/ComicsKingdomBridge.php +++ b/bridges/ComicsKingdomBridge.php @@ -1,64 +1,71 @@ <?php -class ComicsKingdomBridge extends BridgeAbstract { - const MAINTAINER = 'stjohnjohnson'; - const NAME = 'Comics Kingdom Unofficial RSS'; - const URI = 'https://comicskingdom.com/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Comics Kingdom Unofficial RSS'; - const PARAMETERS = array( array( - 'comicname' => array( - 'name' => 'comicname', - 'type' => 'text', - 'exampleValue' => 'mutts', - 'title' => 'The name of the comic in the URL after https://comicskingdom.com/', - 'required' => true - ) - )); +class ComicsKingdomBridge extends BridgeAbstract +{ + const MAINTAINER = 'stjohnjohnson'; + const NAME = 'Comics Kingdom Unofficial RSS'; + const URI = 'https://comicskingdom.com/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Comics Kingdom Unofficial RSS'; + const PARAMETERS = [ [ + 'comicname' => [ + 'name' => 'comicname', + 'type' => 'text', + 'exampleValue' => 'mutts', + 'title' => 'The name of the comic in the URL after https://comicskingdom.com/', + 'required' => true + ] + ]]; - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI(), array(), array(), true, false); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI(), [], [], true, false); - // Get author from first page - $author = $html->find('div.author p', 0);; + // Get author from first page + $author = $html->find('div.author p', 0); + ; - // Get current date/link - $link = $html->find('meta[property=og:url]', -1)->content; - for($i = 0; $i < 3; $i++) { - $item = array(); + // Get current date/link + $link = $html->find('meta[property=og:url]', -1)->content; + for ($i = 0; $i < 3; $i++) { + $item = []; - $page = getSimpleHTMLDOM($link); + $page = getSimpleHTMLDOM($link); - $imagelink = $page->find('meta[property=og:image]', 0)->content; + $imagelink = $page->find('meta[property=og:image]', 0)->content; - $date = explode('/', $link); + $date = explode('/', $link); - $item['id'] = $imagelink; - $item['uri'] = $link; - $item['author'] = $author; - $item['title'] = 'Comics Kingdom ' . $this->getInput('comicname'); - $item['timestamp'] = DateTime::createFromFormat('Y-m-d', $date[count($date) - 1])->getTimestamp(); - $item['content'] = '<img src="' . $imagelink . '" />'; + $item['id'] = $imagelink; + $item['uri'] = $link; + $item['author'] = $author; + $item['title'] = 'Comics Kingdom ' . $this->getInput('comicname'); + $item['timestamp'] = DateTime::createFromFormat('Y-m-d', $date[count($date) - 1])->getTimestamp(); + $item['content'] = '<img src="' . $imagelink . '" />'; - $this->items[] = $item; - $link = $page->find('div.comic-viewer-inline a', 0)->href; - if (empty($link)) break; // allow bridge to continue if there's less than 3 comics - } - } + $this->items[] = $item; + $link = $page->find('div.comic-viewer-inline a', 0)->href; + if (empty($link)) { + break; // allow bridge to continue if there's less than 3 comics + } + } + } - public function getURI(){ - if(!is_null($this->getInput('comicname'))) { - return self::URI . urlencode($this->getInput('comicname')); - } + public function getURI() + { + if (!is_null($this->getInput('comicname'))) { + return self::URI . urlencode($this->getInput('comicname')); + } - return parent::getURI(); - } + return parent::getURI(); + } - public function getName(){ - if(!is_null($this->getInput('comicname'))) { - return $this->getInput('comicname') . ' - Comics Kingdom'; - } + public function getName() + { + if (!is_null($this->getInput('comicname'))) { + return $this->getInput('comicname') . ' - Comics Kingdom'; + } - return parent::getName(); - } + return parent::getName(); + } } diff --git a/bridges/CommonDreamsBridge.php b/bridges/CommonDreamsBridge.php index 22b9238d..ea21b436 100644 --- a/bridges/CommonDreamsBridge.php +++ b/bridges/CommonDreamsBridge.php @@ -1,26 +1,30 @@ <?php -class CommonDreamsBridge extends FeedExpander { - const MAINTAINER = 'nyutag'; - const NAME = 'CommonDreams Bridge'; - const URI = 'https://www.commondreams.org/'; - const DESCRIPTION = 'Returns the newest articles.'; +class CommonDreamsBridge extends FeedExpander +{ + const MAINTAINER = 'nyutag'; + const NAME = 'CommonDreams Bridge'; + const URI = 'https://www.commondreams.org/'; + const DESCRIPTION = 'Returns the newest articles.'; - public function collectData(){ - $this->collectExpandableDatas('http://www.commondreams.org/rss.xml', 10); - } + public function collectData() + { + $this->collectExpandableDatas('http://www.commondreams.org/rss.xml', 10); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $item['content'] = $this->extractContent($item['uri']); - return $item; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $item['content'] = $this->extractContent($item['uri']); + return $item; + } - private function extractContent($url){ - $html3 = getSimpleHTMLDOMCached($url); - $text = $html3->find('div[class=field--type-text-with-summary]', 0)->innertext; - $html3->clear(); - unset ($html3); - return $text; - } + private function extractContent($url) + { + $html3 = getSimpleHTMLDOMCached($url); + $text = $html3->find('div[class=field--type-text-with-summary]', 0)->innertext; + $html3->clear(); + unset($html3); + return $text; + } } diff --git a/bridges/CopieDoubleBridge.php b/bridges/CopieDoubleBridge.php index 756ecb9e..00739a4e 100644 --- a/bridges/CopieDoubleBridge.php +++ b/bridges/CopieDoubleBridge.php @@ -1,34 +1,36 @@ <?php -class CopieDoubleBridge extends BridgeAbstract { - const MAINTAINER = 'superbaillot.net'; - const NAME = 'CopieDouble'; - const URI = 'http://www.copie-double.com/'; - const CACHE_TIMEOUT = 14400; // 4h - const DESCRIPTION = 'CopieDouble'; +class CopieDoubleBridge extends BridgeAbstract +{ + const MAINTAINER = 'superbaillot.net'; + const NAME = 'CopieDouble'; + const URI = 'http://www.copie-double.com/'; + const CACHE_TIMEOUT = 14400; // 4h + const DESCRIPTION = 'CopieDouble'; - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $table = $html->find('table table', 2); + $table = $html->find('table table', 2); - foreach($table->find('tr') as $element) { - $td = $element->find('td', 0); + foreach ($table->find('tr') as $element) { + $td = $element->find('td', 0); - if($td->class === 'couleur_1') { - $item = array(); - $title = $td->innertext; - $pos = strpos($title, '<a'); - $title = substr($title, 0, $pos); - $item['title'] = $title; - } elseif(strpos($element->innertext, '/images/suivant.gif') === false) { - $a = $element->find('a', 0); - $item['uri'] = self::URI . $a->href; - $content = str_replace('src="/', 'src="/' . self::URI, $element->find('td', 0)->innertext); - $content = str_replace('href="/', 'href="' . self::URI, $content); - $item['content'] = $content; - $this->items[] = $item; - } - } - } + if ($td->class === 'couleur_1') { + $item = []; + $title = $td->innertext; + $pos = strpos($title, '<a'); + $title = substr($title, 0, $pos); + $item['title'] = $title; + } elseif (strpos($element->innertext, '/images/suivant.gif') === false) { + $a = $element->find('a', 0); + $item['uri'] = self::URI . $a->href; + $content = str_replace('src="/', 'src="/' . self::URI, $element->find('td', 0)->innertext); + $content = str_replace('href="/', 'href="' . self::URI, $content); + $item['content'] = $content; + $this->items[] = $item; + } + } + } } diff --git a/bridges/CourrierInternationalBridge.php b/bridges/CourrierInternationalBridge.php index a490e28f..fdbe2ea6 100644 --- a/bridges/CourrierInternationalBridge.php +++ b/bridges/CourrierInternationalBridge.php @@ -1,26 +1,29 @@ <?php -class CourrierInternationalBridge extends FeedExpander { - const MAINTAINER = 'teromene'; - const NAME = 'Courrier International Bridge'; - const URI = 'https://www.courrierinternational.com/'; - const CACHE_TIMEOUT = 300; // 5 min - const DESCRIPTION = 'Returns the newest articles'; +class CourrierInternationalBridge extends FeedExpander +{ + const MAINTAINER = 'teromene'; + const NAME = 'Courrier International Bridge'; + const URI = 'https://www.courrierinternational.com/'; + const CACHE_TIMEOUT = 300; // 5 min + const DESCRIPTION = 'Returns the newest articles'; - public function collectData(){ - $this->collectExpandableDatas(static::URI . 'feed/all/rss.xml', 20); - } + public function collectData() + { + $this->collectExpandableDatas(static::URI . 'feed/all/rss.xml', 20); + } - protected function parseItem($feedItem){ - $item = parent::parseItem($feedItem); + protected function parseItem($feedItem) + { + $item = parent::parseItem($feedItem); - $articlePage = getSimpleHTMLDOMCached($feedItem->link); - $content = $articlePage->find('.article-text, depeche-text', 0); - if (!$content) { - return $item; - } - $item['content'] = sanitize($content); + $articlePage = getSimpleHTMLDOMCached($feedItem->link); + $content = $articlePage->find('.article-text, depeche-text', 0); + if (!$content) { + return $item; + } + $item['content'] = sanitize($content); - return $item; - } + return $item; + } } diff --git a/bridges/CraigslistBridge.php b/bridges/CraigslistBridge.php index 8e677cf4..d56c770e 100644 --- a/bridges/CraigslistBridge.php +++ b/bridges/CraigslistBridge.php @@ -1,106 +1,110 @@ <?php -class CraigslistBridge extends BridgeAbstract { - const NAME = 'Craigslist Bridge'; - const URI = 'https://craigslist.org/'; - const DESCRIPTION = 'Returns craigslist search results'; - const PARAMETERS = array( array( - 'region' => array( - 'name' => 'Region', - 'title' => 'The subdomain before craigslist.org in the URL', - 'exampleValue' => 'sfbay', - 'required' => true - ), - 'search' => array( - 'name' => 'Search Query', - 'title' => 'Everything in the URL after /search/', - 'exampleValue' => 'sya?query=laptop', - 'required' => true - ), - 'limit' => array( - 'name' => 'Number of Posts', - 'type' => 'number', - 'title' => 'The maximum number of posts is 120. Use 0 for unlimited posts.', - 'defaultValue' => '25' - ) - )); +class CraigslistBridge extends BridgeAbstract +{ + const NAME = 'Craigslist Bridge'; + const URI = 'https://craigslist.org/'; + const DESCRIPTION = 'Returns craigslist search results'; - const TEST_DETECT_PARAMETERS = array( - 'https://sfbay.craigslist.org/search/sya?query=laptop' => array( - 'region' => 'sfbay', 'search' => 'sya?query=laptop' - ), - 'https://newyork.craigslist.org/search/sss?query=32gb+flash+drive&bundleDuplicates=1&max_price=20' => array( - 'region' => 'newyork', 'search' => 'sss?query=32gb+flash+drive&bundleDuplicates=1&max_price=20' - ), - ); + const PARAMETERS = [ [ + 'region' => [ + 'name' => 'Region', + 'title' => 'The subdomain before craigslist.org in the URL', + 'exampleValue' => 'sfbay', + 'required' => true + ], + 'search' => [ + 'name' => 'Search Query', + 'title' => 'Everything in the URL after /search/', + 'exampleValue' => 'sya?query=laptop', + 'required' => true + ], + 'limit' => [ + 'name' => 'Number of Posts', + 'type' => 'number', + 'title' => 'The maximum number of posts is 120. Use 0 for unlimited posts.', + 'defaultValue' => '25' + ] + ]]; - const URL_REGEX = '/^https:\/\/(?<region>\w+).craigslist.org\/search\/(?<search>.+)/'; + const TEST_DETECT_PARAMETERS = [ + 'https://sfbay.craigslist.org/search/sya?query=laptop' => [ + 'region' => 'sfbay', 'search' => 'sya?query=laptop' + ], + 'https://newyork.craigslist.org/search/sss?query=32gb+flash+drive&bundleDuplicates=1&max_price=20' => [ + 'region' => 'newyork', 'search' => 'sss?query=32gb+flash+drive&bundleDuplicates=1&max_price=20' + ], + ]; - public function detectParameters($url) { - if(preg_match(self::URL_REGEX, $url, $matches)) { - $params = array(); - $params['region'] = $matches['region']; - $params['search'] = $matches['search']; - return $params; - } - } + const URL_REGEX = '/^https:\/\/(?<region>\w+).craigslist.org\/search\/(?<search>.+)/'; - public function getURI() { - if (!is_null($this->getInput('region'))) { - $domain = 'https://' . $this->getInput('region') . '.craigslist.org/search/'; - return urljoin($domain, $this->getInput('search')); - } - return parent::getURI(); - } + public function detectParameters($url) + { + if (preg_match(self::URL_REGEX, $url, $matches)) { + $params = []; + $params['region'] = $matches['region']; + $params['search'] = $matches['search']; + return $params; + } + } - public function collectData() { - $uri = $this->getURI(); - $html = getSimpleHTMLDOM($uri); + public function getURI() + { + if (!is_null($this->getInput('region'))) { + $domain = 'https://' . $this->getInput('region') . '.craigslist.org/search/'; + return urljoin($domain, $this->getInput('search')); + } + return parent::getURI(); + } - // Check if no results page is shown (nearby results) - if ($html->find('.displaycountShow', 0)->plaintext == '0') { - return; - } + public function collectData() + { + $uri = $this->getURI(); + $html = getSimpleHTMLDOM($uri); - // Search for "more from nearby areas" banner in order to skip those results - $results = $html->find('.result-row, h4.nearby'); + // Check if no results page is shown (nearby results) + if ($html->find('.displaycountShow', 0)->plaintext == '0') { + return; + } - // Limit the number of posts - if ($this->getInput('limit') > 0) { - $results = array_slice($results, 0, $this->getInput('limit')); - } + // Search for "more from nearby areas" banner in order to skip those results + $results = $html->find('.result-row, h4.nearby'); - foreach($results as $post) { + // Limit the number of posts + if ($this->getInput('limit') > 0) { + $results = array_slice($results, 0, $this->getInput('limit')); + } - // Skip "nearby results" banner and results - // This only appears when searchNearby is not specified - if ($post->tag == 'h4') { - break; - } + foreach ($results as $post) { + // Skip "nearby results" banner and results + // This only appears when searchNearby is not specified + if ($post->tag == 'h4') { + break; + } - $item = array(); + $item = []; - $heading = $post->find('.result-heading a', 0); - $item['uri'] = $heading->href; - $item['title'] = $heading->plaintext; - $item['timestamp'] = $post->find('.result-date', 0)->datetime; - $item['uid'] = $heading->id; - $item['content'] = $post->find('.result-price', 0)->plaintext . ' ' - // Find the location (local and nearby results if searchNearby=1) - . $post->find('.result-hood, span.nearby', 0)->plaintext; + $heading = $post->find('.result-heading a', 0); + $item['uri'] = $heading->href; + $item['title'] = $heading->plaintext; + $item['timestamp'] = $post->find('.result-date', 0)->datetime; + $item['uid'] = $heading->id; + $item['content'] = $post->find('.result-price', 0)->plaintext . ' ' + // Find the location (local and nearby results if searchNearby=1) + . $post->find('.result-hood, span.nearby', 0)->plaintext; - $images = $post->find('.result-image[data-ids]', 0); - if (!is_null($images)) { - $item['content'] .= '<br>'; - foreach(explode(',', $images->getAttribute('data-ids')) as $image) { - // Remove leading 3: from each image id - $id = substr($image, 2); - $image_uri = 'https://images.craigslist.org/' . $id . '_300x300.jpg'; - $item['content'] .= '<img src="' . $image_uri . '">'; - $item['enclosures'][] = $image_uri; - } - } - $this->items[] = $item; - } - } + $images = $post->find('.result-image[data-ids]', 0); + if (!is_null($images)) { + $item['content'] .= '<br>'; + foreach (explode(',', $images->getAttribute('data-ids')) as $image) { + // Remove leading 3: from each image id + $id = substr($image, 2); + $image_uri = 'https://images.craigslist.org/' . $id . '_300x300.jpg'; + $item['content'] .= '<img src="' . $image_uri . '">'; + $item['enclosures'][] = $image_uri; + } + } + $this->items[] = $item; + } + } } diff --git a/bridges/CrewbayBridge.php b/bridges/CrewbayBridge.php index a3c52b9a..0ca017c2 100644 --- a/bridges/CrewbayBridge.php +++ b/bridges/CrewbayBridge.php @@ -1,227 +1,233 @@ <?php -class CrewbayBridge extends BridgeAbstract { - const MAINTAINER = 'couraudt'; - const NAME = 'Crewbay Bridge'; - const URI = 'https://www.crewbay.com'; - const DESCRIPTION = 'Returns the newest sailing offers.'; - const PARAMETERS = array( - array( - 'keyword' => array( - 'name' => 'Filter by keyword', - 'title' => 'Enter the keyword to filter here' - ), - 'type' => array( - 'name' => 'Type of search', - 'title' => 'Choose between finding a boat or a crew', - 'type' => 'list', - 'values' => array( - 'Find a boat' => 'boats', - 'Find a crew' => 'crew' - ) - ), - 'status' => array( - 'name' => 'Status on the boat', - 'title' => 'Choose between recreational or professional classified ads', - 'type' => 'list', - 'values' => array( - 'Recreational' => 'recreational', - 'Professional' => 'professional' - ) - ), - 'recreational_position' => array( - 'name' => 'Recreational position wanted', - 'title' => 'Filter by recreational position you wanted aboard', - 'required' => false, - 'type' => 'list', - 'values' => array( - '' => '', - 'Amateur Crew' => 'Amateur Crew', - 'Friendship' => 'Friendship', - 'Competent Crew' => 'Competent Crew', - 'Racing' => 'Racing', - 'Voluntary work' => 'Voluntary work', - 'Mile building' => 'Mile building' - ) - ), - 'professional_position' => array( - 'name' => 'Professional position wanted', - 'title' => 'Filter by professional position you wanted aboard', - 'required' => false, - 'type' => 'list', - 'values' => array( - '' => '', - '1st Engineer' => '1st Engineer', - '1st Mate' => '1st Mate', - 'Beautician' => 'Beautician', - 'Bosun' => 'Bosun', - 'Captain' => 'Captain', - 'Chef' => 'Chef', - 'Steward(ess)' => 'Steward(ess)', - 'Deckhand' => 'Deckhand', - 'Delivery Crew' => 'Delivery Crew', - 'Dive Instructor' => 'Dive Instructor', - 'Masseur' => 'Masseur', - 'Medical Staff' => 'Medical Staff', - 'Nanny' => 'Nanny', - 'Navigator' => 'Navigator', - 'Racing Crew' => 'Racing Crew', - 'Teacher' => 'Teacher', - 'Electrical Engineer' => 'Electrical Engineer', - 'Fitter' => 'Fitter', - '2nd Engineer' => '2nd Engineer', - '3rd Engineer' => '3rd Engineer', - 'Lead Deckhand' => 'Lead Deckhand', - 'Security Officer' => 'Security Officer', - 'O.O.W' => 'O.O.W', - '1st Officer' => '1st Officer', - '2nd Officer' => '2nd Officer', - '3rd Officer' => '3rd Officer', - 'Captain/Engineer' => 'Captain/Engineer', - 'Hairdresser' => 'Hairdresser', - 'Fitness Trainer' => 'Fitness Trainer', - 'Laundry' => 'Laundry', - 'Solo Steward/ess' => 'Solo Steward/ess', - 'Stew/Deck' => 'Stew/Deck', - '2nd Steward/ess' => '2nd Steward/ess', - '3rd Steward/ess' => '3rd Steward/ess', - 'Chief Steward/ess' => 'Chief Steward/ess', - 'Head Housekeeper' => 'Head Housekeeper', - 'Purser' => 'Purser', - 'Cook' => 'Cook', - 'Cook/Stew' => 'Cook/Stew', - '2nd Chef' => '2nd Chef', - 'Head Chef' => 'Head Chef', - 'Administrator' => 'Administrator', - 'P.A' => 'P.A', - 'Villa staff' => 'Villa staff', - 'Housekeeping/Stew' => 'Housekeeping/Stew', - 'Stew/Beautician' => 'Stew/Beautician', - 'Stew/Masseuse' => 'Stew/Masseuse', - 'Manager' => 'Manager', - 'Sailing instructor' => 'Sailing instructor' - ) - ) - ) - ); - - public function collectData() { - $url = $this->getURI(); - $html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.'); - - $annonces = $html->find('#SearchResults div.result'); - $limit = 0; - - foreach ($annonces as $annonce) { - $detail = $annonce->find('.btn--profile', 0); - $htmlDetail = getSimpleHTMLDOMCached($detail->href); - - if (!empty($this->getInput('recreational_position')) || !empty($this->getInput('professional_position'))) { - if ($this->getInput('type') == 'boats') { - if ($this->getInput('status') == 'professional') { - $positions = array($annonce->find('.title .position', 0)->plaintext); - } else { - $positions = array(str_replace('Wanted:', '', $annonce->find('.content li', 0)->plaintext)); - } - } else { - $list = $htmlDetail->find('.viewer-details .viewer-list'); - $positions = explode("\r\n", end($list)->find('span.value', 0)->plaintext); - } - - $found = false; - $keyword = $this->getInput('status') == 'professional' ? 'professional_position' : 'recreational_position'; - foreach ($positions as $position) { - if (strpos(trim($position), $this->getInput($keyword)) !== false) { - $found = true; - break; - } - } - - if (!$found) { - continue; - } - } - - $item = array(); - - if ($this->getInput('type') == 'boats') { - $titleSelector = '.title h2'; - } else { - $titleSelector = '.layout__item h2'; - } - $userName = $annonce->find('.result--description a', 0)->plaintext; - $annonceTitle = trim($annonce->find($titleSelector, 0)->plaintext); - if (empty($annonceTitle)) { - $item['title'] = $userName; - } else { - $item['title'] = $userName . ' - ' . $annonceTitle; - } - - $item['uri'] = $detail->href; - $images = $annonce->find('.avatar img'); - $item['enclosures'] = array(end($images)->getAttribute('src')); - - $content = $htmlDetail->find('.viewer-intro--info', 0)->innertext; - - $sections = $htmlDetail->find('.viewer-container .viewer-section'); - foreach ($sections as $section) { - if ($section->find('.viewer-section-title', 0)) { - $class = str_replace('viewer-', '', explode(' ', $section->getAttribute('class'))[0]); - if (!in_array($class, array('apply', 'photos', 'reviews', 'contact', 'experience', 'qa'))) { - // Basic sections - $content .= $section->find('.viewer-section-title h3', 0)->outertext; - $content .= $section->find('.viewer-section-content', 0)->innertext; - } - } else { - // Info section - $content .= $section->find('.viewer-section-content h3', 0)->outertext; - $content .= $section->find('.viewer-section-content p', 0)->outertext; - } - } - - if (!empty($this->getInput('keyword'))) { - $keyword = strtolower($this->getInput('keyword')); - if (strpos(strtolower($item['title']), $keyword) === false) { - if (strpos(strtolower($content), $keyword) === false) { - continue; - } - } - } - - $item['content'] = $content; - - $tags = $htmlDetail->find('li.viewer-tags--tag'); - foreach ($tags as $tag) { - if (!isset($item['categories'])) { - $item['categories'] = array(); - } - $text = trim($tag->plaintext); - if (!in_array($text, $item['categories'])) { - $item['categories'][] = $text; - } - } - - $this->items[] = $item; - $limit += 1; - - if ($limit == 10) break; - } - } - - public function getURI() { - $uri = parent::getURI(); - - if ($this->getInput('type') == 'boats') { - $uri .= '/boats'; - } else { - $uri .= '/crew'; - } - - if ($this->getInput('status') == 'professional') { - $uri .= '/professional'; - } else { - $uri .= '/recreational'; - } - - return $uri; - } + +class CrewbayBridge extends BridgeAbstract +{ + const MAINTAINER = 'couraudt'; + const NAME = 'Crewbay Bridge'; + const URI = 'https://www.crewbay.com'; + const DESCRIPTION = 'Returns the newest sailing offers.'; + const PARAMETERS = [ + [ + 'keyword' => [ + 'name' => 'Filter by keyword', + 'title' => 'Enter the keyword to filter here' + ], + 'type' => [ + 'name' => 'Type of search', + 'title' => 'Choose between finding a boat or a crew', + 'type' => 'list', + 'values' => [ + 'Find a boat' => 'boats', + 'Find a crew' => 'crew' + ] + ], + 'status' => [ + 'name' => 'Status on the boat', + 'title' => 'Choose between recreational or professional classified ads', + 'type' => 'list', + 'values' => [ + 'Recreational' => 'recreational', + 'Professional' => 'professional' + ] + ], + 'recreational_position' => [ + 'name' => 'Recreational position wanted', + 'title' => 'Filter by recreational position you wanted aboard', + 'required' => false, + 'type' => 'list', + 'values' => [ + '' => '', + 'Amateur Crew' => 'Amateur Crew', + 'Friendship' => 'Friendship', + 'Competent Crew' => 'Competent Crew', + 'Racing' => 'Racing', + 'Voluntary work' => 'Voluntary work', + 'Mile building' => 'Mile building' + ] + ], + 'professional_position' => [ + 'name' => 'Professional position wanted', + 'title' => 'Filter by professional position you wanted aboard', + 'required' => false, + 'type' => 'list', + 'values' => [ + '' => '', + '1st Engineer' => '1st Engineer', + '1st Mate' => '1st Mate', + 'Beautician' => 'Beautician', + 'Bosun' => 'Bosun', + 'Captain' => 'Captain', + 'Chef' => 'Chef', + 'Steward(ess)' => 'Steward(ess)', + 'Deckhand' => 'Deckhand', + 'Delivery Crew' => 'Delivery Crew', + 'Dive Instructor' => 'Dive Instructor', + 'Masseur' => 'Masseur', + 'Medical Staff' => 'Medical Staff', + 'Nanny' => 'Nanny', + 'Navigator' => 'Navigator', + 'Racing Crew' => 'Racing Crew', + 'Teacher' => 'Teacher', + 'Electrical Engineer' => 'Electrical Engineer', + 'Fitter' => 'Fitter', + '2nd Engineer' => '2nd Engineer', + '3rd Engineer' => '3rd Engineer', + 'Lead Deckhand' => 'Lead Deckhand', + 'Security Officer' => 'Security Officer', + 'O.O.W' => 'O.O.W', + '1st Officer' => '1st Officer', + '2nd Officer' => '2nd Officer', + '3rd Officer' => '3rd Officer', + 'Captain/Engineer' => 'Captain/Engineer', + 'Hairdresser' => 'Hairdresser', + 'Fitness Trainer' => 'Fitness Trainer', + 'Laundry' => 'Laundry', + 'Solo Steward/ess' => 'Solo Steward/ess', + 'Stew/Deck' => 'Stew/Deck', + '2nd Steward/ess' => '2nd Steward/ess', + '3rd Steward/ess' => '3rd Steward/ess', + 'Chief Steward/ess' => 'Chief Steward/ess', + 'Head Housekeeper' => 'Head Housekeeper', + 'Purser' => 'Purser', + 'Cook' => 'Cook', + 'Cook/Stew' => 'Cook/Stew', + '2nd Chef' => '2nd Chef', + 'Head Chef' => 'Head Chef', + 'Administrator' => 'Administrator', + 'P.A' => 'P.A', + 'Villa staff' => 'Villa staff', + 'Housekeeping/Stew' => 'Housekeeping/Stew', + 'Stew/Beautician' => 'Stew/Beautician', + 'Stew/Masseuse' => 'Stew/Masseuse', + 'Manager' => 'Manager', + 'Sailing instructor' => 'Sailing instructor' + ] + ] + ] + ]; + + public function collectData() + { + $url = $this->getURI(); + $html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.'); + + $annonces = $html->find('#SearchResults div.result'); + $limit = 0; + + foreach ($annonces as $annonce) { + $detail = $annonce->find('.btn--profile', 0); + $htmlDetail = getSimpleHTMLDOMCached($detail->href); + + if (!empty($this->getInput('recreational_position')) || !empty($this->getInput('professional_position'))) { + if ($this->getInput('type') == 'boats') { + if ($this->getInput('status') == 'professional') { + $positions = [$annonce->find('.title .position', 0)->plaintext]; + } else { + $positions = [str_replace('Wanted:', '', $annonce->find('.content li', 0)->plaintext)]; + } + } else { + $list = $htmlDetail->find('.viewer-details .viewer-list'); + $positions = explode("\r\n", end($list)->find('span.value', 0)->plaintext); + } + + $found = false; + $keyword = $this->getInput('status') == 'professional' ? 'professional_position' : 'recreational_position'; + foreach ($positions as $position) { + if (strpos(trim($position), $this->getInput($keyword)) !== false) { + $found = true; + break; + } + } + + if (!$found) { + continue; + } + } + + $item = []; + + if ($this->getInput('type') == 'boats') { + $titleSelector = '.title h2'; + } else { + $titleSelector = '.layout__item h2'; + } + $userName = $annonce->find('.result--description a', 0)->plaintext; + $annonceTitle = trim($annonce->find($titleSelector, 0)->plaintext); + if (empty($annonceTitle)) { + $item['title'] = $userName; + } else { + $item['title'] = $userName . ' - ' . $annonceTitle; + } + + $item['uri'] = $detail->href; + $images = $annonce->find('.avatar img'); + $item['enclosures'] = [end($images)->getAttribute('src')]; + + $content = $htmlDetail->find('.viewer-intro--info', 0)->innertext; + + $sections = $htmlDetail->find('.viewer-container .viewer-section'); + foreach ($sections as $section) { + if ($section->find('.viewer-section-title', 0)) { + $class = str_replace('viewer-', '', explode(' ', $section->getAttribute('class'))[0]); + if (!in_array($class, ['apply', 'photos', 'reviews', 'contact', 'experience', 'qa'])) { + // Basic sections + $content .= $section->find('.viewer-section-title h3', 0)->outertext; + $content .= $section->find('.viewer-section-content', 0)->innertext; + } + } else { + // Info section + $content .= $section->find('.viewer-section-content h3', 0)->outertext; + $content .= $section->find('.viewer-section-content p', 0)->outertext; + } + } + + if (!empty($this->getInput('keyword'))) { + $keyword = strtolower($this->getInput('keyword')); + if (strpos(strtolower($item['title']), $keyword) === false) { + if (strpos(strtolower($content), $keyword) === false) { + continue; + } + } + } + + $item['content'] = $content; + + $tags = $htmlDetail->find('li.viewer-tags--tag'); + foreach ($tags as $tag) { + if (!isset($item['categories'])) { + $item['categories'] = []; + } + $text = trim($tag->plaintext); + if (!in_array($text, $item['categories'])) { + $item['categories'][] = $text; + } + } + + $this->items[] = $item; + $limit += 1; + + if ($limit == 10) { + break; + } + } + } + + public function getURI() + { + $uri = parent::getURI(); + + if ($this->getInput('type') == 'boats') { + $uri .= '/boats'; + } else { + $uri .= '/crew'; + } + + if ($this->getInput('status') == 'professional') { + $uri .= '/professional'; + } else { + $uri .= '/recreational'; + } + + return $uri; + } } diff --git a/bridges/CryptomeBridge.php b/bridges/CryptomeBridge.php index 50ab48bc..de5544ec 100644 --- a/bridges/CryptomeBridge.php +++ b/bridges/CryptomeBridge.php @@ -1,44 +1,47 @@ <?php -class CryptomeBridge extends BridgeAbstract { - const MAINTAINER = 'BoboTiG'; - const NAME = 'Cryptome'; - const URI = 'https://cryptome.org/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Returns the N most recent documents.'; - const PARAMETERS = array( array( - 'n' => array( - 'name' => 'number of elements', - 'type' => 'number', - 'required' => true, - 'exampleValue' => 10 - ) - )); +class CryptomeBridge extends BridgeAbstract +{ + const MAINTAINER = 'BoboTiG'; + const NAME = 'Cryptome'; + const URI = 'https://cryptome.org/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Returns the N most recent documents.'; + const PARAMETERS = [ [ + 'n' => [ + 'name' => 'number of elements', + 'type' => 'number', + 'required' => true, + 'exampleValue' => 10 + ] + ]]; - public function getIcon() { - return self::URI . '/favicon.ico'; - } + public function getIcon() + { + return self::URI . '/favicon.ico'; + } - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $number = $this->getInput('n'); - if(!empty($number)) { - $num = min($number, 20); - } - $i = 0; - foreach($html->find('pre', 1)->find('b') as $element) { - foreach($element->find('a') as $element1) { - $item = array(); - $item['uri'] = $element1->href; - $item['title'] = $element->plaintext; - $this->items[] = $item; + $number = $this->getInput('n'); + if (!empty($number)) { + $num = min($number, 20); + } + $i = 0; + foreach ($html->find('pre', 1)->find('b') as $element) { + foreach ($element->find('a') as $element1) { + $item = []; + $item['uri'] = $element1->href; + $item['title'] = $element->plaintext; + $this->items[] = $item; - if ($i > $num) { - break 2; - } - $i++; - } - } - } + if ($i > $num) { + break 2; + } + $i++; + } + } + } } diff --git a/bridges/CubariBridge.php b/bridges/CubariBridge.php index 00a2a0ba..9a08dbba 100644 --- a/bridges/CubariBridge.php +++ b/bridges/CubariBridge.php @@ -1,98 +1,102 @@ <?php + class CubariBridge extends BridgeAbstract { - const NAME = 'Cubari'; - const URI = 'https://cubari.moe'; - const DESCRIPTION = 'Parses given cubari-formatted JSON file for updates.'; - const MAINTAINER = 'KamaleiZestri'; - const PARAMETERS = array(array( - 'gist' => array( - 'name' => 'Gist/Raw Url', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'https://raw.githubusercontent.com/kurisumx/baka/main/ikedan' - ) - )); - - private $mangaTitle = ''; - - public function getName() - { - if (!empty($this->mangaTitle)) - return $this->mangaTitle . ' - ' . self::NAME; - else - return self::NAME; - } - - public function getURI() - { - if ($this->getInput('gist') != '') - return self::URI . '/read/gist/' . $this->getEncodedGist(); - else - return self::URI; - } - - /** - * The Cubari bridge. - * - * Cubari urls are base64 encodes of a given github raw or gist link described as below: - * https://cubari.moe/read/gist/${bаse64.url_encode(raw/<rest of the url...>)}/ - * https://cubari.moe/read/gist/${bаse64.url_encode(gist/<rest of the url...>)}/ - * https://cubari.moe/read/gist/${gitio shortcode} - * - * This bridge uses just the raw/gist and generates matching cubari urls. - */ - public function collectData() - { - $jsonSite = getContents($this->getInput('gist')); - $jsonFile = json_decode($jsonSite, true); - - $this->mangaTitle = $jsonFile['title']; - - $chapters = $jsonFile['chapters']; - - foreach ($chapters as $chapnum => $chapter) { - $item = $this->getItemFromChapter($chapnum, $chapter); - $this->items[] = $item; - } - - array_multisort(array_column($this->items, 'timestamp'), SORT_DESC, $this->items); - } - - protected function getEncodedGist() - { - $url = $this->getInput('gist'); - - preg_match('/\/([a-z]*)\.githubusercontent.com(.*)/', $url, $matches); - - // raw or gist is first match. - $unencoded = $matches[1] . $matches[2]; - - return base64_encode($unencoded); - } - - private function getSanitizedHash($string) - { - return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string)))); - } - - protected function getItemFromChapter($chapnum, $chapter) - { - $item = array(); - - $item['uri'] = $this->getURI() . '/' . $chapnum; - $item['title'] = 'Chapter ' . $chapnum . ' - ' . $chapter['title'] . ' - ' . $this->mangaTitle; - foreach ($chapter['groups'] as $key => $value) - $item['author'] = $key; - $item['timestamp'] = $chapter['last_updated']; - - $item['content'] = '<p>Manga: <a href=' . $this->getURI() . '>' . $this->mangaTitle . '</a> </p> + const NAME = 'Cubari'; + const URI = 'https://cubari.moe'; + const DESCRIPTION = 'Parses given cubari-formatted JSON file for updates.'; + const MAINTAINER = 'KamaleiZestri'; + const PARAMETERS = [[ + 'gist' => [ + 'name' => 'Gist/Raw Url', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'https://raw.githubusercontent.com/kurisumx/baka/main/ikedan' + ] + ]]; + + private $mangaTitle = ''; + + public function getName() + { + if (!empty($this->mangaTitle)) { + return $this->mangaTitle . ' - ' . self::NAME; + } else { + return self::NAME; + } + } + + public function getURI() + { + if ($this->getInput('gist') != '') { + return self::URI . '/read/gist/' . $this->getEncodedGist(); + } else { + return self::URI; + } + } + + /** + * The Cubari bridge. + * + * Cubari urls are base64 encodes of a given github raw or gist link described as below: + * https://cubari.moe/read/gist/${bаse64.url_encode(raw/<rest of the url...>)}/ + * https://cubari.moe/read/gist/${bаse64.url_encode(gist/<rest of the url...>)}/ + * https://cubari.moe/read/gist/${gitio shortcode} + * + * This bridge uses just the raw/gist and generates matching cubari urls. + */ + public function collectData() + { + $jsonSite = getContents($this->getInput('gist')); + $jsonFile = json_decode($jsonSite, true); + + $this->mangaTitle = $jsonFile['title']; + + $chapters = $jsonFile['chapters']; + + foreach ($chapters as $chapnum => $chapter) { + $item = $this->getItemFromChapter($chapnum, $chapter); + $this->items[] = $item; + } + + array_multisort(array_column($this->items, 'timestamp'), SORT_DESC, $this->items); + } + + protected function getEncodedGist() + { + $url = $this->getInput('gist'); + + preg_match('/\/([a-z]*)\.githubusercontent.com(.*)/', $url, $matches); + + // raw or gist is first match. + $unencoded = $matches[1] . $matches[2]; + + return base64_encode($unencoded); + } + + private function getSanitizedHash($string) + { + return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string)))); + } + + protected function getItemFromChapter($chapnum, $chapter) + { + $item = []; + + $item['uri'] = $this->getURI() . '/' . $chapnum; + $item['title'] = 'Chapter ' . $chapnum . ' - ' . $chapter['title'] . ' - ' . $this->mangaTitle; + foreach ($chapter['groups'] as $key => $value) { + $item['author'] = $key; + } + $item['timestamp'] = $chapter['last_updated']; + + $item['content'] = '<p>Manga: <a href=' . $this->getURI() . '>' . $this->mangaTitle . '</a> </p> <p>Chapter Number: ' . $chapnum . '</p> <p>Chapter Title: <a href=' . $item['uri'] . '>' . $chapter['title'] . '</a></p> <p>Group: ' . $item['author'] . '</p>'; - $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']); + $item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']); - return $item; - } + return $item; + } } diff --git a/bridges/CuriousCatBridge.php b/bridges/CuriousCatBridge.php index 641da5d8..573c776f 100644 --- a/bridges/CuriousCatBridge.php +++ b/bridges/CuriousCatBridge.php @@ -1,108 +1,110 @@ <?php -class CuriousCatBridge extends BridgeAbstract { - const NAME = 'Curious Cat Bridge'; - const URI = 'https://curiouscat.me'; - const DESCRIPTION = 'Returns list of newest questions and answers for a user profile'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(array( - 'username' => array( - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'koethekoethe', - ) - )); - const CACHE_TIMEOUT = 3600; +class CuriousCatBridge extends BridgeAbstract +{ + const NAME = 'Curious Cat Bridge'; + const URI = 'https://curiouscat.me'; + const DESCRIPTION = 'Returns list of newest questions and answers for a user profile'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [[ + 'username' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'koethekoethe', + ] + ]]; - public function collectData() { + const CACHE_TIMEOUT = 3600; - $url = self::URI . '/api/v2/profile?username=' . urlencode($this->getInput('username')); + public function collectData() + { + $url = self::URI . '/api/v2/profile?username=' . urlencode($this->getInput('username')); - $apiJson = getContents($url); + $apiJson = getContents($url); - $apiData = json_decode($apiJson, true); + $apiData = json_decode($apiJson, true); - foreach($apiData['posts'] as $post) { - $item = array(); + foreach ($apiData['posts'] as $post) { + $item = []; - $item['author'] = 'Anonymous'; + $item['author'] = 'Anonymous'; - if ($post['senderData']['id'] !== false) { - $item['author'] = $post['senderData']['username']; - } + if ($post['senderData']['id'] !== false) { + $item['author'] = $post['senderData']['username']; + } - $item['uri'] = $this->getURI() . '/post/' . $post['id']; - $item['title'] = $this->ellipsisTitle($post['comment']); + $item['uri'] = $this->getURI() . '/post/' . $post['id']; + $item['title'] = $this->ellipsisTitle($post['comment']); - $item['content'] = $this->processContent($post); - $item['timestamp'] = $post['timestamp']; + $item['content'] = $this->processContent($post); + $item['timestamp'] = $post['timestamp']; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getURI() { + public function getURI() + { + if (!is_null($this->getInput('username'))) { + return self::URI . '/' . $this->getInput('username'); + } - if (!is_null($this->getInput('username'))) { - return self::URI . '/' . $this->getInput('username'); - } + return parent::getURI(); + } - return parent::getURI(); - } + public function getName() + { + if (!is_null($this->getInput('username'))) { + return $this->getInput('username') . ' - Curious Cat'; + } - public function getName() { + return parent::getName(); + } - if (!is_null($this->getInput('username'))) { - return $this->getInput('username') . ' - Curious Cat'; - } + private function processContent($post) + { + $author = 'Anonymous'; - return parent::getName(); - } + if ($post['senderData']['id'] !== false) { + $authorUrl = self::URI . '/' . $post['senderData']['username']; - private function processContent($post) { - - $author = 'Anonymous'; - - if ($post['senderData']['id'] !== false) { - $authorUrl = self::URI . '/' . $post['senderData']['username']; - - $author = <<<EOD + $author = <<<EOD <a href="{$authorUrl}">{$post['senderData']['username']}</a> EOD; - } + } - $question = $this->formatUrls($post['comment']); - $answer = $this->formatUrls($post['reply']); + $question = $this->formatUrls($post['comment']); + $answer = $this->formatUrls($post['reply']); - $content = <<<EOD + $content = <<<EOD <p>{$author} asked:</p> <blockquote>{$question}</blockquote><br/> <p>{$post['addresseeData']['username']} answered:</p> <blockquote>{$answer}</blockquote> EOD; - return $content; - } - - private function ellipsisTitle($text) { - $length = 150; - - if (strlen($text) > $length) { - $text = explode('<br>', wordwrap($text, $length, '<br>')); - return $text[0] . '...'; - } - - return $text; - } - - private function formatUrls($content) { - - return preg_replace( - '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims', - '<a target="_blank" href="$1" target="_blank">$1</a> ', - $content - ); - - } + return $content; + } + + private function ellipsisTitle($text) + { + $length = 150; + + if (strlen($text) > $length) { + $text = explode('<br>', wordwrap($text, $length, '<br>')); + return $text[0] . '...'; + } + + return $text; + } + + private function formatUrls($content) + { + return preg_replace( + '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims', + '<a target="_blank" href="$1" target="_blank">$1</a> ', + $content + ); + } } diff --git a/bridges/CyanideAndHappinessBridge.php b/bridges/CyanideAndHappinessBridge.php index 41ac096a..2b54affc 100644 --- a/bridges/CyanideAndHappinessBridge.php +++ b/bridges/CyanideAndHappinessBridge.php @@ -1,37 +1,42 @@ <?php -class CyanideAndHappinessBridge extends BridgeAbstract { - const NAME = 'Cyanide & Happiness'; - const URI = 'https://explosm.net/'; - const DESCRIPTION = 'The Webcomic from Explosm.'; - const MAINTAINER = 'sal0max'; - const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours - public function getIcon() { - return self::URI . 'favicon-32x32.png'; - } +class CyanideAndHappinessBridge extends BridgeAbstract +{ + const NAME = 'Cyanide & Happiness'; + const URI = 'https://explosm.net/'; + const DESCRIPTION = 'The Webcomic from Explosm.'; + const MAINTAINER = 'sal0max'; + const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours - public function getURI(){ - return self::URI . 'comics/latest#comic'; - } + public function getIcon() + { + return self::URI . 'favicon-32x32.png'; + } - public function collectData() { - $html = getSimpleHTMLDOM($this->getUri()); + public function getURI() + { + return self::URI . 'comics/latest#comic'; + } - foreach ($html->find('[class*=ComicImage]') as $element) { - $date = $element->find('[class^=Author__Right] p', 0)->plaintext; - $author = str_replace('by ', '', $element->find('[class^=Author__Right] p', 1)->plaintext); - $image = $element->find('img', 0)->src; - $link = $html->find('[rel=canonical]', 0)->href; + public function collectData() + { + $html = getSimpleHTMLDOM($this->getUri()); - $item = array( - 'uid' => $link, - 'author' => $author, - 'title' => $date, - 'uri' => $link . '#comic', - 'timestamp' => str_replace('.', '-', $date) . 'T00:00:00Z', - 'content' => "<img src=\"$image\" />" - ); - $this->items[] = $item; - } - } + foreach ($html->find('[class*=ComicImage]') as $element) { + $date = $element->find('[class^=Author__Right] p', 0)->plaintext; + $author = str_replace('by ', '', $element->find('[class^=Author__Right] p', 1)->plaintext); + $image = $element->find('img', 0)->src; + $link = $html->find('[rel=canonical]', 0)->href; + + $item = [ + 'uid' => $link, + 'author' => $author, + 'title' => $date, + 'uri' => $link . '#comic', + 'timestamp' => str_replace('.', '-', $date) . 'T00:00:00Z', + 'content' => "<img src=\"$image\" />" + ]; + $this->items[] = $item; + } + } } diff --git a/bridges/DailymotionBridge.php b/bridges/DailymotionBridge.php index 688c0a10..5d892954 100644 --- a/bridges/DailymotionBridge.php +++ b/bridges/DailymotionBridge.php @@ -1,203 +1,209 @@ <?php -class DailymotionBridge extends BridgeAbstract { - - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Dailymotion Bridge'; - const URI = 'https://www.dailymotion.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns the 5 newest videos by username/playlist or search'; - - const PARAMETERS = array ( - 'By username' => array( - 'u' => array( - 'name' => 'username', - 'required' => true, - 'exampleValue' => 'moviepilot', - ) - ), - 'By playlist id' => array( - 'p' => array( - 'name' => 'playlist id', - 'required' => true, - 'exampleValue' => 'x6xyc6', - ) - ), - 'From search results' => array( - 's' => array( - 'name' => 'Search keyword', - 'required' => true, - 'exampleValue' => 'matrix', - ), - 'pa' => array( - 'name' => 'Page', - 'type' => 'number', - 'defaultValue' => 1, - ) - ) - ); - - private $feedName = ''; - - private $apiUrl = 'https://api.dailymotion.com'; - private $apiFields = 'created_time,description,id,owner.screenname,tags,thumbnail_url,title,url'; - - public function getIcon() { - return 'https://static1-ssl.dmcdn.net/images/neon/favicons/android-icon-36x36.png.vf806ca4ed0deed812'; - } - - public function collectData() { - - if ($this->queriedContext === 'By username' || $this->queriedContext === 'By playlist id') { - - $apiJson = getContents($this->getApiUrl()); - - $apiData = json_decode($apiJson, true); - - $this->feedName = $this->getPlaylistTitle($this->getInput('p')); - - foreach ($apiData['list'] as $apiItem) { - $item = array(); - - $item['uri'] = $apiItem['url']; - $item['uid'] = $apiItem['id']; - $item['title'] = $apiItem['title']; - $item['timestamp'] = $apiItem['created_time']; - $item['author'] = $apiItem['owner.screenname']; - $item['content'] = '<p><a href="' . $apiItem['url'] . '"> + +class DailymotionBridge extends BridgeAbstract +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Dailymotion Bridge'; + const URI = 'https://www.dailymotion.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns the 5 newest videos by username/playlist or search'; + + const PARAMETERS = [ + 'By username' => [ + 'u' => [ + 'name' => 'username', + 'required' => true, + 'exampleValue' => 'moviepilot', + ] + ], + 'By playlist id' => [ + 'p' => [ + 'name' => 'playlist id', + 'required' => true, + 'exampleValue' => 'x6xyc6', + ] + ], + 'From search results' => [ + 's' => [ + 'name' => 'Search keyword', + 'required' => true, + 'exampleValue' => 'matrix', + ], + 'pa' => [ + 'name' => 'Page', + 'type' => 'number', + 'defaultValue' => 1, + ] + ] + ]; + + private $feedName = ''; + + private $apiUrl = 'https://api.dailymotion.com'; + private $apiFields = 'created_time,description,id,owner.screenname,tags,thumbnail_url,title,url'; + + public function getIcon() + { + return 'https://static1-ssl.dmcdn.net/images/neon/favicons/android-icon-36x36.png.vf806ca4ed0deed812'; + } + + public function collectData() + { + if ($this->queriedContext === 'By username' || $this->queriedContext === 'By playlist id') { + $apiJson = getContents($this->getApiUrl()); + + $apiData = json_decode($apiJson, true); + + $this->feedName = $this->getPlaylistTitle($this->getInput('p')); + + foreach ($apiData['list'] as $apiItem) { + $item = []; + + $item['uri'] = $apiItem['url']; + $item['uid'] = $apiItem['id']; + $item['title'] = $apiItem['title']; + $item['timestamp'] = $apiItem['created_time']; + $item['author'] = $apiItem['owner.screenname']; + $item['content'] = '<p><a href="' . $apiItem['url'] . '"> <img src="' . $apiItem['thumbnail_url'] . '"></a></p><p>' . $apiItem['description'] . '</p>'; - $item['categories'] = $apiItem['tags']; - $item['enclosures'][] = $apiItem['thumbnail_url']; - - $this->items[] = $item; - } - } - - if ($this->queriedContext === 'From search results') { - - $html = getSimpleHTMLDOM($this->getURI()); - - foreach($html->find('div.media a.preview_link') as $element) { - $item = array(); - - $item['id'] = str_replace('/video/', '', strtok($element->href, '_')); - $metadata = $this->getMetadata($item['id']); - - if(empty($metadata)) { - continue; - } - - $item['uri'] = $metadata['uri']; - $item['title'] = $metadata['title']; - $item['timestamp'] = $metadata['timestamp']; - - $item['content'] = '<a href="' - . $item['uri'] - . '"><img src="' - . $metadata['thumbnailUri'] - . '" /></a><br><a href="' - . $item['uri'] - . '">' - . $item['title'] - . '</a>'; - - $this->items[] = $item; - - if (count($this->items) >= 5) { - break; - } - } - } - } - - public function getName() { - switch($this->queriedContext) { - case 'By username': - $specific = $this->getInput('u'); - break; - case 'By playlist id': - $specific = strtok($this->getInput('p'), '_'); - - if ($this->feedName) { - $specific = $this->feedName; - } - - break; - case 'From search results': - $specific = $this->getInput('s'); - break; - default: return parent::getName(); - } - - return $specific . ' : Dailymotion'; - } - - public function getURI(){ - $uri = self::URI; - switch($this->queriedContext) { - case 'By username': - $uri .= 'user/' . urlencode($this->getInput('u')); - break; - case 'By playlist id': - $uri .= 'playlist/' . urlencode(strtok($this->getInput('p'), '_')); - break; - case 'From search results': - $uri .= 'search/' . urlencode($this->getInput('s')); - - if(!is_null($this->getInput('pa'))) { - $pa = $this->getInput('pa'); - - if ($this->getInput('pa') < 1) { - $pa = 1; - } - - $uri .= '/' . $pa; - } - break; - default: return parent::getURI(); - } - return $uri; - } - - private function getMetadata($id) { - $metadata = array(); - - $html = getSimpleHTMLDOM(self::URI . 'video/' . $id); - - if(!$html) { - return $metadata; - } - - $metadata['title'] = $html->find('meta[property=og:title]', 0)->getAttribute('content'); - $metadata['timestamp'] = strtotime( - $html->find('meta[property=video:release_date]', 0)->getAttribute('content') - ); - $metadata['thumbnailUri'] = $html->find('meta[property=og:image]', 0)->getAttribute('content'); - $metadata['uri'] = $html->find('meta[property=og:url]', 0)->getAttribute('content'); - return $metadata; - } - - private function getPlaylistTitle($id) { - $title = ''; - - $url = self::URI . 'playlist/' . $id; - - $html = getSimpleHTMLDOM($url); - - $title = $html->find('meta[property=og:title]', 0)->getAttribute('content'); - return $title; - } - - private function getApiUrl() { - - switch($this->queriedContext) { - case 'By username': - return $this->apiUrl . '/user/' . $this->getInput('u') - . '/videos?fields=' . urlencode($this->apiFields) . '&availability=1&sort=recent&limit=5'; - break; - case 'By playlist id': - return $this->apiUrl . '/playlist/' . $this->getInput('p') - . '/videos?fields=' . urlencode($this->apiFields) . '&limit=5'; - break; - } - } + $item['categories'] = $apiItem['tags']; + $item['enclosures'][] = $apiItem['thumbnail_url']; + + $this->items[] = $item; + } + } + + if ($this->queriedContext === 'From search results') { + $html = getSimpleHTMLDOM($this->getURI()); + + foreach ($html->find('div.media a.preview_link') as $element) { + $item = []; + + $item['id'] = str_replace('/video/', '', strtok($element->href, '_')); + $metadata = $this->getMetadata($item['id']); + + if (empty($metadata)) { + continue; + } + + $item['uri'] = $metadata['uri']; + $item['title'] = $metadata['title']; + $item['timestamp'] = $metadata['timestamp']; + + $item['content'] = '<a href="' + . $item['uri'] + . '"><img src="' + . $metadata['thumbnailUri'] + . '" /></a><br><a href="' + . $item['uri'] + . '">' + . $item['title'] + . '</a>'; + + $this->items[] = $item; + + if (count($this->items) >= 5) { + break; + } + } + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'By username': + $specific = $this->getInput('u'); + break; + case 'By playlist id': + $specific = strtok($this->getInput('p'), '_'); + + if ($this->feedName) { + $specific = $this->feedName; + } + + break; + case 'From search results': + $specific = $this->getInput('s'); + break; + default: + return parent::getName(); + } + + return $specific . ' : Dailymotion'; + } + + public function getURI() + { + $uri = self::URI; + switch ($this->queriedContext) { + case 'By username': + $uri .= 'user/' . urlencode($this->getInput('u')); + break; + case 'By playlist id': + $uri .= 'playlist/' . urlencode(strtok($this->getInput('p'), '_')); + break; + case 'From search results': + $uri .= 'search/' . urlencode($this->getInput('s')); + + if (!is_null($this->getInput('pa'))) { + $pa = $this->getInput('pa'); + + if ($this->getInput('pa') < 1) { + $pa = 1; + } + + $uri .= '/' . $pa; + } + break; + default: + return parent::getURI(); + } + return $uri; + } + + private function getMetadata($id) + { + $metadata = []; + + $html = getSimpleHTMLDOM(self::URI . 'video/' . $id); + + if (!$html) { + return $metadata; + } + + $metadata['title'] = $html->find('meta[property=og:title]', 0)->getAttribute('content'); + $metadata['timestamp'] = strtotime( + $html->find('meta[property=video:release_date]', 0)->getAttribute('content') + ); + $metadata['thumbnailUri'] = $html->find('meta[property=og:image]', 0)->getAttribute('content'); + $metadata['uri'] = $html->find('meta[property=og:url]', 0)->getAttribute('content'); + return $metadata; + } + + private function getPlaylistTitle($id) + { + $title = ''; + + $url = self::URI . 'playlist/' . $id; + + $html = getSimpleHTMLDOM($url); + + $title = $html->find('meta[property=og:title]', 0)->getAttribute('content'); + return $title; + } + + private function getApiUrl() + { + switch ($this->queriedContext) { + case 'By username': + return $this->apiUrl . '/user/' . $this->getInput('u') + . '/videos?fields=' . urlencode($this->apiFields) . '&availability=1&sort=recent&limit=5'; + break; + case 'By playlist id': + return $this->apiUrl . '/playlist/' . $this->getInput('p') + . '/videos?fields=' . urlencode($this->apiFields) . '&limit=5'; + break; + } + } } diff --git a/bridges/DanbooruBridge.php b/bridges/DanbooruBridge.php index d6337f6b..3ca4476e 100644 --- a/bridges/DanbooruBridge.php +++ b/bridges/DanbooruBridge.php @@ -1,68 +1,73 @@ <?php -class DanbooruBridge extends BridgeAbstract { - const MAINTAINER = 'mitsukarenai, logmanoriginal'; - const NAME = 'Danbooru'; - const URI = 'http://donmai.us/'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns images from given page'; +class DanbooruBridge extends BridgeAbstract +{ + const MAINTAINER = 'mitsukarenai, logmanoriginal'; + const NAME = 'Danbooru'; + const URI = 'http://donmai.us/'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns images from given page'; - const PARAMETERS = array( - 'global' => array( - 'p' => array( - 'name' => 'page', - 'defaultValue' => 1, - 'type' => 'number' - ), - 't' => array( - 'type' => 'text', - 'name' => 'tags', - 'exampleValue' => 'cosplay', - ) - ), - 0 => array() - ); + const PARAMETERS = [ + 'global' => [ + 'p' => [ + 'name' => 'page', + 'defaultValue' => 1, + 'type' => 'number' + ], + 't' => [ + 'type' => 'text', + 'name' => 'tags', + 'exampleValue' => 'cosplay', + ] + ], + 0 => [] + ]; - const PATHTODATA = 'article'; - const IDATTRIBUTE = 'data-id'; - const TAGATTRIBUTE = 'alt'; + const PATHTODATA = 'article'; + const IDATTRIBUTE = 'data-id'; + const TAGATTRIBUTE = 'alt'; - protected function getFullURI(){ - return $this->getURI() - . 'posts?&page=' . $this->getInput('p') - . '&tags=' . urlencode($this->getInput('t')); - } + protected function getFullURI() + { + return $this->getURI() + . 'posts?&page=' . $this->getInput('p') + . '&tags=' . urlencode($this->getInput('t')); + } - protected function getTags($element){ - return $element->find('img', 0)->getAttribute(static::TAGATTRIBUTE); - } + protected function getTags($element) + { + return $element->find('img', 0)->getAttribute(static::TAGATTRIBUTE); + } - protected function getItemFromElement($element){ - // Fix links - defaultLinkTo($element, $this->getURI()); + protected function getItemFromElement($element) + { + // Fix links + defaultLinkTo($element, $this->getURI()); - $item = array(); - $item['uri'] = html_entity_decode($element->find('a', 0)->href); - $item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); - $item['timestamp'] = time(); - $thumbnailUri = $element->find('img', 0)->src; - $item['categories'] = array_filter(explode(' ', $this->getTags($element))); - $item['title'] = $this->getName() . ' | ' . $item['postid']; - $item['content'] = '<a href="' - . $item['uri'] - . '"><img src="' - . $thumbnailUri - . '" /></a><br>Tags: ' - . $this->getTags($element); + $item = []; + $item['uri'] = html_entity_decode($element->find('a', 0)->href); + $item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); + $item['timestamp'] = time(); + $thumbnailUri = $element->find('img', 0)->src; + $item['categories'] = array_filter(explode(' ', $this->getTags($element))); + $item['title'] = $this->getName() . ' | ' . $item['postid']; + $item['content'] = '<a href="' + . $item['uri'] + . '"><img src="' + . $thumbnailUri + . '" /></a><br>Tags: ' + . $this->getTags($element); - return $item; - } + return $item; + } - public function collectData(){ - $html = getSimpleHTMLDOMCached($this->getFullURI()); + public function collectData() + { + $html = getSimpleHTMLDOMCached($this->getFullURI()); - foreach($html->find(static::PATHTODATA) as $element) { - $this->items[] = $this->getItemFromElement($element); - } - } + foreach ($html->find(static::PATHTODATA) as $element) { + $this->items[] = $this->getItemFromElement($element); + } + } } diff --git a/bridges/DansTonChatBridge.php b/bridges/DansTonChatBridge.php index 1f1115f7..9712ec9d 100644 --- a/bridges/DansTonChatBridge.php +++ b/bridges/DansTonChatBridge.php @@ -1,27 +1,28 @@ <?php -class DansTonChatBridge extends BridgeAbstract { - const MAINTAINER = 'Astalaseven'; - const NAME = 'DansTonChat Bridge'; - const URI = 'https://danstonchat.com/'; - const CACHE_TIMEOUT = 21600; //6h - const DESCRIPTION = 'Returns latest quotes from DansTonChat.'; +class DansTonChatBridge extends BridgeAbstract +{ + const MAINTAINER = 'Astalaseven'; + const NAME = 'DansTonChat Bridge'; + const URI = 'https://danstonchat.com/'; + const CACHE_TIMEOUT = 21600; //6h + const DESCRIPTION = 'Returns latest quotes from DansTonChat.'; - public function collectData(){ + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI . 'latest.html'); - $html = getSimpleHTMLDOM(self::URI . 'latest.html'); - - foreach($html->find('div.item') as $element) { - $item = array(); - $item['uri'] = $element->find('a', 0)->href; - $titleContent = $element->find('h3 a', 0); - if($titleContent) { - $item['title'] = 'DansTonChat ' . html_entity_decode($titleContent->plaintext, ENT_QUOTES); - } else { - $item['title'] = 'DansTonChat'; - } - $item['content'] = $element->find('div.item-content a', 0)->innertext; - $this->items[] = $item; - } - } + foreach ($html->find('div.item') as $element) { + $item = []; + $item['uri'] = $element->find('a', 0)->href; + $titleContent = $element->find('h3 a', 0); + if ($titleContent) { + $item['title'] = 'DansTonChat ' . html_entity_decode($titleContent->plaintext, ENT_QUOTES); + } else { + $item['title'] = 'DansTonChat'; + } + $item['content'] = $element->find('div.item-content a', 0)->innertext; + $this->items[] = $item; + } + } } diff --git a/bridges/DarkReadingBridge.php b/bridges/DarkReadingBridge.php index 8fe242dd..6881c604 100644 --- a/bridges/DarkReadingBridge.php +++ b/bridges/DarkReadingBridge.php @@ -1,83 +1,90 @@ <?php -class DarkReadingBridge extends FeedExpander { - const MAINTAINER = 'ORelio'; - const NAME = 'Dark Reading Bridge'; - const URI = 'https://www.darkreading.com/'; - const DESCRIPTION = 'Returns the newest articles from Dark Reading'; - const PARAMETERS = array( array( - 'feed' => array( - 'name' => 'Feed', - 'type' => 'list', - 'values' => array( - 'All Dark Reading Stories' => '000_AllArticles', - 'Attacks/Breaches' => '644_Attacks/Breaches', - 'Application Security' => '645_Application%20Security', - 'Database Security' => '646_Database%20Security', - 'Cloud' => '647_Cloud', - 'Endpoint' => '648_Endpoint', - 'Authentication' => '649_Authentication', - 'Privacy' => '650_Privacy', - 'Mobile' => '651_Mobile', - 'Perimeter' => '652_Perimeter', - 'Risk' => '653_Risk', - 'Compliance' => '654_Compliance', - 'Operations' => '655_Operations', - 'Careers and People' => '656_Careers%20and%20People', - 'Identity and Access Management' => '657_Identity%20and%20Access%20Management', - 'Analytics' => '658_Analytics', - 'Threat Intelligence' => '659_Threat%20Intelligence', - 'Security Monitoring' => '660_Security%20Monitoring', - 'Vulnerabilities / Threats' => '661_Vulnerabilities%20/%20Threats', - 'Advanced Threats' => '662_Advanced%20Threats', - 'Insider Threats' => '663_Insider%20Threats', - 'Vulnerability Management' => '664_Vulnerability%20Management', - ) - ), - 'limit' => self::LIMIT, - )); +class DarkReadingBridge extends FeedExpander +{ + const MAINTAINER = 'ORelio'; + const NAME = 'Dark Reading Bridge'; + const URI = 'https://www.darkreading.com/'; + const DESCRIPTION = 'Returns the newest articles from Dark Reading'; - public function collectData(){ - $feed = $this->getInput('feed'); - $feed_splitted = explode('_', $feed); - $feed_id = $feed_splitted[0]; - $feed_name = $feed_splitted[1]; - if(empty($feed) || !ctype_digit($feed_id) || !preg_match('/[A-Za-z%20\/]/', $feed_name)) { - returnClientError('Invalid feed, please check the "feed" parameter.'); - } - $feed_url = $this->getURI() . 'rss_simple.asp'; - if ($feed_id != '000') { - $feed_url .= '?f_n=' . $feed_id . '&f_ln=' . $feed_name; - } - $limit = $this->getInput('limit') ?? 10; - $this->collectExpandableDatas($feed_url, $limit); - } + const PARAMETERS = [ [ + 'feed' => [ + 'name' => 'Feed', + 'type' => 'list', + 'values' => [ + 'All Dark Reading Stories' => '000_AllArticles', + 'Attacks/Breaches' => '644_Attacks/Breaches', + 'Application Security' => '645_Application%20Security', + 'Database Security' => '646_Database%20Security', + 'Cloud' => '647_Cloud', + 'Endpoint' => '648_Endpoint', + 'Authentication' => '649_Authentication', + 'Privacy' => '650_Privacy', + 'Mobile' => '651_Mobile', + 'Perimeter' => '652_Perimeter', + 'Risk' => '653_Risk', + 'Compliance' => '654_Compliance', + 'Operations' => '655_Operations', + 'Careers and People' => '656_Careers%20and%20People', + 'Identity and Access Management' => '657_Identity%20and%20Access%20Management', + 'Analytics' => '658_Analytics', + 'Threat Intelligence' => '659_Threat%20Intelligence', + 'Security Monitoring' => '660_Security%20Monitoring', + 'Vulnerabilities / Threats' => '661_Vulnerabilities%20/%20Threats', + 'Advanced Threats' => '662_Advanced%20Threats', + 'Insider Threats' => '663_Insider%20Threats', + 'Vulnerability Management' => '664_Vulnerability%20Management', + ] + ], + 'limit' => self::LIMIT, + ]]; - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $article = getSimpleHTMLDOMCached($item['uri']); - $item['content'] = $this->extractArticleContent($article); - $item['enclosures'] = array(); //remove author profile picture - $image = $article->find('meta[property="og:image"]', 0); - if (is_object($image)) { - $image = $image->content; - $item['enclosures'] = array($image); - } - return $item; - } + public function collectData() + { + $feed = $this->getInput('feed'); + $feed_splitted = explode('_', $feed); + $feed_id = $feed_splitted[0]; + $feed_name = $feed_splitted[1]; + if (empty($feed) || !ctype_digit($feed_id) || !preg_match('/[A-Za-z%20\/]/', $feed_name)) { + returnClientError('Invalid feed, please check the "feed" parameter.'); + } + $feed_url = $this->getURI() . 'rss_simple.asp'; + if ($feed_id != '000') { + $feed_url .= '?f_n=' . $feed_id . '&f_ln=' . $feed_name; + } + $limit = $this->getInput('limit') ?? 10; + $this->collectExpandableDatas($feed_url, $limit); + } - private function extractArticleContent($article){ - $content = $article->find('div.article-content', 0)->innertext; + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $article = getSimpleHTMLDOMCached($item['uri']); + $item['content'] = $this->extractArticleContent($article); + $item['enclosures'] = []; //remove author profile picture + $image = $article->find('meta[property="og:image"]', 0); + if (is_object($image)) { + $image = $image->content; + $item['enclosures'] = [$image]; + } + return $item; + } - foreach (array( - '<div class="divsplitter', - '<div style="float: left; margin-right: 2px;', - '<div class="more-insights', - '<div id="more-insights', - ) as $div_start) { - $content = stripRecursiveHTMLSection($content, 'div', $div_start); - } + private function extractArticleContent($article) + { + $content = $article->find('div.article-content', 0)->innertext; - return $content; - } + foreach ( + [ + '<div class="divsplitter', + '<div style="float: left; margin-right: 2px;', + '<div class="more-insights', + '<div id="more-insights', + ] as $div_start + ) { + $content = stripRecursiveHTMLSection($content, 'div', $div_start); + } + + return $content; + } } diff --git a/bridges/DauphineLibereBridge.php b/bridges/DauphineLibereBridge.php index 20c82070..82323036 100644 --- a/bridges/DauphineLibereBridge.php +++ b/bridges/DauphineLibereBridge.php @@ -1,57 +1,61 @@ <?php -class DauphineLibereBridge extends FeedExpander { - const MAINTAINER = 'qwertygc'; - const NAME = 'Dauphine Bridge'; - const URI = 'https://www.ledauphine.com/'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Returns the newest articles.'; +class DauphineLibereBridge extends FeedExpander +{ + const MAINTAINER = 'qwertygc'; + const NAME = 'Dauphine Bridge'; + const URI = 'https://www.ledauphine.com/'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Returns the newest articles.'; - const PARAMETERS = array( array( - 'u' => array( - 'name' => 'Catégorie de l\'article', - 'type' => 'list', - 'values' => array( - 'À la une' => '', - 'France Monde' => 'france-monde', - 'Faits Divers' => 'faits-divers', - 'Économie et Finance' => 'economie-et-finance', - 'Politique' => 'politique', - 'Sport' => 'sport', - 'Ain' => 'ain', - 'Alpes-de-Haute-Provence' => 'haute-provence', - 'Hautes-Alpes' => 'hautes-alpes', - 'Ardèche' => 'ardeche', - 'Drôme' => 'drome', - 'Isère Sud' => 'isere-sud', - 'Savoie' => 'savoie', - 'Haute-Savoie' => 'haute-savoie', - 'Vaucluse' => 'vaucluse' - ) - ) - )); + const PARAMETERS = [ [ + 'u' => [ + 'name' => 'Catégorie de l\'article', + 'type' => 'list', + 'values' => [ + 'À la une' => '', + 'France Monde' => 'france-monde', + 'Faits Divers' => 'faits-divers', + 'Économie et Finance' => 'economie-et-finance', + 'Politique' => 'politique', + 'Sport' => 'sport', + 'Ain' => 'ain', + 'Alpes-de-Haute-Provence' => 'haute-provence', + 'Hautes-Alpes' => 'hautes-alpes', + 'Ardèche' => 'ardeche', + 'Drôme' => 'drome', + 'Isère Sud' => 'isere-sud', + 'Savoie' => 'savoie', + 'Haute-Savoie' => 'haute-savoie', + 'Vaucluse' => 'vaucluse' + ] + ] + ]]; - public function collectData(){ - $url = self::URI . 'rss'; + public function collectData() + { + $url = self::URI . 'rss'; - if(empty($this->getInput('u'))) { - $url = self::URI . $this->getInput('u') . '/rss'; - } + if (empty($this->getInput('u'))) { + $url = self::URI . $this->getInput('u') . '/rss'; + } - $this->collectExpandableDatas($url, 10); - } + $this->collectExpandableDatas($url, 10); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $item['content'] = $this->extractContent($item['uri']); - return $item; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $item['content'] = $this->extractContent($item['uri']); + return $item; + } - private function extractContent($url){ - $html2 = getSimpleHTMLDOMCached($url); - foreach ($html2->find('.noprint, link, script, iframe, .shareTool, .contentInfo') as $remove) { - $remove->outertext = ''; - } - return $html2->find('div.content', 0)->innertext; - } + private function extractContent($url) + { + $html2 = getSimpleHTMLDOMCached($url); + foreach ($html2->find('.noprint, link, script, iframe, .shareTool, .contentInfo') as $remove) { + $remove->outertext = ''; + } + return $html2->find('div.content', 0)->innertext; + } } diff --git a/bridges/DavesTrailerPageBridge.php b/bridges/DavesTrailerPageBridge.php index 88e0bbb3..965f7e59 100644 --- a/bridges/DavesTrailerPageBridge.php +++ b/bridges/DavesTrailerPageBridge.php @@ -1,37 +1,40 @@ <?php -class DavesTrailerPageBridge extends BridgeAbstract { - const MAINTAINER = 'johnnygroovy'; - const NAME = 'Daves Trailer Page Bridge'; - const URI = 'https://www.davestrailerpage.co.uk/'; - const DESCRIPTION = 'Last trailers in HD thanks to Dave.'; - public function collectData(){ - $html = getSimpleHTMLDOM(static::URI) - or returnClientError('No results for this query.'); +class DavesTrailerPageBridge extends BridgeAbstract +{ + const MAINTAINER = 'johnnygroovy'; + const NAME = 'Daves Trailer Page Bridge'; + const URI = 'https://www.davestrailerpage.co.uk/'; + const DESCRIPTION = 'Last trailers in HD thanks to Dave.'; - $curr_date = null; - foreach ($html->find('tr') as $tr) { - // If it's a date row, update the current date - if ($tr->align == 'center') { - $curr_date = $tr->plaintext; - continue; - } + public function collectData() + { + $html = getSimpleHTMLDOM(static::URI) + or returnClientError('No results for this query.'); - $item = array(); + $curr_date = null; + foreach ($html->find('tr') as $tr) { + // If it's a date row, update the current date + if ($tr->align == 'center') { + $curr_date = $tr->plaintext; + continue; + } - // title - $item['title'] = $tr->find('td', 0)->find('b', 0)->plaintext; + $item = []; - // content - $item['content'] = $tr->find('ul', 1); + // title + $item['title'] = $tr->find('td', 0)->find('b', 0)->plaintext; - // uri - $item['uri'] = $tr->find('a', 3)->getAttribute('href'); + // content + $item['content'] = $tr->find('ul', 1); - // date: parsed by FeedItem using strtotime - $item['timestamp'] = $curr_date; + // uri + $item['uri'] = $tr->find('a', 3)->getAttribute('href'); - $this->items[] = $item; - } - } + // date: parsed by FeedItem using strtotime + $item['timestamp'] = $curr_date; + + $this->items[] = $item; + } + } } diff --git a/bridges/DealabsBridge.php b/bridges/DealabsBridge.php index c2d8c9f3..6e5ba9e3 100644 --- a/bridges/DealabsBridge.php +++ b/bridges/DealabsBridge.php @@ -1,1965 +1,1963 @@ <?php -class DealabsBridge extends PepperBridgeAbstract { - const NAME = 'Dealabs Bridge'; - const URI = 'https://www.dealabs.com/'; - const DESCRIPTION = 'Affiche les Deals de Dealabs'; - const MAINTAINER = 'sysadminstory'; - const PARAMETERS = array( - 'Recherche par Mot(s) clé(s)' => array ( - 'q' => array( - 'name' => 'Mot(s) clé(s)', - 'type' => 'text', - 'exampleValue' => 'lampe', - 'required' => true - ), - 'hide_expired' => array( - 'name' => 'Masquer les éléments expirés', - 'type' => 'checkbox', - ), - 'hide_local' => array( - 'name' => 'Masquer les deals locaux', - 'type' => 'checkbox', - 'title' => 'Masquer les deals en magasins physiques', - ), - 'priceFrom' => array( - 'name' => 'Prix minimum', - 'type' => 'text', - 'title' => 'Prix mnimum en euros', - 'required' => false - ), - 'priceTo' => array( - 'name' => 'Prix maximum', - 'type' => 'text', - 'title' => 'Prix maximum en euros', - 'required' => false - ), - ), +class DealabsBridge extends PepperBridgeAbstract +{ + const NAME = 'Dealabs Bridge'; + const URI = 'https://www.dealabs.com/'; + const DESCRIPTION = 'Affiche les Deals de Dealabs'; + const MAINTAINER = 'sysadminstory'; + const PARAMETERS = [ + 'Recherche par Mot(s) clé(s)' => [ + 'q' => [ + 'name' => 'Mot(s) clé(s)', + 'type' => 'text', + 'exampleValue' => 'lampe', + 'required' => true + ], + 'hide_expired' => [ + 'name' => 'Masquer les éléments expirés', + 'type' => 'checkbox', + ], + 'hide_local' => [ + 'name' => 'Masquer les deals locaux', + 'type' => 'checkbox', + 'title' => 'Masquer les deals en magasins physiques', + ], + 'priceFrom' => [ + 'name' => 'Prix minimum', + 'type' => 'text', + 'title' => 'Prix mnimum en euros', + 'required' => false + ], + 'priceTo' => [ + 'name' => 'Prix maximum', + 'type' => 'text', + 'title' => 'Prix maximum en euros', + 'required' => false + ], + ], - 'Deals par groupe' => array( - 'group' => array( - 'name' => 'Groupe', - 'type' => 'list', - 'title' => 'Groupe dont il faut afficher les deals', - 'values' => array( - 'Abattants WC' => 'abattants-wc', - 'Abonnement PlayStation Plus' => 'playstation-plus', - 'Abonnements cinéma' => 'abonnements-cinema', - 'Abonnements de train' => 'abonnements-de-train', - 'Abonnements internet' => 'abonnements-internet', - 'Abonnements presse' => 'abonnements-presse', - 'Accessoires aquarium' => 'accessoires-aquarium', - 'Accessoires auto' => 'auto', - 'Accessoires électroniques' => 'accessoires-gadgets', - 'Accessoires gamers PC' => 'accessoires-gamers-pc', - 'Accessoires gaming' => 'accessoires-gaming', - 'Accessoires iPhone' => 'accessoires-iphone', - 'Accessoires mode' => 'accessoires-mode', - 'Accessoires moto' => 'moto', - 'Accessoires Nintendo' => 'accessoires-nintendo', - 'Accessoires PC portables' => 'accessoires-pc-portables', - 'Accessoires photo' => 'accessoires-photo', - 'Accessoires PlayStation' => 'accessoires-playstation', - 'Accessoires pour barbecue' => 'accessoires-barbecue', - 'Accessoires studio photo' => 'accessoires-studio-photo', - 'Accessoires téléphonie' => 'accessoires-telephonie', - 'Accessoires TV' => 'accessoires-tv', - 'Accessoires vélo' => 'accessoires-velo', - 'Accessoires Xbox' => 'accessoires-xbox', - 'Acer' => 'acer', - 'Acer Predator' => 'acer-predator', - 'Achats / Ventes' => 'achats-ventes-echanges-estimations-dons', - 'Achats à l'étranger' => 'limport-sites-avis-questions-langues', - 'Adaptateurs' => 'adaptateurs', - 'Adhérents Fnac' => 'adherents-fnac', - 'Adhésions & Souscriptions' => 'adhesions-souscriptions-abonnements', - 'adidas' => 'adidas', - 'Adidas Gazelle' => 'adidas-gazelle', - 'adidas Stan Smith' => 'adidas-stan-smith', - 'adidas Superstar' => 'adidas-superstar', - 'adidas Ultraboost' => 'adidas-ultraboost', - 'adidas Yung-1' => 'adidas-yung-1', - 'adidas ZX Flux' => 'adidas-zx-flux', - 'Adoucissant' => 'adoucissant', - 'Agendas' => 'agendas', - 'Age of Empires' => 'age-of-empires', - 'Age of Empires: Definitive Edition' => 'age-of-empires-definitive-edition', - 'Alarmes' => 'alarmes', - 'Albums photo' => 'albums-photo', - 'Alcools' => 'alcools', - 'Alcools forts' => 'alcools-forts', - 'Alimentation' => 'epicerie', - 'Alimentation bébés' => 'alimentation-bebes', - 'Alimentation PC' => 'alimentation-pc', - 'Alimentation sportifs' => 'alimentation-sportifs', - 'Amazfit Bip' => 'xiaomi-amazfit-bip', - 'Amazon Echo' => 'amazon-echo', - 'Amazon Echo Dot' => 'amazon-echo-dot', - 'Amazon Echo Plus' => 'amazon-echo-plus', - 'Amazon Echo Show' => 'amazon-echo-show', - 'Amazon Echo Show 5' => 'amazon-echo-show-5', - 'Amazon Echo Spot' => 'amazon-echo-spot', - 'Amazon Fire TV' => 'amazon-fire-tv', - 'Amazon Kindle' => 'amazon-kindle', - 'Amazon Prime' => 'amazon-prime', - 'AMD Radeon' => 'amd-radeon', - 'AMD Ryzen' => 'amd-ryzen', - 'AMD Ryzen 5 5600X' => 'amd-ryzen-5-5600x', - 'AMD Ryzen 7 5800X' => 'amd-ryzen-7-5800x', - 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x', - 'AMD Ryzen 9 5950X' => 'amd-ryzen-9-5950x', - 'AMD Vega' => 'amd-vega', - 'amiibo' => 'amiibo', - 'Amplis (guitare/basse)' => 'amplis-guitare-basse', - 'Amplis audio' => 'amplis', - 'Ampoules' => 'ampoules', - 'Ampoules à LED' => 'ampoules-a-led', - 'Angleterre' => 'angleterre', - 'Animal Crossing' => 'animal-crossing', - 'Animal Crossing: New Horizons' => 'animal-crossing-new-horizons', - 'Animaux' => 'animaux', - 'Anker' => 'anker', - 'Anno 1800' => 'anno-1800', - 'Annonces officielles' => 'annonces-officielles', - 'Anthem' => 'anthem', - 'Anti-nuisibles' => 'anti-nuisibles', - 'Anti-puces' => 'anti-puces', - 'Antivirus' => 'antivirus', - 'Antivols' => 'antivols', - 'Apex Legends' => 'apex-legends', - 'Appareils à raclette' => 'appareils-raclette', - 'Appareils de musculation' => 'appareils-de-musculation', - 'Appareils photo' => 'appareils-photo', - 'Appareils photo Canon' => 'appareils-photo-canon', - 'Appareils photo compacts' => 'appareils-photo-compacts', - 'Appareils photo instantanés' => 'appareils-photo-instantanes', - 'Appareils photo Nikon' => 'appareils-photo-nikon', - 'Appareils photo Olympus' => 'appareils-photo-olympus', - 'Appareils photo Panasonic' => 'appareils-photo-panasonic', - 'Appareils photo Sony' => 'appareils-photo-sony', - 'Apple' => 'apple', - 'Apple AirPods' => 'apple-airpods', - 'Apple AirPods 2' => 'apple-airpods-2', - 'Apple AirPods Max' => 'apple-airpods-max', - 'Apple AirPods Pro' => 'apple-airpods-pro', - 'Apple HomePod' => 'apple-homepod', - 'Apple HomePod Mini' => 'apple-homepod-mini', - 'Apple TV' => 'apple-tv', - 'Apple TV+' => 'apple-tv-plus', - 'Apple Watch' => 'apple-watch', - 'Apple Watch 3' => 'apple-watch-3', - 'Apple Watch 4' => 'apple-watch-4', - 'Apple Watch 5' => 'apple-watch-5', - 'Apple Watch 6' => 'apple-watch-6', - 'Apple Watch SE' => 'apple-watch-se', - 'Applications' => 'applications', - 'Applications Android' => 'applications-android', - 'Applications iOS' => 'applications-ios', - 'Appliques murales' => 'appliques-murales', - 'Applis & logiciels' => 'applis-logiciels', - 'Après-shampooings' => 'apres-shampooings', - 'Aquariums' => 'aquariums', - 'Arbres à chat' => 'arbres-a-chat', - 'Arduino' => 'arduino', - 'Armoires & placards' => 'armoires-et-placards', - 'Articles de cuisine et d'entretien' => 'articles-de-cuisine', - 'Arts culinaires' => 'arts-culinaires', - 'Arts de la table' => 'arts-de-la-table', - 'ASICS' => 'asics', - 'Asmodée' => 'asmodee', - 'Aspirateurs' => 'aspirateurs', - 'Aspirateurs balais' => 'aspirateurs-balais', - 'Aspirateurs Dreame' => 'aspirateurs-xiaomi', - 'Aspirateurs Dyson' => 'aspirateurs-dyson', - 'Aspirateurs robot' => 'aspirateurs-robot', - 'Aspirateurs Rowenta' => 'apsirateurs-rowenta', - 'Aspirateurs sans sac' => 'aspirateurs-sans-sac', - 'Assassin's Creed' => 'assassin-s-creed', - 'Assassin's Creed: Unity' => 'assassins-creed-unity', - 'Assassin's Creed: Valhalla' => 'assassin-s-creed-valhalla', - 'Assassin's Creed Odyssey' => 'assassin-s-creed-odyssey', - 'Assassin's Creed Origins' => 'assassin-s-creed-origins', - 'Assurances' => 'assurances', - 'Astuces pour économiser' => 'vos-astuces-pour-faire-des-economies', - 'Asus' => 'asus', - 'Asus ROG' => 'asus-rog', - 'Asus ROG Phone' => 'asus-rog-phone', - 'Asus ROG Phone 2' => 'asus-rog-phone-2', - 'ASUS Transformer' => 'asus-transformer', - 'Asus VivoBook' => 'asus-vivobook', - 'Asus ZenBook' => 'asus-zenbook', - 'Asus ZenFone 2' => 'asus-zenfone-2', - 'Asus ZenFone 3' => 'asus-zenfone-3', - 'Asus ZenFone 4' => 'asus-zenfone-4', - 'Asus ZenFone 6' => 'asus-zenfone-6', - 'Asus ZenFone GO' => 'asus-zenfone-go', - 'Asus ZenFone Zoom' => 'asus-zenfone-zoom', - 'Audio & Hi-fi' => 'audio-et-hi-fi', - 'Aukey' => 'aukey', - 'Auto-Moto' => 'auto-moto', - 'Autoradios' => 'autoradios', - 'Azzaro Wanted' => 'azzaro-wanted', - 'Baby foot' => 'baby-foot', - 'BabyLiss' => 'babyliss', - 'Babyphones' => 'babyphones', - 'Badminton' => 'badminton', - 'Bagagerie' => 'bagagerie', - 'Baignoires pour bébé' => 'baignoires-pour-bebe', - 'Bains de bouche' => 'bains-de-bouche', - 'Balais & serpillères' => 'balais-et-serpilleres', - 'Balances connectées' => 'balances-connectees', - 'Balançoires' => 'balancoires', - 'Ballet & danse' => 'ballet-et-danse', - 'Ballons de football' => 'ballons-de-football', - 'Bandes dessinées' => 'bandes-dessinees', - 'Banques' => 'banques', - 'Barbecue' => 'barbecue', - 'Barbecue électrique' => 'barbecue-electrique', - 'Barbecue Weber' => 'barbecue-weber', - 'Barbie' => 'barbie', - 'Barres de son' => 'barres-de-son', - 'Barres de son Yamaha' => 'barres-de-son-yamaha', - 'Batman Arkham' => 'batman-arkham', - 'Batteries externes' => 'batteries-externes', - 'Batteries voiture' => 'batteries-voiture', - 'Batteurs' => 'batteurs-electriques', - 'Battlefield' => 'battlefield', - 'Battlefield 1' => 'battlefield-1', - 'Battlefield V' => 'battlefield-5', - 'Béaba' => 'beaba', - 'Beats by Dre' => 'beats-by-dre', - 'Beats Solo 3' => 'beats-solo-3', - 'Beats Studio 3' => 'beats-studio-3', - 'Beauté' => 'beaute', - 'Bébés' => 'bebes-nouveaux-nes', - 'BenQ' => 'benq', - 'Be quiet!' => 'be-quiet', - 'Beyerdynamic MMX 300' => 'beyerdynamic-mmx-300', - 'Biberons' => 'biberons', - 'Bien-être & santé' => 'bien-etre-et-massages', - 'Bières' => 'bieres', - 'Bijoux' => 'bijoux', - 'Bikinis' => 'bikinis', - 'Bilans de santé & dépistages' => 'bilans-de-sante-et-depistages', - 'Billets de bus' => 'billets-de-bus', - 'Billets de train' => 'billets-de-train', - 'BioShock' => 'bioshock', - 'BioShock Infinite' => 'bioshock-infinite', - 'Bitdefender' => 'bitdefender', - 'Blabla' => 'blabla-parlez-de-tout-et-de-rien', - 'Black & Decker' => 'black-decker', - 'Blackberry' => 'blackberry', - 'Black Desert Online' => 'black-desert-online', - 'Blédina' => 'bledina', - 'Blenders' => 'blenders', - 'Bleu de Chanel' => 'bleu-de-chanel', - 'Blousons de moto' => 'blousons-de-moto', - 'Blu-Ray' => 'blu-ray', - 'Bodys pour bébé' => 'bodys-pour-bebe', - 'Boissons' => 'boissons', - 'Boîtes à outils' => 'boites-a-outils', - 'Boîtiers PC' => 'boitiers-pc', - 'Boîtiers TV' => 'boitiers-tv', - 'Bonbons' => 'bonbons', - 'Bonnets' => 'bonnets', - 'Bonnets de bain' => 'bonnets-de-bain', - 'Borderlands' => 'borderlands', - 'Borderlands 3' => 'borderlands-3', - 'Bosch' => 'bosch', - 'Bose' => 'bose', - 'Bose Headphones 700' => 'bose-headphones-700', - 'Bose Home Speaker 500' => 'bose-home-speaker-500', - 'Bose QuietComfort' => 'bose-quietcomfort', - 'Bose QuietComfort 35 II' => 'bose-quietcomfort-35ii', - 'Bose SoundLink' => 'bose-soundlink', - 'Bose SoundTouch' => 'bose-soundtouch', - 'Bottes' => 'bottes', - 'Bottes de moto' => 'bottes-de-moto', - 'Bottes de neige' => 'bottes-neige', - 'Bottes de pluie' => 'bottes-pluie', - 'Bottes femme' => 'bottes-femme', - 'Bottes homme' => 'bottes-homme', - 'Bougies & bougeoirs' => 'bougies-et-bougeoirs', - 'Box beauté' => 'box-beaute', - 'Bracelet fitness' => 'bracelet-fitness', - 'Brandt' => 'brandt', - 'Braun Series 3' => 'braun-series-3', - 'Braun Series 5' => 'braun-series-5', - 'Braun Series 7' => 'braun-series-7', - 'Braun Series 9' => 'braun-series-9', - 'Braun Silk Épil' => 'braun-silk-epil', - 'Brita' => 'brita', - 'Brosses à dents' => 'brosses-a-dents', - 'Brosses à dents électriques' => 'brosses-a-dents-electriques', - 'Brosses à dents électriques Oral-B' => 'brosses-a-dents-electriques-oral-b', - 'Brosses pour animaux' => 'brosses-pour-animaux', - 'Cable management' => 'cable-management', - 'Câbles' => 'cables', - 'Câbles Ethernet' => 'cables-ethernet', - 'Câbles HDMI' => 'cables-hdmi', - 'Câbles Jack' => 'cables-jack', - 'Câbles USB' => 'cables-usb', - 'Cadeaux' => 'cadeaux', - 'Cadres' => 'cadres', - 'Cadres de vélo' => 'cadres-de-velo', - 'Café' => 'cafe', - 'Café en dosettes' => 'cafe-en-dosettes', - 'Café en grain' => 'cafe-en-grain', - 'Cafetières' => 'cafetieres', - 'Cafetières expresso' => 'cafetieres-expresso', - 'Cafetières filtre' => 'cafetieres-filtre', - 'Cafetières italiennes' => 'cafetieres-italiennes', - 'Cahiers' => 'cahiers', - 'Caissons de basses' => 'caissons-de-basses', - 'Calendrier de l'Avent Lego' => 'calendriers-avent-lego', - 'Calendriers' => 'calendriers', - 'Calendriers de l'Avent' => 'calendriers-avent', - 'Call of Duty' => 'call-of-duty', - 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war', - 'Call of Duty: Black Ops III' => 'call-of-duty-black-ops-3', - 'Call of Duty: Black Ops IIII' => 'call-of-duty-black-ops-4', - 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare', - 'Call of Duty: Modern Warfare' => 'call-of-duty-modern-warfare', - 'Call of Duty: WW2' => 'call-of-duty-ww2', - 'Calor' => 'calor', - 'Caméras' => 'cameras', - 'Caméras IP' => 'cameras-ip', - 'Caméras sportives' => 'cameras-sportives', - 'Camping' => 'camping', - 'Canapés' => 'canape', - 'Canon' => 'canon', - 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker', - 'Caravanes' => 'caravanes', - 'Carburant' => 'carburant', - 'Cartables' => 'cartables', - 'Cartes & programmes de fidélité' => 'cartes-et-programmes-de-fidelite', - 'Cartes bancaires' => 'cartes-bancaires', - 'Cartes de développement' => 'cartes-developpement', - 'Cartes graphiques' => 'cartes-graphiques', - 'Cartes mémoire' => 'cartes-memoire', - 'Cartes mères' => 'cartes-meres', - 'Cartes postales' => 'cartes-postales', - 'Cartes prépayées Playstation Store' => 'playstation-store', - 'Cartes SD' => 'cartes-sd', - 'Cartes son' => 'cartes-son', - 'Casio' => 'casio', - 'Casque sans fil Xbox' => 'casque-sans-fil-xbox', - 'Casques Apple' => 'casques-apple', - 'Casques à réduction de bruit' => 'casque-reduction-active-bruit', - 'Casques audio' => 'casques-audio', - 'Casques Bose' => 'casques-bose', - 'Casques de moto' => 'casques-de-moto', - 'Casques de vélo' => 'casques-de-velo', - 'Casques Jabra' => 'casques-jabra', - 'Casques Samsung' => 'casques-samsung', - 'Casques sans fil' => 'casques-sans-fil', - 'Casques Sennheiser' => 'casques-sennheiser', - 'Casques Sony' => 'casques-sony', - 'Casques VR' => 'vr', - 'Casquettes' => 'casquettes', - 'Casseroles' => 'casseroles', - 'Catit' => 'catit', - 'Caves à vin' => 'caves-a-vin', - 'CD & vinyles' => 'cd-vinyles', - 'CDAV' => 'cdav', - 'Ceintures' => 'ceintures', - 'Centrales vapeur' => 'centrales-vapeur', - 'Chaînes hi-fi' => 'chaines-hi-fi', - 'Chaises' => 'chaises', - 'Chaises hautes' => 'chaises-hautes', - 'Chambre' => 'chambre', - 'Champagne' => 'champagne', - 'Chapeaux' => 'chapeaux', - 'Chapeaux & casquettes' => 'chapeaux-casquettes', - 'Chargeurs' => 'chargeurs', - 'Chargeurs allume-cigare' => 'chargeurs-allume-cigare', - 'Chargeurs de piles' => 'chargeurs-de-piles', - 'Chargeurs sans fil' => 'chargeurs-sans-fil', - 'Chasse' => 'chasse', - 'Chatières' => 'chatieres', - 'Chats' => 'chats', - 'Chauffage' => 'chauffage', - 'Chaussettes & collants' => 'chaussettes-et-collants', - 'Chaussons' => 'chaussons', - 'Chaussures' => 'chaussures', - 'Chaussures adidas' => 'chaussures-adidas', - 'Chaussures de football' => 'chaussures-de-football', - 'Chaussures de randonnée' => 'chaussures-de-randonnee', - 'Chaussures de ski' => 'chaussures-de-ski', - 'Chaussures de ville' => 'chaussures-de-ville', - 'Chaussures New Balance' => 'chaussures-new-balance', - 'Chaussures Nike' => 'chaussures-nike', - 'Chaussures pour enfants' => 'chaussures-enfants', - 'Chaussures pour femme' => 'chaussures-femme', - 'Chaussures pour homme' => 'chaussures-homme', - 'Chaussures Puma' => 'chaussures-puma', - 'Chaussures Reebok' => 'chaussures-reebok', - 'Chaussures running' => 'chaussures-de-running', - 'Chelsea boots' => 'chelsea-boots', - 'Chemises' => 'chemises', - 'Chiens' => 'chiens', - 'Chocolat' => 'chocolat', - 'Chuck Taylor' => 'chuck-taylor', - 'Cinéma' => 'cinema', - 'Cire dépilatoire' => 'cire-depilatoire', - 'Cirque & arts de rue' => 'cirque-et-arts-de-rue', - 'Citytrips' => 'citytrips', - 'Civilization' => 'civilization', - 'Civilization VI' => 'civilization-vi', - 'CK One' => 'ck-one', - 'Clarks' => 'clarks', - 'Claviers' => 'claviers', - 'Claviers (musique)' => 'claviers-musique', - 'Claviers gamer' => 'claviers-gamer', - 'Claviers Logitech' => 'claviers-logitech', - 'Claviers mécaniques' => 'claviers-mecaniques', - 'Claviers sans fil' => 'claviers-sans-fil', - 'Clés USB' => 'cles-usb', - 'Climatisation' => 'climatisation', - 'Climatiseurs' => 'climatiseurs', - 'Cocottes' => 'cocottes', - 'Coffrets de livres' => 'coffrets-de-livres', - 'Coffrets DVD' => 'coffrets-dvd', - 'Coffrets maquillage' => 'coffrets-maquillage', - 'Colliers & laisses' => 'colliers-et-laisses', - 'Compléments alimentaires' => 'complements-alimentaires', - 'Composteurs' => 'composteurs', - 'Concerts' => 'concerts', - 'Concours' => 'concours', - 'Congélateurs' => 'congelateurs', - 'Connectiques' => 'connectiques', - 'Console Google Stadia' => 'google-stadia', - 'Console Nintendo Classic Mini' => 'nintendo-classic-mini', - 'Console Nintendo Classic Mini: SNES' => 'nintendo-classic-mini-snes', - 'Console Nintendo Switch' => 'nintendo-switch', - 'Console Nintendo Switch Lite' => 'nintendo-switch-lite', - 'Console PS4' => 'playstation-4', - 'Console PS4 Pro' => 'playstation-4-pro', - 'Console PS5' => 'playstation-5', - 'Consoles' => 'consoles', - 'Consoles & jeux vidéo' => 'consoles-jeux-video', - 'Console Sega Mega Drive Mini' => 'sega-mega-drive-mini', - 'Console Xbox One S' => 'xbox-one-s', - 'Console Xbox One X' => 'xbox-one-x', - 'Console Xbox Series S' => 'xbox-series-s', - 'Console Xbox Series X' => 'xbox-series-x', - 'Consommables imprimantes' => 'consommables-imprimantes', - 'Converse' => 'converse', - 'Coques iPhone' => 'coques-iphone', - 'Corsair Void PRO' => 'corsair-void-pro', - 'Costumes' => 'costumes', - 'Costumes & déguisements' => 'costumes-et-deguisements', - 'Couches' => 'couches', - 'Couettes' => 'couettes', - 'Coupes menstruelles' => 'coupes-menstruelles', - 'Cours & formations' => 'cours-et-formations', - 'Courses hippiques' => 'courses-hippiques', - 'Couteaux de cuisine' => 'couteaux-de-cuisine', - 'Couture' => 'couture', - 'Couverts' => 'couverts', - 'Couverts pour bébés' => 'couverts-pour-bebes', - 'Covoiturage' => 'covoiturage', - 'Crash Team Racing Nitro-Fueled' => 'crash-team-racing-nitro-fueled', - 'Cravates' => 'cravates', - 'Crédits' => 'credits', - 'Crèmes hydratantes' => 'cremes-hydratantes', - 'Crèmes solaires' => 'cremes-solaires', - 'Croisières' => 'croisieres', - 'Croquettes pour chat' => 'croquettes-pour-chat', - 'Croquettes pour chien' => 'croquettes-pour-chien', - 'Cuiseurs à riz' => 'cuiseur-riz', - 'Cuisinières' => 'cuisinieres', - 'Culottes menstruelles' => 'culottes-menstruelles', - 'Culture & divertissement' => 'culture-divertissement', - 'Cyberpunk 2077' => 'cyberpunk-2077', - 'Cyclisme' => 'cyclisme', - 'Cyclisme & sports urbains' => 'cyclisme-sports-urbains', - 'Darksiders' => 'darksiders', - 'Dashcams' => 'dashcams', - 'DDR3' => 'ddr3', - 'DDR4' => 'ddr4', - 'Dead Rising' => 'dead-rising', - 'Death Stranding' => 'death-stranding', - 'Décoration' => 'decoration', - 'Décorations de Noël' => 'decoration-noel', - 'Deebot' => 'ecovacs-deebot', - 'Deezer' => 'deezer', - 'Dell' => 'dell', - 'Dell XPS' => 'dell-xps', - 'Delsey' => 'delsey', - 'Demandes de deals' => 'les-demandes-de-deals', - 'Denon' => 'denon', - 'Dentifrices' => 'dentifrices', - 'Déodorants' => 'deodorants', - 'Désherbants' => 'desherbants', - 'Déshumidificateurs' => 'deshumidificateurs', - 'Désinfectant' => 'desinfectants', - 'Désodorisants & parfums d'intérieur' => 'desodorisants-et-parfums-d-interieur', - 'Destiny' => 'destiny', - 'Destiny 2' => 'destiny-2', - 'Détecteurs de fumée' => 'detecteurs-de-fumee', - 'Detroit: Become Human' => 'detroit-become-human', - 'Deus Ex' => 'deus-ex', - 'Deus Ex: Mankind Divided' => 'deus-ex-mankind-divided', - 'Devil May Cry 5' => 'devil-may-cry-5', - 'Dishonored' => 'dishonored', - 'Dishonored 2' => 'dishonored-2', - 'Disney+' => 'disney-plus', - 'Disneyland Paris' => 'disneyland-paris', - 'Disques durs (internes)' => 'hdd', - 'Disques durs externes' => 'disques-durs-externes', - 'Divers' => 'divers', - 'DJI' => 'dji', - 'DJI Mavic Air 2' => 'dji-mavic-air-2', - 'DJI Mavic Mini' => 'dji-mavic-mini', - 'Dolce Gusto' => 'dolce-gusto', - 'Domotique' => 'smart-home', - 'Doom Eternal' => 'doom-eternal', - 'Dosettes Dolce Gusto' => 'dosettes-dolce-guste', - 'Dosettes Nespresso' => 'dosettes-nespresso', - 'Dosettes Senseo' => 'dosettes-senseo', - 'Dosettes Tassimo' => 'dosettes-tassimo', - 'Dr. Martens' => 'dr-martens', - 'Dragon Age' => 'dragon-age', - 'Dragon Ball' => 'dragon-ball', - 'Dragon Ball FighterZ' => 'dragon-ball-fighterz', - 'Dragon Ball Z: Kakarot' => 'dragon-ball-z-kakarot', - 'Dragon Quest' => 'dragon-quest', - 'Dragon Quest Builders' => 'dragon-quest-builders', - 'Dragon Quest Builders 2' => 'dragon-quest-builders-2', - 'Draisiennes' => 'draisiennes', - 'Draps & housses' => 'draps-et-housses', - 'Dreame V10' => 'xiaomi-dreame-v10', - 'Dreame V11' => 'xiaomi-dreame-v11', - 'Drones' => 'drones', - 'Durex' => 'durex', - 'DVD' => 'dvd', - 'Dying Light' => 'dying-light', - 'Dying Light 2' => 'dying-light-2', - 'Dyson' => 'dyson', - 'Dyson V10' => 'dyson-v10', - 'Dyson V11' => 'dyson-v11', - 'Eastpak' => 'eastpak', - 'Ebooks' => 'ebooks', - 'Écharpes & foulards' => 'echarpes-et-foulards', - 'Éclairage intelligent' => 'smart-light', - 'Écouteurs' => 'ecouteurs', - 'Écouteurs sans fil' => 'ecouteurs-sans-fil', - 'Écouteurs sport' => 'ecouteurs-sport', - 'Ecovacs' => 'ecovacs', - 'Ecovacs Deebot 900' => 'ecovacs-deebot-900', - 'Ecovacs Deebot OZMO 930' => 'ecovacs-deebot-ozmo-930', - 'Écrans' => 'ecrans', - 'Écrans 4K / UHD' => 'ecrans-4k-uhd', - 'Écrans 21" et moins' => 'ecrans-21-pouces-et-moins', - 'Écrans 24"' => 'ecrans-24-pouces', - 'Écrans 27"' => 'ecrans-27-pouces', - 'Écrans 29" et plus' => 'ecrans-29-pouces-et-plus', - 'Écrans Acer' => 'ecrans-acer', - 'Écrans Asus' => 'ecrans-asus', - 'Écrans BenQ' => 'ecrans-benq', - 'Écrans Dell' => 'ecrans-dell', - 'Écrans de projection' => 'ecrans-de-projection', - 'Écrans FreeSync' => 'ecrans-freesync', - 'Écrans gaming' => 'ecrans-gamer', - 'Écrans incurvés' => 'ecrans-incurves', - 'Écrans Philips' => 'ecrans-philips', - 'Écrans Samsung' => 'ecrans-samsung', - 'Électricité (matériel)' => 'electricite', - 'Electrolux' => 'electrolux', - 'Électroménager' => 'electromenager', - 'Embauchoirs' => 'embauchoirs', - 'Enceintes' => 'enceintes', - 'Enceintes Bluetooth' => 'enceintes-bluetooth', - 'Enceintes connectées' => 'enceintes-connectees', - 'Enceintes portables sans fil' => 'enceintes-portables-sans-fil', - 'Énergie' => 'energie', - 'Engrais' => 'engrais', - 'Épicerie & courses' => 'epicerie-courses-supermarches', - 'Épilateurs à lumière pulsée' => 'epilateurs-a-lumiere-pulsee', - 'Épilateurs électriques' => 'epilateurs-electriques', - 'Épilation' => 'epilation', - 'Équipement motard' => 'equipement-motard', - 'Équipement running' => 'equipement-running', - 'Équipement sportif' => 'equipement-sportif', - 'Érotisme' => 'erotisme', - 'Escarpins' => 'escarpins', - 'Événements sportifs' => 'evenements-sportifs', - 'Expositions' => 'expositions', - 'Extracteurs de jus' => 'extracteurs-de-jus', - 'F1 2017' => 'f1-2017', - 'F1 2019' => 'f1-2019', - 'Facom' => 'facom', - 'Fallout' => 'fallout', - 'Fallout 4' => 'fallout-4', - 'Fallout 76' => 'fallout-76', - 'Famille & enfants' => 'famille-enfants', - 'Far Cry' => 'far-cry', - 'Far Cry New Dawn' => 'far-cry-new-dawn', - 'Fards à paupières' => 'fards-a-paupieres', - 'Fast-foods' => 'fast-foods', - 'Fauteuils' => 'fauteuils', - 'Fauteuils gamer' => 'fauteuils-gaming', - 'Fe' => 'fe', - 'Fers à lisser / à friser' => 'fers-a-lisser-a-friser', - 'Fers à repasser' => 'fers-a-repasser', - 'Fers à souder' => 'fers-a-souder', - 'Festivals' => 'festivals', - 'Feutres' => 'feutres', - 'FIFA' => 'fifa', - 'FIFA 17' => 'fifa-17', - 'FIFA 18' => 'fifa-18', - 'FIFA 19' => 'fifa-19', - 'FIFA 20' => 'fifa-20', - 'FIFA 21' => 'fifa-21', - 'Figurines' => 'figurines', - 'Films & Séries' => 'films', - 'Final Fantasy' => 'final-fantasy', - 'Final Fantasy XII' => 'final-fantasy-xii', - 'Finances & Assurances' => 'finances-assurances', - 'fitbit' => 'fitbit', - 'Fitness & yoga' => 'fitness-yoga', - 'Flash' => 'flash', - 'Fluval' => 'fluval', - 'Foires & salons' => 'foires-et-salons', - 'Fonds de teint' => 'fonds-de-teint', - 'Football' => 'football', - 'Forfaits de ski' => 'forfaits-ski', - 'Forfaits mobiles' => 'forfaits-mobiles', - 'Forfaits mobiles et internet' => 'telecommunications', - 'For Honor' => 'for-honor', - 'Formations premiers secours' => 'formations-premiers-secours', - 'Formule 1' => 'formule-1', - 'Fortnite' => 'fortnite', - 'Fortnite: Pack Feu Obscur' => 'fortnite-pack-feu-obscur', - 'Forza' => 'forza', - 'Forza Horizon' => 'forza-horizon', - 'Forza Horizon 3' => 'forza-horizon-3', - 'Forza Horizon 4' => 'forza-horizon-4', - 'Forza Motorsport' => 'forza-motosport', - 'Forza Motorsport 7' => 'forza-motorsport-7', - 'Fossil' => 'fossil', - 'Fournitures scolaires' => 'fournitures-scolaires', - 'Fours' => 'fours', - 'Fours à poser' => 'fours-a-poser', - 'Fours encastrables' => 'fours-encastrables', - 'Friandises pour chat' => 'friandises-pour-chat', - 'Friandises pour chien' => 'friandises-pour-chien', - 'Friskies' => 'friskies', - 'Friteuses' => 'friteuses', - 'Friteuses sans huile' => 'friteuses-sans-huile', - 'Fruits & légumes' => 'fruits-et-legumes', - 'Fujifilm' => 'fujifilm', - 'Funko Pop' => 'funko-pop', - 'FURminator' => 'furminator', - 'Futuroscope' => 'futuroscope', - 'Gamelles' => 'gamelles', - 'Game of Thrones' => 'game-of-thrones', - 'Gaming' => 'le-laboratoire-des-gamers', - 'Gants' => 'gants', - 'Gants moto' => 'gants-moto', - 'Garmin' => 'garmin', - 'Garmin Fenix' => 'garmin-fenix', - 'Garmin Forerunner' => 'garmin-forerunner', - 'Garmin Vivoactive' => 'garmin-vivoactive', - 'Garmin Vivomove' => 'garmin-vivomove', - 'Gâteaux & biscuits' => 'gateaux-et-biscuits', - 'Gears 5' => 'gears-5', - 'Gel hydroalcoolique' => 'gel-hydroalcoolique', - 'Gels douche' => 'gels-douche', - 'Geox' => 'geox', - 'Ghost of Tsushima' => 'ghost-of-tsushima', - 'Gigoteuses' => 'gigoteuses', - 'Gillette Fusion' => 'gillette-fusion', - 'Gillette Mach3' => 'gillette-mach3', - 'Glaces' => 'glaces', - 'Glacières' => 'glacieres', - 'Glisse urbaine' => 'glisse-urbaine', - 'God of War' => 'god-of-war', - 'Google Chromecast' => 'google-chromecast', - 'Google Home' => 'google-home', - 'Google Home Max' => 'google-home-max', - 'Google Home Mini' => 'google-home-mini', - 'Google Nest Hub' => 'google-nest-hub', - 'Google Nest Mini' => 'google-nest-mini', - 'Google Pixel' => 'google-pixel', - 'Google Pixel 2' => 'google-pixel-2', - 'Google Pixel 2 XL' => 'google-pixel-2-xl', - 'Google Pixel 3' => 'google-pixel-3', - 'Google Pixel 3 XL' => 'google-pixel-3-xl', - 'Google Pixel 3a' => 'google-pixel-3a', - 'Google Pixel 4' => 'google-pixel-4', - 'Google Pixel 4 XL' => 'google-pixel-4xl', - 'Google Pixel 4a' => 'google-pixel-4a', - 'Google Pixel 5' => 'google-pixel-5', - 'Google Pixel XL' => 'google-pixel-xl', - 'GoPro' => 'gopro-hero', - 'GoPro Hero 9' => 'gopro-hero-9', - 'Gran Turismo' => 'gran-turismo', - 'Grille-pain' => 'grille-pain', - 'Grossesse & maternité' => 'grossesse-maternite', - 'GTA' => 'gta', - 'GTA V' => 'gta-v', - 'GTX 1060' => 'nvidia-geforce-gtx-1060', - 'GTX 1070' => 'nvidia-geforce-gtx-1070', - 'GTX 1080' => 'nvidia-geforce-gtx-1080', - 'GTX 1080 Ti' => 'nvidia-geforce-gtx-1080-ti', - 'GTX 1650' => 'gtx-1650', - 'GTX 1660' => 'gtx-1660', - 'GTX 1660 Ti' => 'gtx-1660-ti', - 'Guerlain La Petite Robe Noire' => 'guerlain-petite-robe-noire', - 'Guirlandes lumineuses' => 'guirlandes-lumineuses', - 'Guitares' => 'guitares', - 'Gyropodes' => 'gyropodes', - 'Half Life' => 'half-life', - 'Half Life 2' => 'half-life-2', - 'Half Life Alyx' => 'half-life-alyx', - 'Halloween' => 'halloween', - 'Haltères & poids' => 'halteres-et-poids', - 'Hama' => 'hama', - 'Hamacs' => 'hamacs', - 'Hand spinners' => 'hand-spinners', - 'Harnais pour chien' => 'harnais-pour-chien', - 'Harry Potter' => 'harry-potter', - 'Havaianas' => 'havaianas', - 'High-Tech' => 'high-tech', - 'High-tech & informatique' => 'le-laboratoire-high-tech-informatique', - 'Hisense' => 'hisense', - 'Home Cinéma' => 'home-cinema', - 'Honor' => 'honor', - 'Honor 6X' => 'honor-6x', - 'Honor 8' => 'honor-8', - 'Honor 8 Pro' => 'honor-8-pro', - 'Honor 8X' => 'honor-8x', - 'Honor 8X Max' => 'honor-8x-max', - 'Honor 9' => 'honor-9', - 'Honor 10' => 'honor-10', - 'Honor 20' => 'honor-20', - 'Honor 20 Lite' => 'honor-20-lite', - 'Honor 20 Pro' => 'honor-20-pro', - 'Honor Band 5' => 'honor-band-5', - 'Honor MagicBook' => 'honor-magicbook', - 'Honor MagicWatch 2' => 'honor-magicwatch-2', - 'Honor View 20' => 'honor-view-20', - 'Horizon Zero Dawn' => 'horizon-zero-dawn', - 'Hôtels & Hébergements' => 'hotels', - 'Hoverboards' => 'hoverboards', - 'HTC 10' => 'htc-10', - 'HTC Desire' => 'htc-desire', - 'HTC One M9' => 'htc-one-m9', - 'HTC U11' => 'htc-u11', - 'HTC U Play' => 'htc-u-play', - 'HTC U Ultra' => 'htc-u-ultra', - 'HTC Vive' => 'htc-vive', - 'Huawei' => 'huawei', - 'Huawei FreeBuds 3' => 'huawei-freebuds-3', - 'Huawei Mate 9' => 'huawei-mate-9', - 'Huawei Mate 10' => 'huawei-mate-10', - 'Huawei Mate 10 Pro' => 'huawei-mate-10-pro', - 'Huawei Mate 20' => 'huawei-mate-20', - 'Huawei Mate 20 Lite' => 'huawei-mate-20-lite', - 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro', - 'Huawei Mate 20 RS' => 'huawei-mate-20-rs', - 'Huawei Mate 30' => 'huawei-mate-30', - 'Huawei Mate 30 Lite' => 'huawei-mate-30-lite', - 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro', - 'Huawei P8 Lite' => 'huawei-p8-lite', - 'Huawei P9 Lite' => 'huawei-p9-lite', - 'Huawei P10' => 'huawei-p10', - 'Huawei P10 Lite' => 'huawei-p10-lite', - 'Huawei P10 Plus' => 'huawei-p10-plus', - 'Huawei P20' => 'huawei-p20', - 'Huawei P20 Lite' => 'huawei-p20-lite', - 'Huawei P20 Pro' => 'huawei-p20-pro', - 'Huawei P30' => 'huawei-p30', - 'Huawei P30 Lite' => 'huawei-p30-lite', - 'Huawei P30 Pro' => 'huawei-p30-pro', - 'Huawei P40' => 'huawei-p40', - 'Huawei P40 Lite' => 'huawei-p40-lite', - 'Huawei P40 Pro' => 'huawei-p40-pro', - 'Huawei Watch' => 'huawei-watch', - 'Huawei Watch 2' => 'huawei-watch-2', - 'Hubs' => 'hubs', - 'Hugo Boss Bottled' => 'hugo-boss-bottled', - 'Huile moteur' => 'huile-moteur', - 'Hygiène & soins' => 'hygiene-soins', - 'Hygiène de la maison' => 'hygiene-de-la-maison', - 'Hygiène des bébés' => 'hygiene-des-bebes', - 'Hygiène intime' => 'hygiene-intime', - 'iMac' => 'mac-de-bureau', - 'iMac 2021' => 'imac-2021', - 'Image, son, photo' => 'le-laboratoire-audiovisuel', - 'Impressions photo' => 'impressions-photo', - 'Imprimantes' => 'imprimantes', - 'Imprimantes 3D' => 'imprimantes-3d', - 'Imprimantes Brother' => 'imprimantes-brother', - 'Imprimantes Canon' => 'imprimantes-canon', - 'Imprimantes Epson' => 'imprimantes-epson', - 'Imprimantes HP' => 'imprimantes-hp', - 'Imprimantes laser' => 'imprimantes-laser', - 'Imprimantes multifonctions' => 'imprimantes-multifonctions', - 'Informatique' => 'informatique', - 'Instax Mini' => 'instax-mini', - 'Instruments de musique' => 'instruments-de-musique', - 'Intel i5' => 'intel-i5', - 'Intel i7' => 'intel-i7', - 'Intel i9' => 'intel-i9', - 'iPad' => 'apple-ipad', - 'iPad 2019' => 'ipad-2019', - 'iPad 2020' => 'ipad-2020', - 'iPad Air' => 'ipad-air', - 'iPad Air 2019' => 'ipad-air-2019', - 'iPad Air 2020' => 'ipad-air-2020', - 'iPad Mini' => 'apple-ipad-mini', - 'iPad Pro' => 'apple-ipad-pro', - 'iPad Pro 11' => 'ipad-pro-11', - 'iPad Pro 12.9' => 'ipad-pro-12-9', - 'iPad Pro 2020' => 'ipad-pro-2020', - 'iPhone' => 'apple-iphone', - 'iPhone 6' => 'apple-iphone-6', - 'iPhone 7' => 'apple-iphone-7', - 'iPhone 7 Plus' => 'apple-iphone-7-plus', - 'iPhone 8' => 'apple-iphone-8', - 'iPhone 8 Plus' => 'apple-iphone-8-plus', - 'iPhone 11' => 'iphone-11', - 'iPhone 11 Pro' => 'iphone-11-pro', - 'iPhone 11 Pro Max' => 'iphone-11-pro-max', - 'iPhone 12' => 'iphone-12', - 'iPhone 12 Mini' => 'iphone-12-mini', - 'iPhone 12 Pro' => 'iphone-12-pro', - 'iPhone 12 Pro Max' => 'iphone-12-pro-max', - 'iPhone SE' => 'apple-iphone-se', - 'iPhone X' => 'apple-iphone-x', - 'iPhone XR' => 'apple-iphone-xr', - 'iPhone XS' => 'apple-iphone-xs', - 'iPhone XS Max' => 'apple-iphone-xs-max', - 'iRobot Roomba' => 'irobot-roomba', - 'Isolation' => 'isolation', - 'Jabra Elite 75t' => 'jabra-elite-75t', - 'Jabra Elite 85h' => 'jabra-elite-85h', - 'Jabra Elite 85t' => 'jabra-elite-85t', - 'Jabra Elite Active 65t' => 'jabra-elite-active-65t', - 'Jacuzzis' => 'jacuzzis', - 'Jardin' => 'jardin', - 'Jardin & bricolage' => 'jardin-bricolage', - 'Jardinage' => 'entretien-du-jardin', - 'JBL' => 'jbl', - 'JBL Charge 4' => 'jbl-charge-4', - 'JBL Flip' => 'jbl-flip', - 'JBL GO' => 'jbl-go', - 'JBL Xtreme 2' => 'jbl-xtreme-2', - 'Jeans' => 'jeans', - 'Jets dentaires' => 'jets-dentaires', - 'Jeux & jouets' => 'jeux-jouets', - 'Jeux & sports de café' => 'jeux-sports-cafe-bar', - 'Jeux d'adresse' => 'jeux-adresse', - 'Jeux d'apprentissage' => 'jeux-d-apprentissage', - 'Jeux d'eau' => 'jeux-jouets-eau', - 'Jeux d'extérieur' => 'jeux-d-exterieur', - 'Jeux d'imitation' => 'jeux-d-imitation', - 'Jeux de cartes et de plateau' => 'jeux-cartes-plateau-societe', - 'Jeux de construction' => 'jeux-de-construction', - 'Jeux de hasard & paris' => 'jeux-et-paris', - 'Jeux de société' => 'jeux-de-societe', - 'Jeux Nintendo 3DS' => 'jeux-3ds', - 'Jeux Nintendo Switch' => 'jeux-nintendo-switch', - 'Jeux PC' => 'jeux-pc', - 'Jeux PC dématérialisés' => 'jeux-pc-dematerialises', - 'Jeux pour bébés' => 'jeux-pour-bebes', - 'Jeux PS4' => 'jeux-playstation-4', - 'Jeux PS4 dématérialisés' => 'jeux-ps4-dematerialises', - 'Jeux PS5' => 'jeux-playstation-5', - 'Jeux PS5 dématérialisés' => 'jeux-playstation-5-dematerialises', - 'Jeux PS Plus' => 'jeux-ps-plus', - 'Jeux vidéo' => 'jeux-video', - 'Jeux VR' => 'jeux-vr', - 'Jeux Wii U' => 'jeux-wii-u', - 'Jeux Xbox One' => 'jeux-xbox-one', - 'Jeux Xbox One dématérialisés' => 'jeux-xbox-dematerialises', - 'Jeux Xbox Series X' => 'jeux-xbox-series-x', - 'Jeux Xbox with Gold' => 'jeux-xbox-with-gold', - 'Jouets' => 'jouets', - 'Jouets pour chat' => 'jouets-pour-chat', - 'Jouets pour chien' => 'jouets-pour-chien', - 'Journaux numériques' => 'journaux-numeriques', - 'Journaux papier' => 'journaux-papier', - 'Joy-Con' => 'manettes-nintendo-switch-joy-con', - 'Jungle Speed' => 'jungle-speed', - 'Just Cause' => 'just-cause', - 'Just Cause 3' => 'just-cause-3', - 'Just Cause 4' => 'just-cause-4', - 'Kärcher' => 'karcher', - 'Kaspersky' => 'kaspersky', - 'Kinder' => 'kinder', - 'Kindle Oasis' => 'kindle-oasis', - 'Kindle Paperwhite' => 'kindle-paperwhite', - 'Kindle Voyage' => 'kindle-voyage', - 'Kingdom Hearts' => 'kingdom-hearts', - 'Kingdom Hearts 3' => 'kingdom-hearts-3', - 'Kingston HyperX Cloud II' => 'kingston-hyperx-cloud-2', - 'Kits premiers secours' => 'premiers-secours', - 'Kobo' => 'kobo', - 'Kobo Aura 2' => 'kobo-aura-2', - 'Kobo Aura H2o' => 'kobo-aura-h2o', - 'Kobo Aura One' => 'kobo-aura-one', - 'L'annale du destin' => 'l-annale-du-destin', - 'L'ombre de la guerre' => 'l-ombre-de-la-guerre', - 'L'ombre du Mordor' => 'l-ombre-du-mordor', - 'Lacoste' => 'lacoste', - 'Lampadaires' => 'lampadaires', - 'Lampes' => 'lampes', - 'Lampes de table' => 'lampes-de-table', - 'Lampes solaires' => 'lampes-solaires', - 'Lancôme La Vie est Belle' => 'lancome-la-vie-est-belle', - 'Lapeyre' => 'lapeyre', - 'La Terre du Milieu' => 'la-terre-du-milieu', - 'Lavage auto' => 'lavage-auto', - 'Lavazza' => 'lavazza', - 'Lave-linge' => 'lave-linge', - 'Lave-linge frontal' => 'lave-linge-frontal', - 'Lave-linge séchant' => 'lave-linge-sechant', - 'Lave-linge top' => 'lave-linge-top', - 'Lave-vaisselle' => 'lave-vaisselle', - 'Lay-Z-Spa' => 'lay-z-spa', - 'Leasing voiture' => 'leasing-voiture', - 'Le bâton de la vérité' => 'le-baton-de-la-verite', - 'Lecteurs Blu-Ray' => 'lecteurs-blu-ray', - 'Lecteurs CD' => 'lecteurs-cd', - 'Lecteurs DVD' => 'lecteurs-dvd', - 'Lego' => 'lego', - 'Lego Architecture' => 'lego-architecture', - 'Lego Batman' => 'lego-batman', - 'Lego City' => 'lego-city', - 'Lego Creator' => 'lego-creator', - 'Lego Dimensions' => 'lego-dimensions', - 'Lego Duplo' => 'lego-duplo', - 'Lego Friends' => 'lego-friends', - 'Lego Harry Potter' => 'lego-harry-potter', - 'Lego Ideas' => 'lego-ideas', - 'Lego Marvel' => 'lego-marvel', - 'Lego Nexo Knights' => 'lego-nexo-knights', - 'Lego Ninjago' => 'lego-ninjago', - 'Lego Star Wars' => 'lego-star-wars', - 'Lego Technic' => 'lego-technic', - 'Lenovo' => 'lenovo', - 'Lenovo IdeaPad' => 'lenovo-ideapad', - 'Lenovo K6 Note' => 'lenovo-k6-note', - 'Lenovo P8' => 'lenovo-p8', - 'Lenovo Tab 3' => 'lenovo-tab-3', - 'Lenovo Tab 4' => 'lenovo-tab-4', - 'Lenovo ThinkPad' => 'lenovo-thinkpad', - 'Lenovo Yoga' => 'lenovo-yoga', - 'Lenovo Yoga Tab 3' => 'lenovo-yoga-tab-3', - 'Lentilles de contact' => 'lentilles-de-contact', - 'Le Seigneur des anneaux' => 'le-seigneur-des-anneaux', - 'Les Sims' => 'les-sims', - 'Les Sims 4' => 'les-sims-4', - 'Lessive' => 'lessive', - 'Levi's' => 'levi-s', - 'LG' => 'lg', - 'LG G4' => 'lg-g4', - 'LG G5' => 'lg-g5', - 'LG G6' => 'lg-g6', - 'LG OLED TV' => 'lg-oled-tv', - 'LG Q6' => 'lg-q6', - 'LG Q8' => 'lg-q8', - 'Life is Strange' => 'life-is-strange', - 'Linge de maison' => 'linge-de-maison', - 'Lingerie' => 'lingerie', - 'Lingettes désinfectantes' => 'lingettes-desinfectantes', - 'Lingettes pour bébés' => 'lingettes-pour-bebes', - 'Liseuses' => 'liseuses', - 'Litière pour chat' => 'litiere-pour-chat', - 'Lits' => 'lits', - 'Lits pour bébé' => 'lits-pour-bebe', - 'Lits pour enfants' => 'lits-pour-enfants', - 'Little Nightmares' => 'little-nightmares', - 'Livraison de repas' => 'service-de-livraison-de-repas', - 'Livres & littérature' => 'livres-litterature', - 'Livres & Magazines' => 'livres', - 'Livres audio' => 'livres-audio', - 'Livres photo' => 'livres-photo', - 'Location de voiture' => 'location-de-voiture', - 'Logiciels' => 'logiciels', - 'Logiciels de sécurité' => 'logiciels-de-securite', - 'Logiciels Microsoft' => 'logiciels-microsoft', - 'Logitech' => 'logitech', - 'Logitech G502' => 'logitech-g502', - 'Logitech G703' => 'logitech-g703', - 'Logitech G Pro X' => 'logitech-g-pro-x', - 'Logitech Harmony' => 'logitech-harmony', - 'Logitech MX Master' => 'logitech-mx-master', - 'Logitech MX Master 2S' => 'logitech-mx-master-2s', - 'Loisirs créatifs' => 'loisirs-creatifs', - 'Lolita Lempicka' => 'lolita-lempicka-premier-parfum', - 'Loup-Garou' => 'loup-garou', - 'Lubrifiants' => 'lubrifiants', - 'Luges' => 'luges', - 'Luigi's Mansion 3' => 'luigi-mansion-3', - 'Luminaires' => 'luminaires', - 'Lunettes de natation' => 'lunettes-de-natation', - 'Lunettes de soleil' => 'lunettes-de-soleil', - 'M&M's' => 'metm-s', - 'MacBook' => 'macbook', - 'MacBook Air' => 'apple-macbook-air', - 'MacBook Pro' => 'apple-macbook-pro', - 'MacBook Pro 13' => 'macbook-pro-13', - 'MacBook Pro 15' => 'macbook-pro-15', - 'MacBook Pro 16' => 'macbook-pro-16', - 'Machines à café à dosettes' => 'machines-a-cafe-a-dosettes', - 'Machines à café en grain' => 'machines-a-cafe-en-grain', - 'Machines à coudre' => 'machines-a-coudre', - 'Machines à pain' => 'machines-a-pain', - 'Machines de sport' => 'machines-sport', - 'Machines Dolce Gusto' => 'machines-dolce-gusto', - 'Machines Nespresso' => 'machines-nespresso', - 'Machines Senseo' => 'machines-senseo', - 'Machines Tassimo' => 'machines-tassimo', - 'Mac mini' => 'mac-mini', - 'Madden NFL 20' => 'madden-nfl-20', - 'Magasins d'usine' => 'magasins-usine', - 'Magazines' => 'magazines', - 'Maillots de bain' => 'maillots-de-bain', - 'Maillots de football' => 'maillots-de-football', - 'Maison & Habitat' => 'maison-habitat', - 'Maisons de poupées' => 'maisons-poupees', - 'Makita' => 'makita', - 'Manettes' => 'manettes-accessoires-consoles', - 'Manettes DualSense' => 'manettes-playstation-5', - 'Manettes Nintendo Switch' => 'manettes-nintendo-switch', - 'Manettes Nintendo Switch Pro' => 'manettes-nintendo-switch-pro', - 'Manettes PlayStation 4' => 'manettes-playstation-4', - 'Manettes Xbox' => 'manettes-xbox', - 'Manettes Xbox One' => 'manettes-xbox-one', - 'Manettes Xbox One Elite' => 'manettes-xbox-one-elite', - 'Manettes Xbox Series X' => 'manettes-xbox-series-x', - 'Manix' => 'manix', - 'Manteaux' => 'manteaux', - 'Maquillage' => 'maquillage', - 'Marchands et leurs offres' => 'vos-avisdemandes-sur-les-marchands-et-leurs-offres', - 'Mario & Sonic aux Jeux Olympiques de Tokyo 2020' => 'mario-sonic-jeux-olympiques-tokyo-2020', - 'Mario Kart' => 'mario-kart', - 'Marques' => 'marques', - 'Marteaux & maillets' => 'marteaux-et-maillets', - 'Marvel's Avengers' => 'marvels-avengers', - 'Mascara' => 'mascara', - 'Masques cheveux' => 'masques-cheveux', - 'Masques de protection' => 'masques-de-protection-respiratoire', - 'Masques de ski' => 'masques-de-ski', - 'Mass Effect' => 'mass-effect', - 'Mass Effect: Andromeda' => 'mass-effect-andromeda', - 'Matchs de football' => 'matchs-de-football', - 'Matelas' => 'matelas', - 'Matelas gonflables' => 'matelas-gonflables', - 'Matériaux de construction' => 'materiaux-de-construction', - 'Matériel de ski' => 'materiel-de-ski', - 'Medion' => 'medion', - 'Metro' => 'metro', - 'Metro 2033' => 'metro-2033', - 'Metro Exodus' => 'metro-exodus', - 'Meubles pour aquarium' => 'meubles-pour-aquarium', - 'Meubles pour chat' => 'meubles-pour-chat', - 'Meubles salle de bain' => 'salle-de-bain', - 'Micro-casques gaming' => 'micro-casques-gaming', - 'Micro-ondes' => 'micro-ondes', - 'Microphones' => 'microphones', - 'Micro SD' => 'micro-sd', - 'Microsoft Flight Simulator' => 'microsoft-flight-simulator', - 'Microsoft Office' => 'microsoft-office', - 'Microsoft Surface Book' => 'microsoft-surface-book', - 'Microsoft Surface Pro 6' => 'microsoft-surface-pro-6', - 'Microsoft Surface Pro 7' => 'microsoft-surface-pro-7', - 'Miele' => 'miele', - 'Minecraft' => 'minecraft', - 'Mini PC' => 'mini-pc', - 'Mini réfrigérateurs' => 'mini-refrigerateurs', - 'Miroirs' => 'miroirs', - 'Mixeurs & Blenders' => 'mixeurs-blenders', - 'Mixeurs plongeants' => 'mixeur-plongeant', - 'Mobilier' => 'mobilier', - 'Mobilier de bureau' => 'fournitures-de-bureau', - 'Mobilier de jardin' => 'mobilier-jardin', - 'Mobilier de salon' => 'mobilier-salon', - 'Mobvoi Ticwatch' => 'mobvoi-ticwatch', - 'Mode' => 'mode', - 'Mode & accessoires' => 'mode-accessoires', - 'Mode & beauté' => 'le-laboratoire-de-la-mode-beaute', - 'Mode enfants' => 'mode-enfants', - 'Mode femme' => 'mode-femme', - 'Mode homme' => 'mode-homme', - 'Modélisme' => 'modelisme', - 'Monopoly' => 'monopoly', - 'Montage PC' => 'montage-pc', - 'Montre connectée Amazfit' => 'montres-connectees-amazfit', - 'Montre connectée Garmin' => 'montres-connectees-garmin', - 'Montre connectée Honor' => 'montres-connectees-honor', - 'Montre connectée Samsung' => 'smartwatch-samsung', - 'Montres' => 'montres', - 'Montres connectées' => 'smartwatch', - 'Mortal Kombat' => 'mortal-kombat', - 'Mortal Kombat 11' => 'mortal-kombat-11', - 'Moto C Plus' => 'moto-c-plus', - 'Moto E4' => 'moto-e4', - 'Moto G5' => 'moto-g5', - 'Moto G5 Plus' => 'moto-g5-plus', - 'Moto G5S' => 'moto-g5s', - 'Moto G5S Plus' => 'moto-g5s-plus', - 'Moto G6' => 'moto-g6', - 'Moto G6 Play' => 'moto-g6-play', - 'Moto G6 Plus' => 'moto-g6-plus', - 'Moto G7 Play' => 'moto-g7-play', - 'Moto G7 Plus' => 'moto-g7-plus', - 'Moto G7 Power' => 'moto-g7-power', - 'Moto M' => 'moto-m', - 'Motorola' => 'motorola', - 'Moto Z2' => 'moto-z2', - 'Moto Z2 Force' => 'moto-z2-force', - 'Moto Z2 Play' => 'moto-z2-play', - 'Moto Z3' => 'moto-z3', - 'Moto Z3 Play' => 'moto-z3-play', - 'Moulinex' => 'moulinex', - 'Mousses à raser' => 'mousses-a-raser', - 'MSI' => 'msi', - 'Musées' => 'musees', - 'Musique' => 'musique', - 'NAS' => 'nas', - 'Natation' => 'natation', - 'Nature & sports d'hiver' => 'nature-sports-hiver', - 'Navigation' => 'navigation', - 'NBA 2K' => 'nba-2k', - 'NBA 2K20' => 'nba-2k20', - 'NERF' => 'nerf', - 'Nescafé' => 'nescafe', - 'Nespresso' => 'nespresso', - 'Nest Learning Thermostat' => 'nest-learning-thermostat', - 'Nest Protect' => 'nest-protect', - 'Netflix' => 'netflix', - 'Nettoyeurs haute-pression' => 'nettoyeurs-haute-pression', - 'Nettoyeurs haute pression Karcher' => 'nettoyeurs-haute-pression-karcher', - 'Nettoyeurs vapeur' => 'nettoyeurs-vapeur', - 'New Balance' => 'new-balance', - 'New Balance 574' => 'new-balance-574', - 'NHL 20' => 'nhl-20', - 'Nike' => 'nike', - 'Nike Air Force' => 'nike-air-force', - 'Nike Air Jordan' => 'nike-air-jordan', - 'Nike Air Max' => 'nike-air-max', - 'Nike Air Max 90' => 'nike-air-max-90', - 'Nike Air Max 200' => 'nike-air-max-200', - 'Nike Air Max 270' => 'nike-air-max-270', - 'Nike Air Max 720' => 'nike-air-max-720', - 'Nike Free' => 'nike-free', - 'Nike Huarache' => 'nike-huarache', - 'Nike Roshe Run' => 'nike-roshe-run', - 'Nikon' => 'nikon', - 'Nikon D3500' => 'nikon-d3500', - 'Ni no Kuni' => 'ni-no-kuni', - 'Ni No Kuni: Wrath of the White Witch' => 'ni-no-kuni-wrath-white-witch', - 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-2-revenant-kingdom', - 'Nintendo' => 'nintendo', - 'Nioh' => 'nioh', - 'Nivea' => 'nivea', - 'Nocciolata' => 'nocciolata', - 'Nokia' => 'nokia', - 'Nokia 5' => 'nokia-5', - 'Nokia 6' => 'nokia-6', - 'Nokia 8' => 'nokia-8', - 'Nokia 9 PureView' => 'nokia-9-pureview', - 'Nougats' => 'nougats', - 'Nourriture pour chat' => 'nourriture-pour-chat', - 'Nourriture pour chien' => 'nourriture-pour-chien', - 'Nourriture pour poissons' => 'nourriture-pour-poissons', - 'Nutella' => 'nutella', - 'Nvidia' => 'nvidia', - 'Nvidia GeForce' => 'nvidia-geforce', - 'Nvidia Shield' => 'nvidia-shield', - 'Objectifs' => 'objectifs', - 'Objets connectés' => 'objets-connectes', - 'Oculus Go' => 'oculus-go', - 'Oculus Rift' => 'oculus-rift', - 'Oiseaux' => 'oiseaux', - 'One Piece: Pirate Warriors' => 'one-piece-pirate-warriors', - 'OnePlus 5' => 'oneplus-5', - 'OnePlus 5T' => 'oneplus-5t', - 'OnePlus 6' => 'oneplus-6', - 'OnePlus 6T' => 'oneplus-6t', - 'OnePlus 7' => 'oneplus-7', - 'OnePlus 7 Pro' => 'oneplus-7-pro', - 'OnePlus 7T' => 'oneplus-7t', - 'OnePlus 7T Pro' => 'oneplus-7t-pro', - 'OnePlus 8' => 'oneplus-8', - 'OnePlus 8 Pro' => 'oneplus-8-pro', - 'OnePlus 8T' => 'oneplus-8t', - 'OnePlus 9' => 'oneplus-9', - 'OnePlus 9 Pro' => 'oneplus-9-pro', - 'OnePlus Nord' => 'oneplus-nord', - 'Onkyo' => 'onkyo', - 'Oppo Find X2 Lite' => 'oppo-find-x2-lite', - 'Oppo Find X2 Neo' => 'oppo-find-x2-neo', - 'Oppo Find X2 Pro' => 'oppo-find-x2-pro', - 'Oppo Reno' => 'oppo-reno', - 'Optique' => 'optique', - 'Oral-B' => 'oral-b', - 'Ordinateurs de bureau' => 'ordinateurs-de-bureau', - 'Ordinateurs tout-en-un' => 'pc-de-bureau-complets', - 'Oreillers' => 'oreillers', - 'Osram Smart+' => 'osram-smart-plus', - 'Outillage' => 'outillage', - 'Outils à main' => 'outils-main', - 'Outils de jardinage' => 'outils-de-jardinage', - 'Outils électriques' => 'outils-electriques', - 'Overwatch' => 'overwatch', - 'Packs clavier-souris' => 'packs-clavier-souris', - 'Packs consoles' => 'packs-consoles', - 'Paco Rabanne Invictus' => 'paco-rabanne-invictus', - 'Paco Rabanne Lady Million' => 'paco-rabanne-lady-million', - 'Paco Rabanne One Million' => 'paco-rabanne-one-million', - 'Pain & pâtisseries' => 'pain-patisseries', - 'Pampers' => 'pampers', - 'Panasonic' => 'panasonic', - 'Panasonic Lumix' => 'panasonic-lumix', - 'Panier Plus' => 'panier-plus', - 'Pantalons' => 'pantalons', - 'Papeterie' => 'papeterie', - 'Papeterie et bureautique' => 'papeterie-bureautique', - 'Papier bureautique' => 'papier-bureautique', - 'Papier peint' => 'papier-peint', - 'Papier toilette' => 'papier-toilette', - 'Parapharmacie' => 'parapharmacie', - 'Parasols' => 'parasols', - 'Parc Astérix' => 'parc-asterix', - 'Parcs d'attraction' => 'parcs-d-attraction', - 'Parfums' => 'parfums', - 'Parfums femme' => 'parfums-femme', - 'Parfums homme' => 'parfums-homme', - 'Parkas' => 'parkas', - 'Parrot' => 'parrot', - 'Partitions' => 'partitions', - 'Pâtée pour chat' => 'patee-pour-chat', - 'Pâtée pour chien' => 'patee-pour-chien', - 'Pâtes à tartiner' => 'pates-tartiner', - 'Pâtisserie' => 'patisserie', - 'PC Barebones' => 'pc-barebones', - 'PC gamer fixe' => 'pc-gamer-complets', - 'PC gaming' => 'pc-gaming', - 'PC hybrides' => 'hybrides', - 'PC Microsoft Surface' => 'pc-microsoft-surface', - 'PC portables' => 'pc-portables', - 'PC portables Acer' => 'pc-portables-acer', - 'PC portables ASUS' => 'pc-portables-asus', - 'PC portables Dell' => 'pc-portables-dell', - 'PC portables gaming' => 'portables-gamer', - 'PC portables Honor' => 'pc-portables-honor', - 'PC portables HP' => 'pc-portables-hp', - 'PC portables Lenovo' => 'pc-portables-lenovo', - 'PC portables Lenovo Legion' => 'lenovo-legion', - 'PC portables Xiaomi' => 'pc-portables-xiaomi', - 'Pêche' => 'peche', - 'Peignes & brosses à cheveux' => 'peignes-et-brosses-a-cheveux', - 'Peignoirs' => 'peignoirs', - 'Peintures' => 'peintures', - 'Peluches' => 'peluches', - 'Perceuses' => 'perceuses', - 'Périphériques PC' => 'peripheriques-pc', - 'Persona 5' => 'persona-5', - 'Persona 5 Royal' => 'persona-5-royal', - 'PES' => 'pro-evolution-soccer', - 'Pèse-personnes' => 'pese-personnes', - 'Petites voitures' => 'petites-voitures', - 'Pharmacie & parapharmacie' => 'pharmacie-parapharmacie', - 'Philips' => 'philips', - 'Philips Hue' => 'philips-hue', - 'Philips Hue E14' => 'philips-hue-e14', - 'Philips Hue E27' => 'philips-hue-e27', - 'Philips Hue Go' => 'philips-hue-go', - 'Philips Hue GU10' => 'philips-hue-gu10', - 'Philips Hue LightStrip' => 'philips-hue-lightstrip', - 'Philips Hue Play HDMI Sync Box' => 'philips-hue-play-hdmi-sync-box', - 'Philips Lumea' => 'philips-lumea', - 'Philips OneBlade' => 'philips-one-blade', - 'Philips Sonicare' => 'philips-sonicare', - 'Photo' => 'photo', - 'Pièces auto' => 'pieces-auto', - 'Pièces moto' => 'pieces-moto', - 'Pièces vélo' => 'pieces-velo', - 'Piles' => 'piles', - 'Piles rechargeables' => 'piles-rechargeables', - 'Pinceaux maquillage' => 'pinceaux-maquillage', - 'Pinces' => 'pinces', - 'Ping-pong' => 'ping-pong', - 'Pioneer' => 'pioneer', - 'Piscines' => 'piscines', - 'Pizza' => 'pizza', - 'Places de cinéma' => 'places-de-cinema', - 'Plafonniers' => 'plafonniers', - 'Plancha' => 'planchas', - 'Plantes & semis' => 'plantes', - 'Plaques de cuisson' => 'plaques-de-cuisson', - 'Platines vinyle' => 'platines-vinyle', - 'Plats & moules' => 'plats-et-moules', - 'PlayerUnknown's Battlegrounds' => 'playerunknown-s-battleground', - 'Playmobil' => 'playmobil', - 'PlayStation' => 'playstation', - 'Pneus' => 'pneus', - 'PocketBook' => 'pocketbook', - 'PocketBook Touch Lux 3' => 'pocketbook-touch-lux-3', - 'POCO F2 Pro' => 'poco-f2-pro', - 'POCO F3' => 'poco-f3', - 'POCO M3' => 'poco-m3', - 'POCO X3' => 'poco-x3', - 'POCO X3 Pro' => 'poco-x3-pro', - 'Poêles' => 'poeles', - 'Pokémon' => 'pokemon', - 'Pokémon: Let's Go' => 'pokemon-letsgo', - 'Pokémon Épée et Bouclier' => 'pokemon-epee-bouclier', - 'Pokémon Tournament' => 'pokemon-tournament', - 'Pokémon Ultra Sun / Moon' => 'pokemon-ultra-sun-moon', - 'Polaroid' => 'polaroid', - 'Polos' => 'polos', - 'Pompes à vélo' => 'pompes-velo', - 'Porte-bébé' => 'porte-bebe', - 'Portefeuilles' => 'portefeuilles', - 'Posters' => 'posters', - 'Potager' => 'potager', - 'Pots & cache-pots' => 'pots-et-cache-pots', - 'Poubelles' => 'poubelles', - 'Poulaillers' => 'poulaillers', - 'Poupées' => 'poupees', - 'Poussettes' => 'poussettes-bebe', - 'Présentez-vous !' => 'mieux-se-connaitre-presentez-vous', - 'Préservatifs' => 'preservatifs', - 'Princesse Tam-Tam' => 'princesse-tam-tam', - 'Prises connectées' => 'prises-connectees', - 'Processeurs' => 'processeurs', - 'Produit pour lentilles' => 'produit-pour-lentilles', - 'Produits de massage' => 'produits-de-massage', - 'Produits frais' => 'produits-frais', - 'Produits reconditionnés' => 'reconditionne', - 'Produits vétérinaires' => 'produits-veterinaires', - 'Programme d'Entraînement Cérébral du Dr. Kawashima' => 'dr-kawashima-brain-training', - 'Project Cars 2' => 'project-cars-2', - 'Protection de la maison' => 'protection-de-la-maison', - 'Protections intimes' => 'protections-intimes', - 'Protection solaire' => 'protection-solaire', - 'Puériculture' => 'puericulture', - 'Pulls' => 'pulls', - 'Puma' => 'puma', - 'Purificateurs d'air' => 'purificateurs-d-air', - 'Purina' => 'purina', - 'Puzzles' => 'puzzles', - 'Pyjamas' => 'pyjamas', - 'Pyjamas & chemises de nuit' => 'pyjamas-chemises-de-nuit', - 'Pyjamas pour bébés' => 'pyjamas-pour-bebes', - 'Qobuz' => 'qobuz', - 'Quiksilver' => 'quiksilver', - 'Radiateurs' => 'radiateurs', - 'Ralph Lauren' => 'ralph-lauren', - 'RAM' => 'ram', - 'Randonnée' => 'randonnee', - 'Raquettes de ping-pong' => 'raquettes-de-ping-pong', - 'Raquettes de tennis' => 'raquettes-de-tennis', - 'Rasage et épilation' => 'rasage-epilation', - 'Rasoirs Braun' => 'rasoirs-braun', - 'Rasoirs électriques' => 'rasoirs-electriques', - 'Rasoirs Gillette' => 'gillette', - 'Rasoirs manuels' => 'rasoirs-manuels', - 'Rasoirs Philips' => 'rasoirs-philips', - 'Rasoirs Wilkinson' => 'rasoirs-wilkinson-sword', - 'Raspberry Pi' => 'raspberry-pi', - 'Ray-Ban' => 'ray-ban', - 'Razer' => 'razer', - 'Razer DeathAdder' => 'razer-deathadder', - 'Realme 5 Pro' => 'realme-5-pro', - 'Realme X2 Pro' => 'realme-x2-pro', - 'Red Dead Redemption' => 'red-dead-redemption', - 'Red Dead Redemption 2' => 'red-dead-redemption-2', - 'Réductions étudiants & jeunes' => 'reductions-etudiants-et-jeunes', - 'Reebok' => 'reebok', - 'Reebok Club C' => 'reebok-club-c', - 'Réfrigérateurs' => 'refrigerateurs', - 'Réfrigérateurs américains' => 'refrigerateurs-americains', - 'Refroidissement PC' => 'refroidissement-pc', - 'Réhausseurs' => 'rehausseurs', - 'Remington' => 'remington', - 'Repas de fête' => 'repas-fete-reveillon', - 'Repassage' => 'repassage', - 'Répéteurs' => 'repeteurs', - 'Réseau' => 'reseau', - 'Resident Evil' => 'resident-evil', - 'Resident Evil 3' => 'resident-evil-3', - 'Resident Evil 7' => 'resident-evil-7', - 'Restaurants' => 'restaurants', - 'Revêtements de sols' => 'revetements-de-sols', - 'Revêtements muraux' => 'revetements-muraux', - 'Rhum' => 'rhum', - 'Richelieus' => 'richelieus', - 'Ring Fit Adventure' => 'ring-fit-adventure', - 'Risk' => 'risk', - 'Robes & jupes' => 'robes-et-jupes', - 'Roborock' => 'roborock', - 'Roborock S5 MAX' => 'roborock-s5-max', - 'Roborock S6' => 'roborock-s6', - 'Robots cuiseurs' => 'robots-cuiseurs', - 'Robots ménagers' => 'robots-menagers', - 'Robot tondeuse' => 'robot-tondeuse', - 'ROCCAT' => 'roccat', - 'Rollers' => 'rollers', - 'Rouges à lèvres' => 'rouges-a-levres', - 'Routeurs' => 'routeurs', - 'Rowenta' => 'rowenta', - 'Royal Canin' => 'royal-canin', - 'RTX 2060' => 'rtx-2060', - 'RTX 2070' => 'rtx-2070', - 'RTX 2080' => 'rtx-2080', - 'RTX 2080 Ti' => 'rtx-2080-ti', - 'RTX 3070' => 'rtx-3070', - 'RTX 3080' => 'rtx-3080', - 'RTX 3090' => 'rtx-3090', - 'RX 480' => 'rx-480', - 'RX 580' => 'rx-580', - 'RX 590' => 'radeon-rx-590', - 'RX Vega 56' => 'rx-vega-56', - 'RX Vega 64' => 'rx-vega-64', - 'Sacs à déjections' => 'sacs-a-dejections', - 'Sacs à dos' => 'sacs-a-dos', - 'Sacs à langer' => 'sacs-a-langer', - 'Sacs à main' => 'sacs-a-main', - 'Sacs bandoulière' => 'sacs-bandouliere', - 'Sacs de couchage' => 'sacs-de-couchage', - 'Sacs de randonnée' => 'sacs-de-randonnee', - 'Sacs de sport' => 'sacs-de-sport', - 'Sacs de voyage' => 'sacs-de-voyage', - 'Salle à manger' => 'salle-manger', - 'Samsonite' => 'samsonite', - 'Samsung' => 'samsung', - 'Samsung Galaxy A5' => 'samsung-galaxy-a5', - 'Samsung Galaxy A50' => 'samsung-galaxy-a50', - 'Samsung Galaxy A51' => 'samsung-galaxy-a51', - 'Samsung Galaxy A51 5G' => 'samsung-galaxy-a51-5g', - 'Samsung Galaxy A70' => 'samsung-galaxy-a70', - 'Samsung Galaxy A80' => 'samsung-galaxy-a80', - 'Samsung Galaxy Buds' => 'samsung-galaxy-buds', - 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus', - 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live', - 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro', - 'Samsung Galaxy Fold' => 'samsung-galaxy-fold', - 'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8', - 'Samsung Galaxy Note 9' => 'samsung-galaxy-note-9', - 'Samsung Galaxy Note 10' => 'samsung-galaxy-note-10', - 'Samsung Galaxy Note 10 Lite' => 'samsung-galaxy-note-10-lite', - 'Samsung Galaxy Note 10 Plus' => 'samsung-galaxy-note-10-plus', - 'Samsung Galaxy Note20' => 'samsung-galaxy-note-20', - 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note-20-ultra', - 'Samsung Galaxy S7' => 'samsung-galaxy-s7', - 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge', - 'Samsung Galaxy S8' => 'samsung-galaxy-s8', - 'Samsung Galaxy S8+' => 'samsung-galaxy-s8plus', - 'Samsung Galaxy S9' => 'samsung-galaxy-s9', - 'Samsung Galaxy S9 Plus' => 'samsung-galaxy-s9-plus', - 'Samsung Galaxy S10' => 'samsung-galaxy-s10', - 'Samsung Galaxy S10 Lite' => 'samsung-galaxy-s10-lite', - 'Samsung Galaxy S10+' => 'samsung-galaxy-s10-plus', - 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e', - 'Samsung Galaxy S20' => 'samsung-galaxy-s20', - 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe', - 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra', - 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus', - 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g', - 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g', - 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g', - 'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a', - 'Samsung Galaxy Tab S2' => 'samsung-galaxy-tab-s2', - 'Samsung Galaxy Tab S3' => 'samsung-galaxy-tab-s3', - 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4', - 'Samsung Galaxy Tab S5e' => 'samsung-galaxy-tab-s5e', - 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6', - 'Samsung Galaxy Tab S7' => 'samsung-galaxy-tab-s7', - 'Samsung Galaxy Watch' => 'samsung-galaxy-watch', - 'Samsung Galaxy Watch3' => 'samsung-galaxy-watch-3', - 'Samsung Galaxy Watch Active 2' => 'samsung-galaxy-watch-active2', - 'Samsung Galaxy Z Flip' => 'galaxy-z-flip', - 'Samsung Gear' => 'samsung-gear', - 'Samsung Gear S3' => 'samsung-gear-s3', - 'Samsung Gear VR' => 'samsung-gear-vr', - 'Sandales' => 'sandales', - 'SanDisk' => 'sandisk', - 'Sanitaires et robinetterie' => 'sanitaires-robinetterie', - 'Santé & Cosmétiques' => 'sante-et-cosmetiques', - 'Sapins de Noël' => 'sapins-noel', - 'Savons' => 'savons', - 'Scanners' => 'scanners', - 'Scanners A3' => 'scanners-a3', - 'Scanners A4' => 'scanners-a4', - 'Scies' => 'scies', - 'Scooters' => 'scooters', - 'Seagate' => 'seagate', - 'Sécateurs' => 'secateurs', - 'Sèche-cheveux' => 'seche-cheveux', - 'Sèche-linge' => 'seche-linge', - 'Seiko' => 'seiko', - 'Séjours' => 'sejours', - 'Sekiro: Shadows Die Twice' => 'sekiro', - 'Semis & graines' => 'semis-et-graines', - 'Sennheiser' => 'sennheiser', - 'Senseo' => 'senseo', - 'Séries TV' => 'series-tv', - 'Service & réparation auto-moto' => 'service-reparation-auto-moto', - 'Services' => 'services-divers', - 'Services auto' => 'services-auto', - 'Services de livraison' => 'services-livraisons', - 'Services moto' => 'services-moto', - 'Services photo' => 'services-photo', - 'Serviettes' => 'serviettes', - 'Serviettes hygiéniques' => 'serviettes-hygieniques', - 'Sextoys' => 'sextoys', - 'Shadow of the Colossus' => 'shadow-of-the-colossus', - 'Shadow of the Tomb Raider' => 'shadow-tomb-raider', - 'Shalimar' => 'shalimar', - 'Shampooings & soins' => 'shampooings-et-soins', - 'Shenmue' => 'shenmue', - 'Shenmue I & II' => 'shenmue-i-ii', - 'Shenmue III' => 'shenmue-iii', - 'Shorts' => 'shorts', - 'Shorts de bain' => 'shorts-de-bain', - 'Sièges auto' => 'sieges-auto', - 'Siemens' => 'siemens', - 'Skates & longboards' => 'skates-et-longboards', - 'Skechers' => 'sketchers', - 'Ski' => 'ski', - 'Skyrim' => 'skyrim', - 'Slips & boxers' => 'slips-et-boxers', - 'Smartphones' => 'smartphones', - 'Smartphones à moins de 100€' => 'smartphones-moins-de-100', - 'Smartphones à moins de 200€' => 'smartphones-moins-de-200', - 'Smartphones Android' => 'smartphones-android', - 'Smartphones Asus' => 'smartphones-asus', - 'Smartphones Google' => 'smartphones-google', - 'Smartphones Honor' => 'smartphones-honor', - 'Smartphones HTC' => 'smartphones-htc', - 'Smartphones Huawei' => 'smartphones-huawei', - 'Smartphones Lenovo Motorola' => 'smartphones-lenovo-motorola', - 'Smartphones LG' => 'smartphones-lg', - 'Smartphones Nokia' => 'smartphones-nokia', - 'Smartphones OnePlus' => 'smartphones-oneplus', - 'Smartphones Oppo' => 'smartphones-oppo', - 'Smartphones Realme' => 'smartphones-realme', - 'Smartphones Samsung' => 'smartphones-samsung', - 'Smartphones Sony' => 'smartphones-sony', - 'Smartphones Xiaomi' => 'smartphones-xiaomi', - 'Smartphones ZTE' => 'smartphones-zte', - 'Smart TV' => 'smart-tv', - 'Sneakers' => 'sneakers', - 'SodaStream' => 'sodastream', - 'Sofas gonflable' => 'sofas-gonflable', - 'Soin barbe et rasage' => 'soin-barbe-rasage', - 'Soin de la peau' => 'soin-peau', - 'Soin des cheveux' => 'soin-des-cheveux', - 'Soin des ongles' => 'soin-ongles', - 'Soins dentaires' => 'soins-dentaires', - 'Sonos' => 'sonos', - 'Sonos Beam' => 'sonos-beam', - 'Sonos Move' => 'sonos-move', - 'Sonos One' => 'sonos-one', - 'Sonos PLAY:1' => 'sonos-play-1', - 'Sonos PLAY:3' => 'sonos-play-3', - 'Sonos PLAY:5' => 'sonos-play-5', - 'Sonos PLAYBAR' => 'sonos-playbar', - 'Sony' => 'sony', - 'Sony PlayStation VR' => 'sony-playstation-vr', - 'Sony Pulse 3D sans fil' => 'casque-audio-sony-pulse-3d', - 'Sony WF-1000XM3' => 'sony-wf-1000xm3', - 'Sony WH-1000XM3' => 'sony-wh-1000xm3', - 'Sony WH-1000XM4' => 'sony-wh-1000xm4', - 'Sony Xperia XA1' => 'sony-xperia-xa1', - 'Sony Xperia X Compact' => 'sony-xperia-x-compact', - 'Sony Xperia XZ1' => 'sony-xperia-xz1', - 'Sony Xperia XZ1 Compact' => 'sony-xperia-xz1-compact', - 'Sony Xperia XZ Premium' => 'sony-xperia-xz-premium', - 'Sony Xperia Z3' => 'sony-xperia-z3', - 'Soulcalibur' => 'soulcalibur', - 'Souris' => 'souris', - 'Souris gamer' => 'souris-gamer', - 'Souris Logitech' => 'souris-logitech', - 'Souris sans fil' => 'souris-sans-fil', - 'Sous-vêtements' => 'sous-vetements', - 'Sous-vêtements de sport' => 'sous-vetements-de-sport', - 'South Park' => 'south-park', - 'Soutiens-gorge' => 'soutiens-gorge', - 'Spas' => 'spa', - 'Spectacles' => 'spectacles', - 'Spectacles & Billetterie' => 'sorties', - 'Spectacles comiques' => 'spectacles-comiques', - 'Spectacles pour enfants' => 'spectacles-pour-enfants', - 'Sports & plein air' => 'sports-plein-air', - 'Sports collectifs' => 'sports-collectifs', - 'Sports nautiques' => 'sports-nautiques', - 'Sportswear' => 'sportswear', - 'Spotify' => 'spotify', - 'SSD' => 'ssd', - 'Star Wars: Jedi Fallen Order' => 'star-wars-jedi-fallen-order', - 'Star Wars: Squadrons' => 'star-wars-squadrons', - 'Star Wars Battlefront' => 'star-wars-battlefront', - 'Stations météo' => 'stations-meteo', - 'Stickers muraux' => 'stickers-muraux', - 'Stihl' => 'stihl', - 'Stockage externe' => 'stockage', - 'Streaming' => 'streaming', - 'Streaming musical' => 'streaming-musical', - 'Streaming vidéo' => 'streaming-video', - 'Stylos' => 'stylos', - 'Sucettes' => 'sucettes', - 'Super Mario' => 'super-mario', - 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars', - 'Super Mario Maker 2' => 'super-mario-maker-2', - 'Super Mario Party' => 'super-mario-party', - 'Super Smash Bros. Ultimate' => 'super-smash-bros-ultimate', - 'Support GPS & smartphone' => 'support-gps-et-smartphone', - 'Supports TV' => 'supports-tv', - 'Surface Pro 4' => 'surface-pro-4', - 'Surgelés' => 'surgeles', - 'Surveillance' => 'surveillance', - 'Suspensions' => 'suspensions', - 'Swatch' => 'swatch', - 'Switch réseau' => 'switch-reseau', - 'Systèmes d'exploitation' => 'systemes-d-exploitation', - 'Systèmes multiroom' => 'systemes-multiroom', - 'T-shirts' => 't-shirts', - 'Tables' => 'tables', - 'Tables à langer' => 'tables-a-langer', - 'Tables à repasser' => 'tables-a-repasser', - 'Tables basses' => 'tables-basses', - 'Tables de camping' => 'tables-de-camping', - 'Tables de mixage' => 'tables-de-mixage', - 'Tables de ping-pong' => 'tables-ping-pong', - 'Tablettes' => 'tablettes', - 'Tablettes graphiques' => 'tablettes-graphiques', - 'Tablettes graphiques Huion' => 'huion', - 'Tablettes graphiques Wacom' => 'wacom', - 'Tablettes Huawei' => 'tablettes-huawei', - 'Tablettes Lenovo' => 'tablettes-lenovo', - 'Tablettes Microsoft Surface' => 'tablettes-microsoft-surface', - 'Tablettes Samsung' => 'tablettes-samsung', - 'Tablettes Xiaomi' => 'tablettes-xiaomi', - 'Tampons' => 'tampons', - 'Tapis' => 'tapis', - 'Tapis de souris' => 'tapis-de-souris', - 'Tassimo' => 'tassimo', - 'Taxis' => 'taxis', - 'Tefal' => 'tefal', - 'Tekken' => 'tekken', - 'Tekken 7' => 'tekken-7', - 'Télécommandes' => 'telecommandes', - 'Téléphones fixes' => 'telephones-fixes', - 'Téléphonie' => 'telephonie', - 'Téléviseurs' => 'televiseurs', - 'Tentes' => 'tentes', - 'Tentes Quechua' => 'tentes-quechua', - 'Têtes de brosse à dents de rechange' => 'tetes-de-brosse-a-dents-de-rechange', - 'Théâtre' => 'theatre', - 'The Last of Us' => 'the-last-of-us', - 'The Last of Us Part II' => 'the-last-of-us-part-2', - 'The Legend of Zelda' => 'the-legend-of-zelda', - 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild', - 'The Legend of Zelda: Link's Awakening' => 'legend-of-zelda-link-s-awakening', - 'The Legend of Zelda: Skyward Sword HD' => 'the-legend-of-zelda-skyward-sword-hd', - 'Thermomètres' => 'thermometres', - 'Thermomix' => 'thermomix', - 'Thermostats connectés' => 'thermostat-connecte', - 'Thés' => 'thes', - 'Thés glacés' => 'thes-glaces', - 'The Walking dead' => 'the-walking-dead', - 'The Witcher' => 'the-witcher', - 'The Witcher 3' => 'the-witcher-3', - 'Time's Up!' => 'time-s-up', - 'Tokyo Laundry' => 'tokyo-laundry', - 'Tomb Raider' => 'tomb-raider', - 'Tom Clancy's' => 'tom-clancy-s', - 'Tom Clancy's Ghost Recon: Wildlands' => 'tom-clancy-s-ghost-recon-wildlands', - 'Tom Clancy's Ghost Recon Breakpoint' => 'tom-clancy-s-ghost-recon-breakpoint', - 'Tom Clancy's The Division' => 'tom-clancy-s-the-division', - 'TomTom' => 'tomtom', - 'Tondeuses' => 'tondeuses', - 'Tondeuses à gazon' => 'tondeuses-a-gazon', - 'Toner' => 'toner', - 'Tongs' => 'tongs', - 'Torchons' => 'torchons', - 'Toshiba' => 'toshiba', - 'Total War' => 'total-war', - 'Total War: Warhammer' => 'total-war-warhammer', - 'Total War: Warhammer II' => 'total-war-warhammer-ii', - 'Tournevis' => 'tournevis-et-visseuses', - 'TP-Link' => 'tp-link', - 'Trains & Bus' => 'trains-bus', - 'Trampolines' => 'trampolines', - 'Transats & cosys' => 'transats-et-cosys', - 'Transport bébé' => 'poussettes', - 'Transport d'animaux' => 'transport-d-animaux', - 'Transports en commun' => 'transports-en-commun', - 'Transports urbains' => 'transports-urbains', - 'Travaux & matériaux' => 'travaux-materiaux', - 'Trépieds' => 'trepieds', - 'Trixie' => 'trixie', - 'Tronçonneuses' => 'tronconneuses', - 'Tropico' => 'tropico', - 'Tropico 6' => 'tropico-6', - 'Trottinettes' => 'trottinettes', - 'Trottinettes électriques' => 'trottinettes-electriques', - 'Trottinettes électriques en libre-service' => 'location-trottinettes-electriques', - 'Trottinettes Xiaomi' => 'trottinettes-xiaomi', - 'TV & Vidéo' => 'tv-video', - 'TV 4K' => 'tv-4k', - 'TV 40'' à 64''' => 'tv-40-pouces-a-64-pouces', - 'TV 65'' et plus' => 'tv-65-pouces-et-plus', - 'TV Hisense' => 'tv-hisense', - 'TV LG' => 'tv-lg', - 'TV OLED' => 'tv-oled', - 'TV Panasonic' => 'tv-panasonic', - 'TV Philips' => 'tv-philips', - 'TV Samsung' => 'tv-samsung', - 'TV Samsung QLED' => 'tv-samsung-qled', - 'TV Samsung The Frame' => 'tv-samsung-the-frame', - 'TV Sony' => 'tv-sony', - 'TV TCL' => 'tv-tcl', - 'TV Toshiba' => 'tv-toshiba', - 'TV Xiaomi' => 'tv-xiaomi', - 'UE Boom 2' => 'ue-boom-2', - 'UE Boom 3' => 'ue-boom-3', - 'UE Megaboom' => 'ue-megaboom', - 'UE Megaboom 3' => 'ue-megaboom-3', - 'UE Wonderboom' => 'ue-wonderboom', - 'Ultraportables' => 'ultraportables', - 'Uncharted' => 'uncharted', - 'Uncharted 4' => 'uncharted-4', - 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy', - 'Under Armour' => 'under-armour', - 'Until Dawn' => 'until-dawn', - 'Ustensiles de cuisine' => 'ustensiles-de-cuisine', - 'Ustensiles de cuisson' => 'ustensiles-de-cuisson', - 'Vacances et séjours' => 'vacances-sejours', - 'Vaisselle' => 'vaisselle', - 'Valises' => 'valises', - 'Valises cabine' => 'valises-cabine', - 'Valises rigides' => 'valises-rigides', - 'Vans Old Skool' => 'vans-old-skool', - 'Variétés & revues' => 'varietes-et-revues', - 'Vases' => 'vases', - 'Veet' => 'veet', - 'Veilleuses' => 'veilleuses', - 'Vélos' => 'velos', - 'Vélos d'appartement' => 'velos-d-appartement', - 'Vélos électriques' => 'velos-electriques', - 'Ventilateurs' => 'ventilateurs', - 'Ventirad' => 'ventirad', - 'Vernis à ongles' => 'vernis-a-ongles', - 'Verres' => 'verres', - 'Vestes' => 'vestes', - 'Vestes polaires' => 'vestes-polaires', - 'Vêtements d'été' => 'vetements-d-ete', - 'Vêtements d'hiver' => 'vetements-d-hiver', - 'Vêtements de grossesse' => 'vetements-de-grossesse', - 'Vêtements de montagne' => 'vetements-techniques', - 'Vêtements de running' => 'vetements-de-running', - 'Vêtements de ski' => 'vetements-de-ski', - 'Vêtements de sport' => 'vetements-de-sport', - 'Vêtements pour bébé' => 'vetements-pour-bebe', - 'Vidéoprojecteurs' => 'projecteurs', - 'Vidéoprojecteurs 3D' => 'videoprojecteurs-3d', - 'Vidéoprojecteurs Acer' => 'videoprojecteurs-acer', - 'Vidéoprojecteurs BenQ' => 'videoprojecteurs-benq', - 'Vidéoprojecteurs Epson' => 'videoprojecteurs-epson', - 'Vidéoprojecteurs HD' => 'videoprojecteurs-hd', - 'Vidéoprojecteurs LG' => 'videoprojecteurs-lg', - 'Vidéoprojecteurs Optoma' => 'videoprojecteurs-optoma', - 'Vins' => 'vins', - 'Visites & patrimoine' => 'visites-et-patrimoine', - 'Visseuses' => 'visseuses', - 'VOD' => 'vod', - 'Voitures & motos' => 'voitures-motos', - 'Voitures télécommandées' => 'voitures-telecommandees', - 'Volants' => 'volants-de-course', - 'Vols' => 'billets-d-avion', - 'Voyages' => 'voyages', - 'Voyages & loisirs' => 'le-laboratoire-des-voyages-loisirs', - 'VPN' => 'vpn', - 'VTC' => 'vtc', - 'VTT' => 'vtt', - 'Wacom Cintiq' => 'cintiq', - 'Watch Dogs' => 'watch-dogs', - 'Watch Dogs 2' => 'watch-dogs-2', - 'Watch Dogs: Legion' => 'watch-dogs-legion', - 'Watercooling' => 'watercooling', - 'WD (Western Digital)' => 'western-digital', - 'Wearables' => 'wearables', - 'Webcams' => 'webcams', - 'Whey' => 'whey', - 'Whirlpool' => 'whirlpool', - 'Whiskas' => 'whiskas', - 'Whisky' => 'whisky', - 'Wiko' => 'wiko', - 'Wilkinson Sword Hydro 5' => 'wilkinson-sword-hydro-5', - 'Windows' => 'windows', - 'WindScribe' => 'windscribe', - 'Wolfenstein' => 'wolfenstein', - 'Wolfenstein II: The New Colossus' => 'wolfenstein-ii-the-new-colossus', - 'Xbox' => 'xbox', - 'Xbox Game Pass' => 'xbox-game-pass', - 'Xbox Live' => 'xbox-live', - 'XCOM' => 'xcom', - 'XCOM 2' => 'xcom-2', - 'Xiaomi' => 'xiaomi', - 'Xiaomi AirDots' => 'xiaomi-airdots', - 'Xiaomi Black Shark' => 'xiaomi-black-shark', - 'Xiaomi Black Shark 2' => 'xiaomi-black-shark-2', - 'Xiaomi Mi6' => 'xiaomi-mi6', - 'Xiaomi Mi8' => 'xiaomi-mi8', - 'Xiaomi Mi8 Lite' => 'xiaomi-mi8-lite', - 'Xiaomi Mi8 Pro' => 'xiaomi-mi8-pro', - 'Xiaomi Mi8 SE' => 'xoaimi-mi8-se', - 'Xiaomi Mi9' => 'xiaomi-mi9', - 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite', - 'Xiaomi Mi 9 Pro' => 'xiaomi-mi-9-pro', - 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se', - 'Xiaomi Mi 9T' => 'xiaomi-mi-9t', - 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro', - 'Xiaomi Mi 10' => 'xiaomi-mi-10', - 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite', - 'Xiaomi Mi 10 Pro' => 'xiaomi-mi-10-pro', - 'Xiaomi Mi 10T' => 'xiaomi-mi-10t', - 'Xiaomi Mi 10T Lite' => 'xiaomi-mi-10t-lite', - 'Xiaomi Mi 10T Pro' => 'xiaomi-mi-10t-pro', - 'Xiaomi Mi 11' => 'xiaomi-mi-11', - 'Xiaomi Mi 11 Lite' => 'xiaomi-mi-11-lite', - 'Xiaomi Mi A1' => 'xiaomi-mi-a1', - 'Xiaomi Mi A2' => 'xiaomi-mi-a2', - 'Xiaomi Mi A2 Lite' => 'xiaomi-mi-a2-lite', - 'Xiaomi Mi Airdots Pro' => 'xiaomi-mi-airdots-pro', - 'Xiaomi Mi Band' => 'xiaomi-mi-band', - 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4', - 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5', - 'Xiaomi Mi Band 6' => 'xiaomi-mi-band-6', - 'Xiaomi Mi Box' => 'xiaomi-mi-box', - 'Xiaomi Mi Electric Scooter M365' => 'xiaomi-mi-electric-scooter-m365', - 'Xiaomi Mi Max' => 'xiaomi-mi-max', - 'Xiaomi Mi Mix' => 'xiaomi-mi-mix', - 'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2', - 'Xiaomi Mi Note 10' => 'xiaomi-mi-note-10', - 'Xiaomi Mi Note 10 Pro' => 'xiaomi-mi-note-10-pro', - 'Xiaomi Mi Pad 3' => 'xiaomi-mi-pad-3', - 'Xiaomi Mi Watch' => 'xiaomi-mi-watch', - 'Xiaomi Pocophone F1' => 'xiaomi-pocophone-f1', - 'Xiaomi Redmi 4A' => 'xiaomi-redmi-4a', - 'Xiaomi Redmi 4X' => 'xiaomi-redmi-4x', - 'Xiaomi Redmi 7' => 'xiaomi-redmi-7', - 'Xiaomi Redmi 9' => 'xiaomi-redmi-9', - 'Xiaomi Redmi AirDots' => 'xiaomi-redmi-airdots', - 'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4', - 'Xiaomi Redmi Note 5' => 'xiaomi-redmi-note-5', - 'Xiaomi Redmi Note 6' => 'xiaomi-redmi-note-6', - 'Xiaomi Redmi Note 7' => 'xiaomi-redmi-note-7', - 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8', - 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro', - 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9', - 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro', - 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s', - 'Xiaomi Redmi Note 10' => 'xiaomi-redmi-note-10', - 'Xiaomi Redmi Note 10 Pro' => 'xiaomi-redmi-10-pro', - 'Xiaomi Smart Home' => 'xiaomi-smart-home', - 'Yamaha' => 'yamaha', - 'Yeelight' => 'xiaomi-yeelight', - 'Yoshi's Crafted World' => 'yoshi-crafted-world', - 'Zoos' => 'zoos', - ) - ), - 'order' => array( - 'name' => 'Trier par', - 'type' => 'list', - 'title' => 'Ordre de tri des deals', - 'values' => array( - 'Du deal le plus Hot au moins Hot' => '-hot', - 'Du deal le plus récent au plus ancien' => '-nouveaux', - ) - ) - ), - 'Surveillance Discussion' => array( - 'url' => array( - 'name' => 'URL de la discussion', - 'type' => 'text', - 'required' => true, - 'title' => 'URL discussion à surveiller: https://www.dealabs.com/discussions/titre-1234', - 'exampleValue' => 'https://www.dealabs.com/discussions/jeux-steam-gratuits-gleam-woobox-etc-1071415', - ), + 'Deals par groupe' => [ + 'group' => [ + 'name' => 'Groupe', + 'type' => 'list', + 'title' => 'Groupe dont il faut afficher les deals', + 'values' => [ + 'Abattants WC' => 'abattants-wc', + 'Abonnement PlayStation Plus' => 'playstation-plus', + 'Abonnements cinéma' => 'abonnements-cinema', + 'Abonnements de train' => 'abonnements-de-train', + 'Abonnements internet' => 'abonnements-internet', + 'Abonnements presse' => 'abonnements-presse', + 'Accessoires aquarium' => 'accessoires-aquarium', + 'Accessoires auto' => 'auto', + 'Accessoires électroniques' => 'accessoires-gadgets', + 'Accessoires gamers PC' => 'accessoires-gamers-pc', + 'Accessoires gaming' => 'accessoires-gaming', + 'Accessoires iPhone' => 'accessoires-iphone', + 'Accessoires mode' => 'accessoires-mode', + 'Accessoires moto' => 'moto', + 'Accessoires Nintendo' => 'accessoires-nintendo', + 'Accessoires PC portables' => 'accessoires-pc-portables', + 'Accessoires photo' => 'accessoires-photo', + 'Accessoires PlayStation' => 'accessoires-playstation', + 'Accessoires pour barbecue' => 'accessoires-barbecue', + 'Accessoires studio photo' => 'accessoires-studio-photo', + 'Accessoires téléphonie' => 'accessoires-telephonie', + 'Accessoires TV' => 'accessoires-tv', + 'Accessoires vélo' => 'accessoires-velo', + 'Accessoires Xbox' => 'accessoires-xbox', + 'Acer' => 'acer', + 'Acer Predator' => 'acer-predator', + 'Achats / Ventes' => 'achats-ventes-echanges-estimations-dons', + 'Achats à l'étranger' => 'limport-sites-avis-questions-langues', + 'Adaptateurs' => 'adaptateurs', + 'Adhérents Fnac' => 'adherents-fnac', + 'Adhésions & Souscriptions' => 'adhesions-souscriptions-abonnements', + 'adidas' => 'adidas', + 'Adidas Gazelle' => 'adidas-gazelle', + 'adidas Stan Smith' => 'adidas-stan-smith', + 'adidas Superstar' => 'adidas-superstar', + 'adidas Ultraboost' => 'adidas-ultraboost', + 'adidas Yung-1' => 'adidas-yung-1', + 'adidas ZX Flux' => 'adidas-zx-flux', + 'Adoucissant' => 'adoucissant', + 'Agendas' => 'agendas', + 'Age of Empires' => 'age-of-empires', + 'Age of Empires: Definitive Edition' => 'age-of-empires-definitive-edition', + 'Alarmes' => 'alarmes', + 'Albums photo' => 'albums-photo', + 'Alcools' => 'alcools', + 'Alcools forts' => 'alcools-forts', + 'Alimentation' => 'epicerie', + 'Alimentation bébés' => 'alimentation-bebes', + 'Alimentation PC' => 'alimentation-pc', + 'Alimentation sportifs' => 'alimentation-sportifs', + 'Amazfit Bip' => 'xiaomi-amazfit-bip', + 'Amazon Echo' => 'amazon-echo', + 'Amazon Echo Dot' => 'amazon-echo-dot', + 'Amazon Echo Plus' => 'amazon-echo-plus', + 'Amazon Echo Show' => 'amazon-echo-show', + 'Amazon Echo Show 5' => 'amazon-echo-show-5', + 'Amazon Echo Spot' => 'amazon-echo-spot', + 'Amazon Fire TV' => 'amazon-fire-tv', + 'Amazon Kindle' => 'amazon-kindle', + 'Amazon Prime' => 'amazon-prime', + 'AMD Radeon' => 'amd-radeon', + 'AMD Ryzen' => 'amd-ryzen', + 'AMD Ryzen 5 5600X' => 'amd-ryzen-5-5600x', + 'AMD Ryzen 7 5800X' => 'amd-ryzen-7-5800x', + 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x', + 'AMD Ryzen 9 5950X' => 'amd-ryzen-9-5950x', + 'AMD Vega' => 'amd-vega', + 'amiibo' => 'amiibo', + 'Amplis (guitare/basse)' => 'amplis-guitare-basse', + 'Amplis audio' => 'amplis', + 'Ampoules' => 'ampoules', + 'Ampoules à LED' => 'ampoules-a-led', + 'Angleterre' => 'angleterre', + 'Animal Crossing' => 'animal-crossing', + 'Animal Crossing: New Horizons' => 'animal-crossing-new-horizons', + 'Animaux' => 'animaux', + 'Anker' => 'anker', + 'Anno 1800' => 'anno-1800', + 'Annonces officielles' => 'annonces-officielles', + 'Anthem' => 'anthem', + 'Anti-nuisibles' => 'anti-nuisibles', + 'Anti-puces' => 'anti-puces', + 'Antivirus' => 'antivirus', + 'Antivols' => 'antivols', + 'Apex Legends' => 'apex-legends', + 'Appareils à raclette' => 'appareils-raclette', + 'Appareils de musculation' => 'appareils-de-musculation', + 'Appareils photo' => 'appareils-photo', + 'Appareils photo Canon' => 'appareils-photo-canon', + 'Appareils photo compacts' => 'appareils-photo-compacts', + 'Appareils photo instantanés' => 'appareils-photo-instantanes', + 'Appareils photo Nikon' => 'appareils-photo-nikon', + 'Appareils photo Olympus' => 'appareils-photo-olympus', + 'Appareils photo Panasonic' => 'appareils-photo-panasonic', + 'Appareils photo Sony' => 'appareils-photo-sony', + 'Apple' => 'apple', + 'Apple AirPods' => 'apple-airpods', + 'Apple AirPods 2' => 'apple-airpods-2', + 'Apple AirPods Max' => 'apple-airpods-max', + 'Apple AirPods Pro' => 'apple-airpods-pro', + 'Apple HomePod' => 'apple-homepod', + 'Apple HomePod Mini' => 'apple-homepod-mini', + 'Apple TV' => 'apple-tv', + 'Apple TV+' => 'apple-tv-plus', + 'Apple Watch' => 'apple-watch', + 'Apple Watch 3' => 'apple-watch-3', + 'Apple Watch 4' => 'apple-watch-4', + 'Apple Watch 5' => 'apple-watch-5', + 'Apple Watch 6' => 'apple-watch-6', + 'Apple Watch SE' => 'apple-watch-se', + 'Applications' => 'applications', + 'Applications Android' => 'applications-android', + 'Applications iOS' => 'applications-ios', + 'Appliques murales' => 'appliques-murales', + 'Applis & logiciels' => 'applis-logiciels', + 'Après-shampooings' => 'apres-shampooings', + 'Aquariums' => 'aquariums', + 'Arbres à chat' => 'arbres-a-chat', + 'Arduino' => 'arduino', + 'Armoires & placards' => 'armoires-et-placards', + 'Articles de cuisine et d'entretien' => 'articles-de-cuisine', + 'Arts culinaires' => 'arts-culinaires', + 'Arts de la table' => 'arts-de-la-table', + 'ASICS' => 'asics', + 'Asmodée' => 'asmodee', + 'Aspirateurs' => 'aspirateurs', + 'Aspirateurs balais' => 'aspirateurs-balais', + 'Aspirateurs Dreame' => 'aspirateurs-xiaomi', + 'Aspirateurs Dyson' => 'aspirateurs-dyson', + 'Aspirateurs robot' => 'aspirateurs-robot', + 'Aspirateurs Rowenta' => 'apsirateurs-rowenta', + 'Aspirateurs sans sac' => 'aspirateurs-sans-sac', + 'Assassin's Creed' => 'assassin-s-creed', + 'Assassin's Creed: Unity' => 'assassins-creed-unity', + 'Assassin's Creed: Valhalla' => 'assassin-s-creed-valhalla', + 'Assassin's Creed Odyssey' => 'assassin-s-creed-odyssey', + 'Assassin's Creed Origins' => 'assassin-s-creed-origins', + 'Assurances' => 'assurances', + 'Astuces pour économiser' => 'vos-astuces-pour-faire-des-economies', + 'Asus' => 'asus', + 'Asus ROG' => 'asus-rog', + 'Asus ROG Phone' => 'asus-rog-phone', + 'Asus ROG Phone 2' => 'asus-rog-phone-2', + 'ASUS Transformer' => 'asus-transformer', + 'Asus VivoBook' => 'asus-vivobook', + 'Asus ZenBook' => 'asus-zenbook', + 'Asus ZenFone 2' => 'asus-zenfone-2', + 'Asus ZenFone 3' => 'asus-zenfone-3', + 'Asus ZenFone 4' => 'asus-zenfone-4', + 'Asus ZenFone 6' => 'asus-zenfone-6', + 'Asus ZenFone GO' => 'asus-zenfone-go', + 'Asus ZenFone Zoom' => 'asus-zenfone-zoom', + 'Audio & Hi-fi' => 'audio-et-hi-fi', + 'Aukey' => 'aukey', + 'Auto-Moto' => 'auto-moto', + 'Autoradios' => 'autoradios', + 'Azzaro Wanted' => 'azzaro-wanted', + 'Baby foot' => 'baby-foot', + 'BabyLiss' => 'babyliss', + 'Babyphones' => 'babyphones', + 'Badminton' => 'badminton', + 'Bagagerie' => 'bagagerie', + 'Baignoires pour bébé' => 'baignoires-pour-bebe', + 'Bains de bouche' => 'bains-de-bouche', + 'Balais & serpillères' => 'balais-et-serpilleres', + 'Balances connectées' => 'balances-connectees', + 'Balançoires' => 'balancoires', + 'Ballet & danse' => 'ballet-et-danse', + 'Ballons de football' => 'ballons-de-football', + 'Bandes dessinées' => 'bandes-dessinees', + 'Banques' => 'banques', + 'Barbecue' => 'barbecue', + 'Barbecue électrique' => 'barbecue-electrique', + 'Barbecue Weber' => 'barbecue-weber', + 'Barbie' => 'barbie', + 'Barres de son' => 'barres-de-son', + 'Barres de son Yamaha' => 'barres-de-son-yamaha', + 'Batman Arkham' => 'batman-arkham', + 'Batteries externes' => 'batteries-externes', + 'Batteries voiture' => 'batteries-voiture', + 'Batteurs' => 'batteurs-electriques', + 'Battlefield' => 'battlefield', + 'Battlefield 1' => 'battlefield-1', + 'Battlefield V' => 'battlefield-5', + 'Béaba' => 'beaba', + 'Beats by Dre' => 'beats-by-dre', + 'Beats Solo 3' => 'beats-solo-3', + 'Beats Studio 3' => 'beats-studio-3', + 'Beauté' => 'beaute', + 'Bébés' => 'bebes-nouveaux-nes', + 'BenQ' => 'benq', + 'Be quiet!' => 'be-quiet', + 'Beyerdynamic MMX 300' => 'beyerdynamic-mmx-300', + 'Biberons' => 'biberons', + 'Bien-être & santé' => 'bien-etre-et-massages', + 'Bières' => 'bieres', + 'Bijoux' => 'bijoux', + 'Bikinis' => 'bikinis', + 'Bilans de santé & dépistages' => 'bilans-de-sante-et-depistages', + 'Billets de bus' => 'billets-de-bus', + 'Billets de train' => 'billets-de-train', + 'BioShock' => 'bioshock', + 'BioShock Infinite' => 'bioshock-infinite', + 'Bitdefender' => 'bitdefender', + 'Blabla' => 'blabla-parlez-de-tout-et-de-rien', + 'Black & Decker' => 'black-decker', + 'Blackberry' => 'blackberry', + 'Black Desert Online' => 'black-desert-online', + 'Blédina' => 'bledina', + 'Blenders' => 'blenders', + 'Bleu de Chanel' => 'bleu-de-chanel', + 'Blousons de moto' => 'blousons-de-moto', + 'Blu-Ray' => 'blu-ray', + 'Bodys pour bébé' => 'bodys-pour-bebe', + 'Boissons' => 'boissons', + 'Boîtes à outils' => 'boites-a-outils', + 'Boîtiers PC' => 'boitiers-pc', + 'Boîtiers TV' => 'boitiers-tv', + 'Bonbons' => 'bonbons', + 'Bonnets' => 'bonnets', + 'Bonnets de bain' => 'bonnets-de-bain', + 'Borderlands' => 'borderlands', + 'Borderlands 3' => 'borderlands-3', + 'Bosch' => 'bosch', + 'Bose' => 'bose', + 'Bose Headphones 700' => 'bose-headphones-700', + 'Bose Home Speaker 500' => 'bose-home-speaker-500', + 'Bose QuietComfort' => 'bose-quietcomfort', + 'Bose QuietComfort 35 II' => 'bose-quietcomfort-35ii', + 'Bose SoundLink' => 'bose-soundlink', + 'Bose SoundTouch' => 'bose-soundtouch', + 'Bottes' => 'bottes', + 'Bottes de moto' => 'bottes-de-moto', + 'Bottes de neige' => 'bottes-neige', + 'Bottes de pluie' => 'bottes-pluie', + 'Bottes femme' => 'bottes-femme', + 'Bottes homme' => 'bottes-homme', + 'Bougies & bougeoirs' => 'bougies-et-bougeoirs', + 'Box beauté' => 'box-beaute', + 'Bracelet fitness' => 'bracelet-fitness', + 'Brandt' => 'brandt', + 'Braun Series 3' => 'braun-series-3', + 'Braun Series 5' => 'braun-series-5', + 'Braun Series 7' => 'braun-series-7', + 'Braun Series 9' => 'braun-series-9', + 'Braun Silk Épil' => 'braun-silk-epil', + 'Brita' => 'brita', + 'Brosses à dents' => 'brosses-a-dents', + 'Brosses à dents électriques' => 'brosses-a-dents-electriques', + 'Brosses à dents électriques Oral-B' => 'brosses-a-dents-electriques-oral-b', + 'Brosses pour animaux' => 'brosses-pour-animaux', + 'Cable management' => 'cable-management', + 'Câbles' => 'cables', + 'Câbles Ethernet' => 'cables-ethernet', + 'Câbles HDMI' => 'cables-hdmi', + 'Câbles Jack' => 'cables-jack', + 'Câbles USB' => 'cables-usb', + 'Cadeaux' => 'cadeaux', + 'Cadres' => 'cadres', + 'Cadres de vélo' => 'cadres-de-velo', + 'Café' => 'cafe', + 'Café en dosettes' => 'cafe-en-dosettes', + 'Café en grain' => 'cafe-en-grain', + 'Cafetières' => 'cafetieres', + 'Cafetières expresso' => 'cafetieres-expresso', + 'Cafetières filtre' => 'cafetieres-filtre', + 'Cafetières italiennes' => 'cafetieres-italiennes', + 'Cahiers' => 'cahiers', + 'Caissons de basses' => 'caissons-de-basses', + 'Calendrier de l'Avent Lego' => 'calendriers-avent-lego', + 'Calendriers' => 'calendriers', + 'Calendriers de l'Avent' => 'calendriers-avent', + 'Call of Duty' => 'call-of-duty', + 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war', + 'Call of Duty: Black Ops III' => 'call-of-duty-black-ops-3', + 'Call of Duty: Black Ops IIII' => 'call-of-duty-black-ops-4', + 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare', + 'Call of Duty: Modern Warfare' => 'call-of-duty-modern-warfare', + 'Call of Duty: WW2' => 'call-of-duty-ww2', + 'Calor' => 'calor', + 'Caméras' => 'cameras', + 'Caméras IP' => 'cameras-ip', + 'Caméras sportives' => 'cameras-sportives', + 'Camping' => 'camping', + 'Canapés' => 'canape', + 'Canon' => 'canon', + 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker', + 'Caravanes' => 'caravanes', + 'Carburant' => 'carburant', + 'Cartables' => 'cartables', + 'Cartes & programmes de fidélité' => 'cartes-et-programmes-de-fidelite', + 'Cartes bancaires' => 'cartes-bancaires', + 'Cartes de développement' => 'cartes-developpement', + 'Cartes graphiques' => 'cartes-graphiques', + 'Cartes mémoire' => 'cartes-memoire', + 'Cartes mères' => 'cartes-meres', + 'Cartes postales' => 'cartes-postales', + 'Cartes prépayées Playstation Store' => 'playstation-store', + 'Cartes SD' => 'cartes-sd', + 'Cartes son' => 'cartes-son', + 'Casio' => 'casio', + 'Casque sans fil Xbox' => 'casque-sans-fil-xbox', + 'Casques Apple' => 'casques-apple', + 'Casques à réduction de bruit' => 'casque-reduction-active-bruit', + 'Casques audio' => 'casques-audio', + 'Casques Bose' => 'casques-bose', + 'Casques de moto' => 'casques-de-moto', + 'Casques de vélo' => 'casques-de-velo', + 'Casques Jabra' => 'casques-jabra', + 'Casques Samsung' => 'casques-samsung', + 'Casques sans fil' => 'casques-sans-fil', + 'Casques Sennheiser' => 'casques-sennheiser', + 'Casques Sony' => 'casques-sony', + 'Casques VR' => 'vr', + 'Casquettes' => 'casquettes', + 'Casseroles' => 'casseroles', + 'Catit' => 'catit', + 'Caves à vin' => 'caves-a-vin', + 'CD & vinyles' => 'cd-vinyles', + 'CDAV' => 'cdav', + 'Ceintures' => 'ceintures', + 'Centrales vapeur' => 'centrales-vapeur', + 'Chaînes hi-fi' => 'chaines-hi-fi', + 'Chaises' => 'chaises', + 'Chaises hautes' => 'chaises-hautes', + 'Chambre' => 'chambre', + 'Champagne' => 'champagne', + 'Chapeaux' => 'chapeaux', + 'Chapeaux & casquettes' => 'chapeaux-casquettes', + 'Chargeurs' => 'chargeurs', + 'Chargeurs allume-cigare' => 'chargeurs-allume-cigare', + 'Chargeurs de piles' => 'chargeurs-de-piles', + 'Chargeurs sans fil' => 'chargeurs-sans-fil', + 'Chasse' => 'chasse', + 'Chatières' => 'chatieres', + 'Chats' => 'chats', + 'Chauffage' => 'chauffage', + 'Chaussettes & collants' => 'chaussettes-et-collants', + 'Chaussons' => 'chaussons', + 'Chaussures' => 'chaussures', + 'Chaussures adidas' => 'chaussures-adidas', + 'Chaussures de football' => 'chaussures-de-football', + 'Chaussures de randonnée' => 'chaussures-de-randonnee', + 'Chaussures de ski' => 'chaussures-de-ski', + 'Chaussures de ville' => 'chaussures-de-ville', + 'Chaussures New Balance' => 'chaussures-new-balance', + 'Chaussures Nike' => 'chaussures-nike', + 'Chaussures pour enfants' => 'chaussures-enfants', + 'Chaussures pour femme' => 'chaussures-femme', + 'Chaussures pour homme' => 'chaussures-homme', + 'Chaussures Puma' => 'chaussures-puma', + 'Chaussures Reebok' => 'chaussures-reebok', + 'Chaussures running' => 'chaussures-de-running', + 'Chelsea boots' => 'chelsea-boots', + 'Chemises' => 'chemises', + 'Chiens' => 'chiens', + 'Chocolat' => 'chocolat', + 'Chuck Taylor' => 'chuck-taylor', + 'Cinéma' => 'cinema', + 'Cire dépilatoire' => 'cire-depilatoire', + 'Cirque & arts de rue' => 'cirque-et-arts-de-rue', + 'Citytrips' => 'citytrips', + 'Civilization' => 'civilization', + 'Civilization VI' => 'civilization-vi', + 'CK One' => 'ck-one', + 'Clarks' => 'clarks', + 'Claviers' => 'claviers', + 'Claviers (musique)' => 'claviers-musique', + 'Claviers gamer' => 'claviers-gamer', + 'Claviers Logitech' => 'claviers-logitech', + 'Claviers mécaniques' => 'claviers-mecaniques', + 'Claviers sans fil' => 'claviers-sans-fil', + 'Clés USB' => 'cles-usb', + 'Climatisation' => 'climatisation', + 'Climatiseurs' => 'climatiseurs', + 'Cocottes' => 'cocottes', + 'Coffrets de livres' => 'coffrets-de-livres', + 'Coffrets DVD' => 'coffrets-dvd', + 'Coffrets maquillage' => 'coffrets-maquillage', + 'Colliers & laisses' => 'colliers-et-laisses', + 'Compléments alimentaires' => 'complements-alimentaires', + 'Composteurs' => 'composteurs', + 'Concerts' => 'concerts', + 'Concours' => 'concours', + 'Congélateurs' => 'congelateurs', + 'Connectiques' => 'connectiques', + 'Console Google Stadia' => 'google-stadia', + 'Console Nintendo Classic Mini' => 'nintendo-classic-mini', + 'Console Nintendo Classic Mini: SNES' => 'nintendo-classic-mini-snes', + 'Console Nintendo Switch' => 'nintendo-switch', + 'Console Nintendo Switch Lite' => 'nintendo-switch-lite', + 'Console PS4' => 'playstation-4', + 'Console PS4 Pro' => 'playstation-4-pro', + 'Console PS5' => 'playstation-5', + 'Consoles' => 'consoles', + 'Consoles & jeux vidéo' => 'consoles-jeux-video', + 'Console Sega Mega Drive Mini' => 'sega-mega-drive-mini', + 'Console Xbox One S' => 'xbox-one-s', + 'Console Xbox One X' => 'xbox-one-x', + 'Console Xbox Series S' => 'xbox-series-s', + 'Console Xbox Series X' => 'xbox-series-x', + 'Consommables imprimantes' => 'consommables-imprimantes', + 'Converse' => 'converse', + 'Coques iPhone' => 'coques-iphone', + 'Corsair Void PRO' => 'corsair-void-pro', + 'Costumes' => 'costumes', + 'Costumes & déguisements' => 'costumes-et-deguisements', + 'Couches' => 'couches', + 'Couettes' => 'couettes', + 'Coupes menstruelles' => 'coupes-menstruelles', + 'Cours & formations' => 'cours-et-formations', + 'Courses hippiques' => 'courses-hippiques', + 'Couteaux de cuisine' => 'couteaux-de-cuisine', + 'Couture' => 'couture', + 'Couverts' => 'couverts', + 'Couverts pour bébés' => 'couverts-pour-bebes', + 'Covoiturage' => 'covoiturage', + 'Crash Team Racing Nitro-Fueled' => 'crash-team-racing-nitro-fueled', + 'Cravates' => 'cravates', + 'Crédits' => 'credits', + 'Crèmes hydratantes' => 'cremes-hydratantes', + 'Crèmes solaires' => 'cremes-solaires', + 'Croisières' => 'croisieres', + 'Croquettes pour chat' => 'croquettes-pour-chat', + 'Croquettes pour chien' => 'croquettes-pour-chien', + 'Cuiseurs à riz' => 'cuiseur-riz', + 'Cuisinières' => 'cuisinieres', + 'Culottes menstruelles' => 'culottes-menstruelles', + 'Culture & divertissement' => 'culture-divertissement', + 'Cyberpunk 2077' => 'cyberpunk-2077', + 'Cyclisme' => 'cyclisme', + 'Cyclisme & sports urbains' => 'cyclisme-sports-urbains', + 'Darksiders' => 'darksiders', + 'Dashcams' => 'dashcams', + 'DDR3' => 'ddr3', + 'DDR4' => 'ddr4', + 'Dead Rising' => 'dead-rising', + 'Death Stranding' => 'death-stranding', + 'Décoration' => 'decoration', + 'Décorations de Noël' => 'decoration-noel', + 'Deebot' => 'ecovacs-deebot', + 'Deezer' => 'deezer', + 'Dell' => 'dell', + 'Dell XPS' => 'dell-xps', + 'Delsey' => 'delsey', + 'Demandes de deals' => 'les-demandes-de-deals', + 'Denon' => 'denon', + 'Dentifrices' => 'dentifrices', + 'Déodorants' => 'deodorants', + 'Désherbants' => 'desherbants', + 'Déshumidificateurs' => 'deshumidificateurs', + 'Désinfectant' => 'desinfectants', + 'Désodorisants & parfums d'intérieur' => 'desodorisants-et-parfums-d-interieur', + 'Destiny' => 'destiny', + 'Destiny 2' => 'destiny-2', + 'Détecteurs de fumée' => 'detecteurs-de-fumee', + 'Detroit: Become Human' => 'detroit-become-human', + 'Deus Ex' => 'deus-ex', + 'Deus Ex: Mankind Divided' => 'deus-ex-mankind-divided', + 'Devil May Cry 5' => 'devil-may-cry-5', + 'Dishonored' => 'dishonored', + 'Dishonored 2' => 'dishonored-2', + 'Disney+' => 'disney-plus', + 'Disneyland Paris' => 'disneyland-paris', + 'Disques durs (internes)' => 'hdd', + 'Disques durs externes' => 'disques-durs-externes', + 'Divers' => 'divers', + 'DJI' => 'dji', + 'DJI Mavic Air 2' => 'dji-mavic-air-2', + 'DJI Mavic Mini' => 'dji-mavic-mini', + 'Dolce Gusto' => 'dolce-gusto', + 'Domotique' => 'smart-home', + 'Doom Eternal' => 'doom-eternal', + 'Dosettes Dolce Gusto' => 'dosettes-dolce-guste', + 'Dosettes Nespresso' => 'dosettes-nespresso', + 'Dosettes Senseo' => 'dosettes-senseo', + 'Dosettes Tassimo' => 'dosettes-tassimo', + 'Dr. Martens' => 'dr-martens', + 'Dragon Age' => 'dragon-age', + 'Dragon Ball' => 'dragon-ball', + 'Dragon Ball FighterZ' => 'dragon-ball-fighterz', + 'Dragon Ball Z: Kakarot' => 'dragon-ball-z-kakarot', + 'Dragon Quest' => 'dragon-quest', + 'Dragon Quest Builders' => 'dragon-quest-builders', + 'Dragon Quest Builders 2' => 'dragon-quest-builders-2', + 'Draisiennes' => 'draisiennes', + 'Draps & housses' => 'draps-et-housses', + 'Dreame V10' => 'xiaomi-dreame-v10', + 'Dreame V11' => 'xiaomi-dreame-v11', + 'Drones' => 'drones', + 'Durex' => 'durex', + 'DVD' => 'dvd', + 'Dying Light' => 'dying-light', + 'Dying Light 2' => 'dying-light-2', + 'Dyson' => 'dyson', + 'Dyson V10' => 'dyson-v10', + 'Dyson V11' => 'dyson-v11', + 'Eastpak' => 'eastpak', + 'Ebooks' => 'ebooks', + 'Écharpes & foulards' => 'echarpes-et-foulards', + 'Éclairage intelligent' => 'smart-light', + 'Écouteurs' => 'ecouteurs', + 'Écouteurs sans fil' => 'ecouteurs-sans-fil', + 'Écouteurs sport' => 'ecouteurs-sport', + 'Ecovacs' => 'ecovacs', + 'Ecovacs Deebot 900' => 'ecovacs-deebot-900', + 'Ecovacs Deebot OZMO 930' => 'ecovacs-deebot-ozmo-930', + 'Écrans' => 'ecrans', + 'Écrans 4K / UHD' => 'ecrans-4k-uhd', + 'Écrans 21" et moins' => 'ecrans-21-pouces-et-moins', + 'Écrans 24"' => 'ecrans-24-pouces', + 'Écrans 27"' => 'ecrans-27-pouces', + 'Écrans 29" et plus' => 'ecrans-29-pouces-et-plus', + 'Écrans Acer' => 'ecrans-acer', + 'Écrans Asus' => 'ecrans-asus', + 'Écrans BenQ' => 'ecrans-benq', + 'Écrans Dell' => 'ecrans-dell', + 'Écrans de projection' => 'ecrans-de-projection', + 'Écrans FreeSync' => 'ecrans-freesync', + 'Écrans gaming' => 'ecrans-gamer', + 'Écrans incurvés' => 'ecrans-incurves', + 'Écrans Philips' => 'ecrans-philips', + 'Écrans Samsung' => 'ecrans-samsung', + 'Électricité (matériel)' => 'electricite', + 'Electrolux' => 'electrolux', + 'Électroménager' => 'electromenager', + 'Embauchoirs' => 'embauchoirs', + 'Enceintes' => 'enceintes', + 'Enceintes Bluetooth' => 'enceintes-bluetooth', + 'Enceintes connectées' => 'enceintes-connectees', + 'Enceintes portables sans fil' => 'enceintes-portables-sans-fil', + 'Énergie' => 'energie', + 'Engrais' => 'engrais', + 'Épicerie & courses' => 'epicerie-courses-supermarches', + 'Épilateurs à lumière pulsée' => 'epilateurs-a-lumiere-pulsee', + 'Épilateurs électriques' => 'epilateurs-electriques', + 'Épilation' => 'epilation', + 'Équipement motard' => 'equipement-motard', + 'Équipement running' => 'equipement-running', + 'Équipement sportif' => 'equipement-sportif', + 'Érotisme' => 'erotisme', + 'Escarpins' => 'escarpins', + 'Événements sportifs' => 'evenements-sportifs', + 'Expositions' => 'expositions', + 'Extracteurs de jus' => 'extracteurs-de-jus', + 'F1 2017' => 'f1-2017', + 'F1 2019' => 'f1-2019', + 'Facom' => 'facom', + 'Fallout' => 'fallout', + 'Fallout 4' => 'fallout-4', + 'Fallout 76' => 'fallout-76', + 'Famille & enfants' => 'famille-enfants', + 'Far Cry' => 'far-cry', + 'Far Cry New Dawn' => 'far-cry-new-dawn', + 'Fards à paupières' => 'fards-a-paupieres', + 'Fast-foods' => 'fast-foods', + 'Fauteuils' => 'fauteuils', + 'Fauteuils gamer' => 'fauteuils-gaming', + 'Fe' => 'fe', + 'Fers à lisser / à friser' => 'fers-a-lisser-a-friser', + 'Fers à repasser' => 'fers-a-repasser', + 'Fers à souder' => 'fers-a-souder', + 'Festivals' => 'festivals', + 'Feutres' => 'feutres', + 'FIFA' => 'fifa', + 'FIFA 17' => 'fifa-17', + 'FIFA 18' => 'fifa-18', + 'FIFA 19' => 'fifa-19', + 'FIFA 20' => 'fifa-20', + 'FIFA 21' => 'fifa-21', + 'Figurines' => 'figurines', + 'Films & Séries' => 'films', + 'Final Fantasy' => 'final-fantasy', + 'Final Fantasy XII' => 'final-fantasy-xii', + 'Finances & Assurances' => 'finances-assurances', + 'fitbit' => 'fitbit', + 'Fitness & yoga' => 'fitness-yoga', + 'Flash' => 'flash', + 'Fluval' => 'fluval', + 'Foires & salons' => 'foires-et-salons', + 'Fonds de teint' => 'fonds-de-teint', + 'Football' => 'football', + 'Forfaits de ski' => 'forfaits-ski', + 'Forfaits mobiles' => 'forfaits-mobiles', + 'Forfaits mobiles et internet' => 'telecommunications', + 'For Honor' => 'for-honor', + 'Formations premiers secours' => 'formations-premiers-secours', + 'Formule 1' => 'formule-1', + 'Fortnite' => 'fortnite', + 'Fortnite: Pack Feu Obscur' => 'fortnite-pack-feu-obscur', + 'Forza' => 'forza', + 'Forza Horizon' => 'forza-horizon', + 'Forza Horizon 3' => 'forza-horizon-3', + 'Forza Horizon 4' => 'forza-horizon-4', + 'Forza Motorsport' => 'forza-motosport', + 'Forza Motorsport 7' => 'forza-motorsport-7', + 'Fossil' => 'fossil', + 'Fournitures scolaires' => 'fournitures-scolaires', + 'Fours' => 'fours', + 'Fours à poser' => 'fours-a-poser', + 'Fours encastrables' => 'fours-encastrables', + 'Friandises pour chat' => 'friandises-pour-chat', + 'Friandises pour chien' => 'friandises-pour-chien', + 'Friskies' => 'friskies', + 'Friteuses' => 'friteuses', + 'Friteuses sans huile' => 'friteuses-sans-huile', + 'Fruits & légumes' => 'fruits-et-legumes', + 'Fujifilm' => 'fujifilm', + 'Funko Pop' => 'funko-pop', + 'FURminator' => 'furminator', + 'Futuroscope' => 'futuroscope', + 'Gamelles' => 'gamelles', + 'Game of Thrones' => 'game-of-thrones', + 'Gaming' => 'le-laboratoire-des-gamers', + 'Gants' => 'gants', + 'Gants moto' => 'gants-moto', + 'Garmin' => 'garmin', + 'Garmin Fenix' => 'garmin-fenix', + 'Garmin Forerunner' => 'garmin-forerunner', + 'Garmin Vivoactive' => 'garmin-vivoactive', + 'Garmin Vivomove' => 'garmin-vivomove', + 'Gâteaux & biscuits' => 'gateaux-et-biscuits', + 'Gears 5' => 'gears-5', + 'Gel hydroalcoolique' => 'gel-hydroalcoolique', + 'Gels douche' => 'gels-douche', + 'Geox' => 'geox', + 'Ghost of Tsushima' => 'ghost-of-tsushima', + 'Gigoteuses' => 'gigoteuses', + 'Gillette Fusion' => 'gillette-fusion', + 'Gillette Mach3' => 'gillette-mach3', + 'Glaces' => 'glaces', + 'Glacières' => 'glacieres', + 'Glisse urbaine' => 'glisse-urbaine', + 'God of War' => 'god-of-war', + 'Google Chromecast' => 'google-chromecast', + 'Google Home' => 'google-home', + 'Google Home Max' => 'google-home-max', + 'Google Home Mini' => 'google-home-mini', + 'Google Nest Hub' => 'google-nest-hub', + 'Google Nest Mini' => 'google-nest-mini', + 'Google Pixel' => 'google-pixel', + 'Google Pixel 2' => 'google-pixel-2', + 'Google Pixel 2 XL' => 'google-pixel-2-xl', + 'Google Pixel 3' => 'google-pixel-3', + 'Google Pixel 3 XL' => 'google-pixel-3-xl', + 'Google Pixel 3a' => 'google-pixel-3a', + 'Google Pixel 4' => 'google-pixel-4', + 'Google Pixel 4 XL' => 'google-pixel-4xl', + 'Google Pixel 4a' => 'google-pixel-4a', + 'Google Pixel 5' => 'google-pixel-5', + 'Google Pixel XL' => 'google-pixel-xl', + 'GoPro' => 'gopro-hero', + 'GoPro Hero 9' => 'gopro-hero-9', + 'Gran Turismo' => 'gran-turismo', + 'Grille-pain' => 'grille-pain', + 'Grossesse & maternité' => 'grossesse-maternite', + 'GTA' => 'gta', + 'GTA V' => 'gta-v', + 'GTX 1060' => 'nvidia-geforce-gtx-1060', + 'GTX 1070' => 'nvidia-geforce-gtx-1070', + 'GTX 1080' => 'nvidia-geforce-gtx-1080', + 'GTX 1080 Ti' => 'nvidia-geforce-gtx-1080-ti', + 'GTX 1650' => 'gtx-1650', + 'GTX 1660' => 'gtx-1660', + 'GTX 1660 Ti' => 'gtx-1660-ti', + 'Guerlain La Petite Robe Noire' => 'guerlain-petite-robe-noire', + 'Guirlandes lumineuses' => 'guirlandes-lumineuses', + 'Guitares' => 'guitares', + 'Gyropodes' => 'gyropodes', + 'Half Life' => 'half-life', + 'Half Life 2' => 'half-life-2', + 'Half Life Alyx' => 'half-life-alyx', + 'Halloween' => 'halloween', + 'Haltères & poids' => 'halteres-et-poids', + 'Hama' => 'hama', + 'Hamacs' => 'hamacs', + 'Hand spinners' => 'hand-spinners', + 'Harnais pour chien' => 'harnais-pour-chien', + 'Harry Potter' => 'harry-potter', + 'Havaianas' => 'havaianas', + 'High-Tech' => 'high-tech', + 'High-tech & informatique' => 'le-laboratoire-high-tech-informatique', + 'Hisense' => 'hisense', + 'Home Cinéma' => 'home-cinema', + 'Honor' => 'honor', + 'Honor 6X' => 'honor-6x', + 'Honor 8' => 'honor-8', + 'Honor 8 Pro' => 'honor-8-pro', + 'Honor 8X' => 'honor-8x', + 'Honor 8X Max' => 'honor-8x-max', + 'Honor 9' => 'honor-9', + 'Honor 10' => 'honor-10', + 'Honor 20' => 'honor-20', + 'Honor 20 Lite' => 'honor-20-lite', + 'Honor 20 Pro' => 'honor-20-pro', + 'Honor Band 5' => 'honor-band-5', + 'Honor MagicBook' => 'honor-magicbook', + 'Honor MagicWatch 2' => 'honor-magicwatch-2', + 'Honor View 20' => 'honor-view-20', + 'Horizon Zero Dawn' => 'horizon-zero-dawn', + 'Hôtels & Hébergements' => 'hotels', + 'Hoverboards' => 'hoverboards', + 'HTC 10' => 'htc-10', + 'HTC Desire' => 'htc-desire', + 'HTC One M9' => 'htc-one-m9', + 'HTC U11' => 'htc-u11', + 'HTC U Play' => 'htc-u-play', + 'HTC U Ultra' => 'htc-u-ultra', + 'HTC Vive' => 'htc-vive', + 'Huawei' => 'huawei', + 'Huawei FreeBuds 3' => 'huawei-freebuds-3', + 'Huawei Mate 9' => 'huawei-mate-9', + 'Huawei Mate 10' => 'huawei-mate-10', + 'Huawei Mate 10 Pro' => 'huawei-mate-10-pro', + 'Huawei Mate 20' => 'huawei-mate-20', + 'Huawei Mate 20 Lite' => 'huawei-mate-20-lite', + 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro', + 'Huawei Mate 20 RS' => 'huawei-mate-20-rs', + 'Huawei Mate 30' => 'huawei-mate-30', + 'Huawei Mate 30 Lite' => 'huawei-mate-30-lite', + 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro', + 'Huawei P8 Lite' => 'huawei-p8-lite', + 'Huawei P9 Lite' => 'huawei-p9-lite', + 'Huawei P10' => 'huawei-p10', + 'Huawei P10 Lite' => 'huawei-p10-lite', + 'Huawei P10 Plus' => 'huawei-p10-plus', + 'Huawei P20' => 'huawei-p20', + 'Huawei P20 Lite' => 'huawei-p20-lite', + 'Huawei P20 Pro' => 'huawei-p20-pro', + 'Huawei P30' => 'huawei-p30', + 'Huawei P30 Lite' => 'huawei-p30-lite', + 'Huawei P30 Pro' => 'huawei-p30-pro', + 'Huawei P40' => 'huawei-p40', + 'Huawei P40 Lite' => 'huawei-p40-lite', + 'Huawei P40 Pro' => 'huawei-p40-pro', + 'Huawei Watch' => 'huawei-watch', + 'Huawei Watch 2' => 'huawei-watch-2', + 'Hubs' => 'hubs', + 'Hugo Boss Bottled' => 'hugo-boss-bottled', + 'Huile moteur' => 'huile-moteur', + 'Hygiène & soins' => 'hygiene-soins', + 'Hygiène de la maison' => 'hygiene-de-la-maison', + 'Hygiène des bébés' => 'hygiene-des-bebes', + 'Hygiène intime' => 'hygiene-intime', + 'iMac' => 'mac-de-bureau', + 'iMac 2021' => 'imac-2021', + 'Image, son, photo' => 'le-laboratoire-audiovisuel', + 'Impressions photo' => 'impressions-photo', + 'Imprimantes' => 'imprimantes', + 'Imprimantes 3D' => 'imprimantes-3d', + 'Imprimantes Brother' => 'imprimantes-brother', + 'Imprimantes Canon' => 'imprimantes-canon', + 'Imprimantes Epson' => 'imprimantes-epson', + 'Imprimantes HP' => 'imprimantes-hp', + 'Imprimantes laser' => 'imprimantes-laser', + 'Imprimantes multifonctions' => 'imprimantes-multifonctions', + 'Informatique' => 'informatique', + 'Instax Mini' => 'instax-mini', + 'Instruments de musique' => 'instruments-de-musique', + 'Intel i5' => 'intel-i5', + 'Intel i7' => 'intel-i7', + 'Intel i9' => 'intel-i9', + 'iPad' => 'apple-ipad', + 'iPad 2019' => 'ipad-2019', + 'iPad 2020' => 'ipad-2020', + 'iPad Air' => 'ipad-air', + 'iPad Air 2019' => 'ipad-air-2019', + 'iPad Air 2020' => 'ipad-air-2020', + 'iPad Mini' => 'apple-ipad-mini', + 'iPad Pro' => 'apple-ipad-pro', + 'iPad Pro 11' => 'ipad-pro-11', + 'iPad Pro 12.9' => 'ipad-pro-12-9', + 'iPad Pro 2020' => 'ipad-pro-2020', + 'iPhone' => 'apple-iphone', + 'iPhone 6' => 'apple-iphone-6', + 'iPhone 7' => 'apple-iphone-7', + 'iPhone 7 Plus' => 'apple-iphone-7-plus', + 'iPhone 8' => 'apple-iphone-8', + 'iPhone 8 Plus' => 'apple-iphone-8-plus', + 'iPhone 11' => 'iphone-11', + 'iPhone 11 Pro' => 'iphone-11-pro', + 'iPhone 11 Pro Max' => 'iphone-11-pro-max', + 'iPhone 12' => 'iphone-12', + 'iPhone 12 Mini' => 'iphone-12-mini', + 'iPhone 12 Pro' => 'iphone-12-pro', + 'iPhone 12 Pro Max' => 'iphone-12-pro-max', + 'iPhone SE' => 'apple-iphone-se', + 'iPhone X' => 'apple-iphone-x', + 'iPhone XR' => 'apple-iphone-xr', + 'iPhone XS' => 'apple-iphone-xs', + 'iPhone XS Max' => 'apple-iphone-xs-max', + 'iRobot Roomba' => 'irobot-roomba', + 'Isolation' => 'isolation', + 'Jabra Elite 75t' => 'jabra-elite-75t', + 'Jabra Elite 85h' => 'jabra-elite-85h', + 'Jabra Elite 85t' => 'jabra-elite-85t', + 'Jabra Elite Active 65t' => 'jabra-elite-active-65t', + 'Jacuzzis' => 'jacuzzis', + 'Jardin' => 'jardin', + 'Jardin & bricolage' => 'jardin-bricolage', + 'Jardinage' => 'entretien-du-jardin', + 'JBL' => 'jbl', + 'JBL Charge 4' => 'jbl-charge-4', + 'JBL Flip' => 'jbl-flip', + 'JBL GO' => 'jbl-go', + 'JBL Xtreme 2' => 'jbl-xtreme-2', + 'Jeans' => 'jeans', + 'Jets dentaires' => 'jets-dentaires', + 'Jeux & jouets' => 'jeux-jouets', + 'Jeux & sports de café' => 'jeux-sports-cafe-bar', + 'Jeux d'adresse' => 'jeux-adresse', + 'Jeux d'apprentissage' => 'jeux-d-apprentissage', + 'Jeux d'eau' => 'jeux-jouets-eau', + 'Jeux d'extérieur' => 'jeux-d-exterieur', + 'Jeux d'imitation' => 'jeux-d-imitation', + 'Jeux de cartes et de plateau' => 'jeux-cartes-plateau-societe', + 'Jeux de construction' => 'jeux-de-construction', + 'Jeux de hasard & paris' => 'jeux-et-paris', + 'Jeux de société' => 'jeux-de-societe', + 'Jeux Nintendo 3DS' => 'jeux-3ds', + 'Jeux Nintendo Switch' => 'jeux-nintendo-switch', + 'Jeux PC' => 'jeux-pc', + 'Jeux PC dématérialisés' => 'jeux-pc-dematerialises', + 'Jeux pour bébés' => 'jeux-pour-bebes', + 'Jeux PS4' => 'jeux-playstation-4', + 'Jeux PS4 dématérialisés' => 'jeux-ps4-dematerialises', + 'Jeux PS5' => 'jeux-playstation-5', + 'Jeux PS5 dématérialisés' => 'jeux-playstation-5-dematerialises', + 'Jeux PS Plus' => 'jeux-ps-plus', + 'Jeux vidéo' => 'jeux-video', + 'Jeux VR' => 'jeux-vr', + 'Jeux Wii U' => 'jeux-wii-u', + 'Jeux Xbox One' => 'jeux-xbox-one', + 'Jeux Xbox One dématérialisés' => 'jeux-xbox-dematerialises', + 'Jeux Xbox Series X' => 'jeux-xbox-series-x', + 'Jeux Xbox with Gold' => 'jeux-xbox-with-gold', + 'Jouets' => 'jouets', + 'Jouets pour chat' => 'jouets-pour-chat', + 'Jouets pour chien' => 'jouets-pour-chien', + 'Journaux numériques' => 'journaux-numeriques', + 'Journaux papier' => 'journaux-papier', + 'Joy-Con' => 'manettes-nintendo-switch-joy-con', + 'Jungle Speed' => 'jungle-speed', + 'Just Cause' => 'just-cause', + 'Just Cause 3' => 'just-cause-3', + 'Just Cause 4' => 'just-cause-4', + 'Kärcher' => 'karcher', + 'Kaspersky' => 'kaspersky', + 'Kinder' => 'kinder', + 'Kindle Oasis' => 'kindle-oasis', + 'Kindle Paperwhite' => 'kindle-paperwhite', + 'Kindle Voyage' => 'kindle-voyage', + 'Kingdom Hearts' => 'kingdom-hearts', + 'Kingdom Hearts 3' => 'kingdom-hearts-3', + 'Kingston HyperX Cloud II' => 'kingston-hyperx-cloud-2', + 'Kits premiers secours' => 'premiers-secours', + 'Kobo' => 'kobo', + 'Kobo Aura 2' => 'kobo-aura-2', + 'Kobo Aura H2o' => 'kobo-aura-h2o', + 'Kobo Aura One' => 'kobo-aura-one', + 'L'annale du destin' => 'l-annale-du-destin', + 'L'ombre de la guerre' => 'l-ombre-de-la-guerre', + 'L'ombre du Mordor' => 'l-ombre-du-mordor', + 'Lacoste' => 'lacoste', + 'Lampadaires' => 'lampadaires', + 'Lampes' => 'lampes', + 'Lampes de table' => 'lampes-de-table', + 'Lampes solaires' => 'lampes-solaires', + 'Lancôme La Vie est Belle' => 'lancome-la-vie-est-belle', + 'Lapeyre' => 'lapeyre', + 'La Terre du Milieu' => 'la-terre-du-milieu', + 'Lavage auto' => 'lavage-auto', + 'Lavazza' => 'lavazza', + 'Lave-linge' => 'lave-linge', + 'Lave-linge frontal' => 'lave-linge-frontal', + 'Lave-linge séchant' => 'lave-linge-sechant', + 'Lave-linge top' => 'lave-linge-top', + 'Lave-vaisselle' => 'lave-vaisselle', + 'Lay-Z-Spa' => 'lay-z-spa', + 'Leasing voiture' => 'leasing-voiture', + 'Le bâton de la vérité' => 'le-baton-de-la-verite', + 'Lecteurs Blu-Ray' => 'lecteurs-blu-ray', + 'Lecteurs CD' => 'lecteurs-cd', + 'Lecteurs DVD' => 'lecteurs-dvd', + 'Lego' => 'lego', + 'Lego Architecture' => 'lego-architecture', + 'Lego Batman' => 'lego-batman', + 'Lego City' => 'lego-city', + 'Lego Creator' => 'lego-creator', + 'Lego Dimensions' => 'lego-dimensions', + 'Lego Duplo' => 'lego-duplo', + 'Lego Friends' => 'lego-friends', + 'Lego Harry Potter' => 'lego-harry-potter', + 'Lego Ideas' => 'lego-ideas', + 'Lego Marvel' => 'lego-marvel', + 'Lego Nexo Knights' => 'lego-nexo-knights', + 'Lego Ninjago' => 'lego-ninjago', + 'Lego Star Wars' => 'lego-star-wars', + 'Lego Technic' => 'lego-technic', + 'Lenovo' => 'lenovo', + 'Lenovo IdeaPad' => 'lenovo-ideapad', + 'Lenovo K6 Note' => 'lenovo-k6-note', + 'Lenovo P8' => 'lenovo-p8', + 'Lenovo Tab 3' => 'lenovo-tab-3', + 'Lenovo Tab 4' => 'lenovo-tab-4', + 'Lenovo ThinkPad' => 'lenovo-thinkpad', + 'Lenovo Yoga' => 'lenovo-yoga', + 'Lenovo Yoga Tab 3' => 'lenovo-yoga-tab-3', + 'Lentilles de contact' => 'lentilles-de-contact', + 'Le Seigneur des anneaux' => 'le-seigneur-des-anneaux', + 'Les Sims' => 'les-sims', + 'Les Sims 4' => 'les-sims-4', + 'Lessive' => 'lessive', + 'Levi's' => 'levi-s', + 'LG' => 'lg', + 'LG G4' => 'lg-g4', + 'LG G5' => 'lg-g5', + 'LG G6' => 'lg-g6', + 'LG OLED TV' => 'lg-oled-tv', + 'LG Q6' => 'lg-q6', + 'LG Q8' => 'lg-q8', + 'Life is Strange' => 'life-is-strange', + 'Linge de maison' => 'linge-de-maison', + 'Lingerie' => 'lingerie', + 'Lingettes désinfectantes' => 'lingettes-desinfectantes', + 'Lingettes pour bébés' => 'lingettes-pour-bebes', + 'Liseuses' => 'liseuses', + 'Litière pour chat' => 'litiere-pour-chat', + 'Lits' => 'lits', + 'Lits pour bébé' => 'lits-pour-bebe', + 'Lits pour enfants' => 'lits-pour-enfants', + 'Little Nightmares' => 'little-nightmares', + 'Livraison de repas' => 'service-de-livraison-de-repas', + 'Livres & littérature' => 'livres-litterature', + 'Livres & Magazines' => 'livres', + 'Livres audio' => 'livres-audio', + 'Livres photo' => 'livres-photo', + 'Location de voiture' => 'location-de-voiture', + 'Logiciels' => 'logiciels', + 'Logiciels de sécurité' => 'logiciels-de-securite', + 'Logiciels Microsoft' => 'logiciels-microsoft', + 'Logitech' => 'logitech', + 'Logitech G502' => 'logitech-g502', + 'Logitech G703' => 'logitech-g703', + 'Logitech G Pro X' => 'logitech-g-pro-x', + 'Logitech Harmony' => 'logitech-harmony', + 'Logitech MX Master' => 'logitech-mx-master', + 'Logitech MX Master 2S' => 'logitech-mx-master-2s', + 'Loisirs créatifs' => 'loisirs-creatifs', + 'Lolita Lempicka' => 'lolita-lempicka-premier-parfum', + 'Loup-Garou' => 'loup-garou', + 'Lubrifiants' => 'lubrifiants', + 'Luges' => 'luges', + 'Luigi's Mansion 3' => 'luigi-mansion-3', + 'Luminaires' => 'luminaires', + 'Lunettes de natation' => 'lunettes-de-natation', + 'Lunettes de soleil' => 'lunettes-de-soleil', + 'M&M's' => 'metm-s', + 'MacBook' => 'macbook', + 'MacBook Air' => 'apple-macbook-air', + 'MacBook Pro' => 'apple-macbook-pro', + 'MacBook Pro 13' => 'macbook-pro-13', + 'MacBook Pro 15' => 'macbook-pro-15', + 'MacBook Pro 16' => 'macbook-pro-16', + 'Machines à café à dosettes' => 'machines-a-cafe-a-dosettes', + 'Machines à café en grain' => 'machines-a-cafe-en-grain', + 'Machines à coudre' => 'machines-a-coudre', + 'Machines à pain' => 'machines-a-pain', + 'Machines de sport' => 'machines-sport', + 'Machines Dolce Gusto' => 'machines-dolce-gusto', + 'Machines Nespresso' => 'machines-nespresso', + 'Machines Senseo' => 'machines-senseo', + 'Machines Tassimo' => 'machines-tassimo', + 'Mac mini' => 'mac-mini', + 'Madden NFL 20' => 'madden-nfl-20', + 'Magasins d'usine' => 'magasins-usine', + 'Magazines' => 'magazines', + 'Maillots de bain' => 'maillots-de-bain', + 'Maillots de football' => 'maillots-de-football', + 'Maison & Habitat' => 'maison-habitat', + 'Maisons de poupées' => 'maisons-poupees', + 'Makita' => 'makita', + 'Manettes' => 'manettes-accessoires-consoles', + 'Manettes DualSense' => 'manettes-playstation-5', + 'Manettes Nintendo Switch' => 'manettes-nintendo-switch', + 'Manettes Nintendo Switch Pro' => 'manettes-nintendo-switch-pro', + 'Manettes PlayStation 4' => 'manettes-playstation-4', + 'Manettes Xbox' => 'manettes-xbox', + 'Manettes Xbox One' => 'manettes-xbox-one', + 'Manettes Xbox One Elite' => 'manettes-xbox-one-elite', + 'Manettes Xbox Series X' => 'manettes-xbox-series-x', + 'Manix' => 'manix', + 'Manteaux' => 'manteaux', + 'Maquillage' => 'maquillage', + 'Marchands et leurs offres' => 'vos-avisdemandes-sur-les-marchands-et-leurs-offres', + 'Mario & Sonic aux Jeux Olympiques de Tokyo 2020' => 'mario-sonic-jeux-olympiques-tokyo-2020', + 'Mario Kart' => 'mario-kart', + 'Marques' => 'marques', + 'Marteaux & maillets' => 'marteaux-et-maillets', + 'Marvel's Avengers' => 'marvels-avengers', + 'Mascara' => 'mascara', + 'Masques cheveux' => 'masques-cheveux', + 'Masques de protection' => 'masques-de-protection-respiratoire', + 'Masques de ski' => 'masques-de-ski', + 'Mass Effect' => 'mass-effect', + 'Mass Effect: Andromeda' => 'mass-effect-andromeda', + 'Matchs de football' => 'matchs-de-football', + 'Matelas' => 'matelas', + 'Matelas gonflables' => 'matelas-gonflables', + 'Matériaux de construction' => 'materiaux-de-construction', + 'Matériel de ski' => 'materiel-de-ski', + 'Medion' => 'medion', + 'Metro' => 'metro', + 'Metro 2033' => 'metro-2033', + 'Metro Exodus' => 'metro-exodus', + 'Meubles pour aquarium' => 'meubles-pour-aquarium', + 'Meubles pour chat' => 'meubles-pour-chat', + 'Meubles salle de bain' => 'salle-de-bain', + 'Micro-casques gaming' => 'micro-casques-gaming', + 'Micro-ondes' => 'micro-ondes', + 'Microphones' => 'microphones', + 'Micro SD' => 'micro-sd', + 'Microsoft Flight Simulator' => 'microsoft-flight-simulator', + 'Microsoft Office' => 'microsoft-office', + 'Microsoft Surface Book' => 'microsoft-surface-book', + 'Microsoft Surface Pro 6' => 'microsoft-surface-pro-6', + 'Microsoft Surface Pro 7' => 'microsoft-surface-pro-7', + 'Miele' => 'miele', + 'Minecraft' => 'minecraft', + 'Mini PC' => 'mini-pc', + 'Mini réfrigérateurs' => 'mini-refrigerateurs', + 'Miroirs' => 'miroirs', + 'Mixeurs & Blenders' => 'mixeurs-blenders', + 'Mixeurs plongeants' => 'mixeur-plongeant', + 'Mobilier' => 'mobilier', + 'Mobilier de bureau' => 'fournitures-de-bureau', + 'Mobilier de jardin' => 'mobilier-jardin', + 'Mobilier de salon' => 'mobilier-salon', + 'Mobvoi Ticwatch' => 'mobvoi-ticwatch', + 'Mode' => 'mode', + 'Mode & accessoires' => 'mode-accessoires', + 'Mode & beauté' => 'le-laboratoire-de-la-mode-beaute', + 'Mode enfants' => 'mode-enfants', + 'Mode femme' => 'mode-femme', + 'Mode homme' => 'mode-homme', + 'Modélisme' => 'modelisme', + 'Monopoly' => 'monopoly', + 'Montage PC' => 'montage-pc', + 'Montre connectée Amazfit' => 'montres-connectees-amazfit', + 'Montre connectée Garmin' => 'montres-connectees-garmin', + 'Montre connectée Honor' => 'montres-connectees-honor', + 'Montre connectée Samsung' => 'smartwatch-samsung', + 'Montres' => 'montres', + 'Montres connectées' => 'smartwatch', + 'Mortal Kombat' => 'mortal-kombat', + 'Mortal Kombat 11' => 'mortal-kombat-11', + 'Moto C Plus' => 'moto-c-plus', + 'Moto E4' => 'moto-e4', + 'Moto G5' => 'moto-g5', + 'Moto G5 Plus' => 'moto-g5-plus', + 'Moto G5S' => 'moto-g5s', + 'Moto G5S Plus' => 'moto-g5s-plus', + 'Moto G6' => 'moto-g6', + 'Moto G6 Play' => 'moto-g6-play', + 'Moto G6 Plus' => 'moto-g6-plus', + 'Moto G7 Play' => 'moto-g7-play', + 'Moto G7 Plus' => 'moto-g7-plus', + 'Moto G7 Power' => 'moto-g7-power', + 'Moto M' => 'moto-m', + 'Motorola' => 'motorola', + 'Moto Z2' => 'moto-z2', + 'Moto Z2 Force' => 'moto-z2-force', + 'Moto Z2 Play' => 'moto-z2-play', + 'Moto Z3' => 'moto-z3', + 'Moto Z3 Play' => 'moto-z3-play', + 'Moulinex' => 'moulinex', + 'Mousses à raser' => 'mousses-a-raser', + 'MSI' => 'msi', + 'Musées' => 'musees', + 'Musique' => 'musique', + 'NAS' => 'nas', + 'Natation' => 'natation', + 'Nature & sports d'hiver' => 'nature-sports-hiver', + 'Navigation' => 'navigation', + 'NBA 2K' => 'nba-2k', + 'NBA 2K20' => 'nba-2k20', + 'NERF' => 'nerf', + 'Nescafé' => 'nescafe', + 'Nespresso' => 'nespresso', + 'Nest Learning Thermostat' => 'nest-learning-thermostat', + 'Nest Protect' => 'nest-protect', + 'Netflix' => 'netflix', + 'Nettoyeurs haute-pression' => 'nettoyeurs-haute-pression', + 'Nettoyeurs haute pression Karcher' => 'nettoyeurs-haute-pression-karcher', + 'Nettoyeurs vapeur' => 'nettoyeurs-vapeur', + 'New Balance' => 'new-balance', + 'New Balance 574' => 'new-balance-574', + 'NHL 20' => 'nhl-20', + 'Nike' => 'nike', + 'Nike Air Force' => 'nike-air-force', + 'Nike Air Jordan' => 'nike-air-jordan', + 'Nike Air Max' => 'nike-air-max', + 'Nike Air Max 90' => 'nike-air-max-90', + 'Nike Air Max 200' => 'nike-air-max-200', + 'Nike Air Max 270' => 'nike-air-max-270', + 'Nike Air Max 720' => 'nike-air-max-720', + 'Nike Free' => 'nike-free', + 'Nike Huarache' => 'nike-huarache', + 'Nike Roshe Run' => 'nike-roshe-run', + 'Nikon' => 'nikon', + 'Nikon D3500' => 'nikon-d3500', + 'Ni no Kuni' => 'ni-no-kuni', + 'Ni No Kuni: Wrath of the White Witch' => 'ni-no-kuni-wrath-white-witch', + 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-2-revenant-kingdom', + 'Nintendo' => 'nintendo', + 'Nioh' => 'nioh', + 'Nivea' => 'nivea', + 'Nocciolata' => 'nocciolata', + 'Nokia' => 'nokia', + 'Nokia 5' => 'nokia-5', + 'Nokia 6' => 'nokia-6', + 'Nokia 8' => 'nokia-8', + 'Nokia 9 PureView' => 'nokia-9-pureview', + 'Nougats' => 'nougats', + 'Nourriture pour chat' => 'nourriture-pour-chat', + 'Nourriture pour chien' => 'nourriture-pour-chien', + 'Nourriture pour poissons' => 'nourriture-pour-poissons', + 'Nutella' => 'nutella', + 'Nvidia' => 'nvidia', + 'Nvidia GeForce' => 'nvidia-geforce', + 'Nvidia Shield' => 'nvidia-shield', + 'Objectifs' => 'objectifs', + 'Objets connectés' => 'objets-connectes', + 'Oculus Go' => 'oculus-go', + 'Oculus Rift' => 'oculus-rift', + 'Oiseaux' => 'oiseaux', + 'One Piece: Pirate Warriors' => 'one-piece-pirate-warriors', + 'OnePlus 5' => 'oneplus-5', + 'OnePlus 5T' => 'oneplus-5t', + 'OnePlus 6' => 'oneplus-6', + 'OnePlus 6T' => 'oneplus-6t', + 'OnePlus 7' => 'oneplus-7', + 'OnePlus 7 Pro' => 'oneplus-7-pro', + 'OnePlus 7T' => 'oneplus-7t', + 'OnePlus 7T Pro' => 'oneplus-7t-pro', + 'OnePlus 8' => 'oneplus-8', + 'OnePlus 8 Pro' => 'oneplus-8-pro', + 'OnePlus 8T' => 'oneplus-8t', + 'OnePlus 9' => 'oneplus-9', + 'OnePlus 9 Pro' => 'oneplus-9-pro', + 'OnePlus Nord' => 'oneplus-nord', + 'Onkyo' => 'onkyo', + 'Oppo Find X2 Lite' => 'oppo-find-x2-lite', + 'Oppo Find X2 Neo' => 'oppo-find-x2-neo', + 'Oppo Find X2 Pro' => 'oppo-find-x2-pro', + 'Oppo Reno' => 'oppo-reno', + 'Optique' => 'optique', + 'Oral-B' => 'oral-b', + 'Ordinateurs de bureau' => 'ordinateurs-de-bureau', + 'Ordinateurs tout-en-un' => 'pc-de-bureau-complets', + 'Oreillers' => 'oreillers', + 'Osram Smart+' => 'osram-smart-plus', + 'Outillage' => 'outillage', + 'Outils à main' => 'outils-main', + 'Outils de jardinage' => 'outils-de-jardinage', + 'Outils électriques' => 'outils-electriques', + 'Overwatch' => 'overwatch', + 'Packs clavier-souris' => 'packs-clavier-souris', + 'Packs consoles' => 'packs-consoles', + 'Paco Rabanne Invictus' => 'paco-rabanne-invictus', + 'Paco Rabanne Lady Million' => 'paco-rabanne-lady-million', + 'Paco Rabanne One Million' => 'paco-rabanne-one-million', + 'Pain & pâtisseries' => 'pain-patisseries', + 'Pampers' => 'pampers', + 'Panasonic' => 'panasonic', + 'Panasonic Lumix' => 'panasonic-lumix', + 'Panier Plus' => 'panier-plus', + 'Pantalons' => 'pantalons', + 'Papeterie' => 'papeterie', + 'Papeterie et bureautique' => 'papeterie-bureautique', + 'Papier bureautique' => 'papier-bureautique', + 'Papier peint' => 'papier-peint', + 'Papier toilette' => 'papier-toilette', + 'Parapharmacie' => 'parapharmacie', + 'Parasols' => 'parasols', + 'Parc Astérix' => 'parc-asterix', + 'Parcs d'attraction' => 'parcs-d-attraction', + 'Parfums' => 'parfums', + 'Parfums femme' => 'parfums-femme', + 'Parfums homme' => 'parfums-homme', + 'Parkas' => 'parkas', + 'Parrot' => 'parrot', + 'Partitions' => 'partitions', + 'Pâtée pour chat' => 'patee-pour-chat', + 'Pâtée pour chien' => 'patee-pour-chien', + 'Pâtes à tartiner' => 'pates-tartiner', + 'Pâtisserie' => 'patisserie', + 'PC Barebones' => 'pc-barebones', + 'PC gamer fixe' => 'pc-gamer-complets', + 'PC gaming' => 'pc-gaming', + 'PC hybrides' => 'hybrides', + 'PC Microsoft Surface' => 'pc-microsoft-surface', + 'PC portables' => 'pc-portables', + 'PC portables Acer' => 'pc-portables-acer', + 'PC portables ASUS' => 'pc-portables-asus', + 'PC portables Dell' => 'pc-portables-dell', + 'PC portables gaming' => 'portables-gamer', + 'PC portables Honor' => 'pc-portables-honor', + 'PC portables HP' => 'pc-portables-hp', + 'PC portables Lenovo' => 'pc-portables-lenovo', + 'PC portables Lenovo Legion' => 'lenovo-legion', + 'PC portables Xiaomi' => 'pc-portables-xiaomi', + 'Pêche' => 'peche', + 'Peignes & brosses à cheveux' => 'peignes-et-brosses-a-cheveux', + 'Peignoirs' => 'peignoirs', + 'Peintures' => 'peintures', + 'Peluches' => 'peluches', + 'Perceuses' => 'perceuses', + 'Périphériques PC' => 'peripheriques-pc', + 'Persona 5' => 'persona-5', + 'Persona 5 Royal' => 'persona-5-royal', + 'PES' => 'pro-evolution-soccer', + 'Pèse-personnes' => 'pese-personnes', + 'Petites voitures' => 'petites-voitures', + 'Pharmacie & parapharmacie' => 'pharmacie-parapharmacie', + 'Philips' => 'philips', + 'Philips Hue' => 'philips-hue', + 'Philips Hue E14' => 'philips-hue-e14', + 'Philips Hue E27' => 'philips-hue-e27', + 'Philips Hue Go' => 'philips-hue-go', + 'Philips Hue GU10' => 'philips-hue-gu10', + 'Philips Hue LightStrip' => 'philips-hue-lightstrip', + 'Philips Hue Play HDMI Sync Box' => 'philips-hue-play-hdmi-sync-box', + 'Philips Lumea' => 'philips-lumea', + 'Philips OneBlade' => 'philips-one-blade', + 'Philips Sonicare' => 'philips-sonicare', + 'Photo' => 'photo', + 'Pièces auto' => 'pieces-auto', + 'Pièces moto' => 'pieces-moto', + 'Pièces vélo' => 'pieces-velo', + 'Piles' => 'piles', + 'Piles rechargeables' => 'piles-rechargeables', + 'Pinceaux maquillage' => 'pinceaux-maquillage', + 'Pinces' => 'pinces', + 'Ping-pong' => 'ping-pong', + 'Pioneer' => 'pioneer', + 'Piscines' => 'piscines', + 'Pizza' => 'pizza', + 'Places de cinéma' => 'places-de-cinema', + 'Plafonniers' => 'plafonniers', + 'Plancha' => 'planchas', + 'Plantes & semis' => 'plantes', + 'Plaques de cuisson' => 'plaques-de-cuisson', + 'Platines vinyle' => 'platines-vinyle', + 'Plats & moules' => 'plats-et-moules', + 'PlayerUnknown's Battlegrounds' => 'playerunknown-s-battleground', + 'Playmobil' => 'playmobil', + 'PlayStation' => 'playstation', + 'Pneus' => 'pneus', + 'PocketBook' => 'pocketbook', + 'PocketBook Touch Lux 3' => 'pocketbook-touch-lux-3', + 'POCO F2 Pro' => 'poco-f2-pro', + 'POCO F3' => 'poco-f3', + 'POCO M3' => 'poco-m3', + 'POCO X3' => 'poco-x3', + 'POCO X3 Pro' => 'poco-x3-pro', + 'Poêles' => 'poeles', + 'Pokémon' => 'pokemon', + 'Pokémon: Let's Go' => 'pokemon-letsgo', + 'Pokémon Épée et Bouclier' => 'pokemon-epee-bouclier', + 'Pokémon Tournament' => 'pokemon-tournament', + 'Pokémon Ultra Sun / Moon' => 'pokemon-ultra-sun-moon', + 'Polaroid' => 'polaroid', + 'Polos' => 'polos', + 'Pompes à vélo' => 'pompes-velo', + 'Porte-bébé' => 'porte-bebe', + 'Portefeuilles' => 'portefeuilles', + 'Posters' => 'posters', + 'Potager' => 'potager', + 'Pots & cache-pots' => 'pots-et-cache-pots', + 'Poubelles' => 'poubelles', + 'Poulaillers' => 'poulaillers', + 'Poupées' => 'poupees', + 'Poussettes' => 'poussettes-bebe', + 'Présentez-vous !' => 'mieux-se-connaitre-presentez-vous', + 'Préservatifs' => 'preservatifs', + 'Princesse Tam-Tam' => 'princesse-tam-tam', + 'Prises connectées' => 'prises-connectees', + 'Processeurs' => 'processeurs', + 'Produit pour lentilles' => 'produit-pour-lentilles', + 'Produits de massage' => 'produits-de-massage', + 'Produits frais' => 'produits-frais', + 'Produits reconditionnés' => 'reconditionne', + 'Produits vétérinaires' => 'produits-veterinaires', + 'Programme d'Entraînement Cérébral du Dr. Kawashima' => 'dr-kawashima-brain-training', + 'Project Cars 2' => 'project-cars-2', + 'Protection de la maison' => 'protection-de-la-maison', + 'Protections intimes' => 'protections-intimes', + 'Protection solaire' => 'protection-solaire', + 'Puériculture' => 'puericulture', + 'Pulls' => 'pulls', + 'Puma' => 'puma', + 'Purificateurs d'air' => 'purificateurs-d-air', + 'Purina' => 'purina', + 'Puzzles' => 'puzzles', + 'Pyjamas' => 'pyjamas', + 'Pyjamas & chemises de nuit' => 'pyjamas-chemises-de-nuit', + 'Pyjamas pour bébés' => 'pyjamas-pour-bebes', + 'Qobuz' => 'qobuz', + 'Quiksilver' => 'quiksilver', + 'Radiateurs' => 'radiateurs', + 'Ralph Lauren' => 'ralph-lauren', + 'RAM' => 'ram', + 'Randonnée' => 'randonnee', + 'Raquettes de ping-pong' => 'raquettes-de-ping-pong', + 'Raquettes de tennis' => 'raquettes-de-tennis', + 'Rasage et épilation' => 'rasage-epilation', + 'Rasoirs Braun' => 'rasoirs-braun', + 'Rasoirs électriques' => 'rasoirs-electriques', + 'Rasoirs Gillette' => 'gillette', + 'Rasoirs manuels' => 'rasoirs-manuels', + 'Rasoirs Philips' => 'rasoirs-philips', + 'Rasoirs Wilkinson' => 'rasoirs-wilkinson-sword', + 'Raspberry Pi' => 'raspberry-pi', + 'Ray-Ban' => 'ray-ban', + 'Razer' => 'razer', + 'Razer DeathAdder' => 'razer-deathadder', + 'Realme 5 Pro' => 'realme-5-pro', + 'Realme X2 Pro' => 'realme-x2-pro', + 'Red Dead Redemption' => 'red-dead-redemption', + 'Red Dead Redemption 2' => 'red-dead-redemption-2', + 'Réductions étudiants & jeunes' => 'reductions-etudiants-et-jeunes', + 'Reebok' => 'reebok', + 'Reebok Club C' => 'reebok-club-c', + 'Réfrigérateurs' => 'refrigerateurs', + 'Réfrigérateurs américains' => 'refrigerateurs-americains', + 'Refroidissement PC' => 'refroidissement-pc', + 'Réhausseurs' => 'rehausseurs', + 'Remington' => 'remington', + 'Repas de fête' => 'repas-fete-reveillon', + 'Repassage' => 'repassage', + 'Répéteurs' => 'repeteurs', + 'Réseau' => 'reseau', + 'Resident Evil' => 'resident-evil', + 'Resident Evil 3' => 'resident-evil-3', + 'Resident Evil 7' => 'resident-evil-7', + 'Restaurants' => 'restaurants', + 'Revêtements de sols' => 'revetements-de-sols', + 'Revêtements muraux' => 'revetements-muraux', + 'Rhum' => 'rhum', + 'Richelieus' => 'richelieus', + 'Ring Fit Adventure' => 'ring-fit-adventure', + 'Risk' => 'risk', + 'Robes & jupes' => 'robes-et-jupes', + 'Roborock' => 'roborock', + 'Roborock S5 MAX' => 'roborock-s5-max', + 'Roborock S6' => 'roborock-s6', + 'Robots cuiseurs' => 'robots-cuiseurs', + 'Robots ménagers' => 'robots-menagers', + 'Robot tondeuse' => 'robot-tondeuse', + 'ROCCAT' => 'roccat', + 'Rollers' => 'rollers', + 'Rouges à lèvres' => 'rouges-a-levres', + 'Routeurs' => 'routeurs', + 'Rowenta' => 'rowenta', + 'Royal Canin' => 'royal-canin', + 'RTX 2060' => 'rtx-2060', + 'RTX 2070' => 'rtx-2070', + 'RTX 2080' => 'rtx-2080', + 'RTX 2080 Ti' => 'rtx-2080-ti', + 'RTX 3070' => 'rtx-3070', + 'RTX 3080' => 'rtx-3080', + 'RTX 3090' => 'rtx-3090', + 'RX 480' => 'rx-480', + 'RX 580' => 'rx-580', + 'RX 590' => 'radeon-rx-590', + 'RX Vega 56' => 'rx-vega-56', + 'RX Vega 64' => 'rx-vega-64', + 'Sacs à déjections' => 'sacs-a-dejections', + 'Sacs à dos' => 'sacs-a-dos', + 'Sacs à langer' => 'sacs-a-langer', + 'Sacs à main' => 'sacs-a-main', + 'Sacs bandoulière' => 'sacs-bandouliere', + 'Sacs de couchage' => 'sacs-de-couchage', + 'Sacs de randonnée' => 'sacs-de-randonnee', + 'Sacs de sport' => 'sacs-de-sport', + 'Sacs de voyage' => 'sacs-de-voyage', + 'Salle à manger' => 'salle-manger', + 'Samsonite' => 'samsonite', + 'Samsung' => 'samsung', + 'Samsung Galaxy A5' => 'samsung-galaxy-a5', + 'Samsung Galaxy A50' => 'samsung-galaxy-a50', + 'Samsung Galaxy A51' => 'samsung-galaxy-a51', + 'Samsung Galaxy A51 5G' => 'samsung-galaxy-a51-5g', + 'Samsung Galaxy A70' => 'samsung-galaxy-a70', + 'Samsung Galaxy A80' => 'samsung-galaxy-a80', + 'Samsung Galaxy Buds' => 'samsung-galaxy-buds', + 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus', + 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live', + 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro', + 'Samsung Galaxy Fold' => 'samsung-galaxy-fold', + 'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8', + 'Samsung Galaxy Note 9' => 'samsung-galaxy-note-9', + 'Samsung Galaxy Note 10' => 'samsung-galaxy-note-10', + 'Samsung Galaxy Note 10 Lite' => 'samsung-galaxy-note-10-lite', + 'Samsung Galaxy Note 10 Plus' => 'samsung-galaxy-note-10-plus', + 'Samsung Galaxy Note20' => 'samsung-galaxy-note-20', + 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note-20-ultra', + 'Samsung Galaxy S7' => 'samsung-galaxy-s7', + 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge', + 'Samsung Galaxy S8' => 'samsung-galaxy-s8', + 'Samsung Galaxy S8+' => 'samsung-galaxy-s8plus', + 'Samsung Galaxy S9' => 'samsung-galaxy-s9', + 'Samsung Galaxy S9 Plus' => 'samsung-galaxy-s9-plus', + 'Samsung Galaxy S10' => 'samsung-galaxy-s10', + 'Samsung Galaxy S10 Lite' => 'samsung-galaxy-s10-lite', + 'Samsung Galaxy S10+' => 'samsung-galaxy-s10-plus', + 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e', + 'Samsung Galaxy S20' => 'samsung-galaxy-s20', + 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe', + 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra', + 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus', + 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g', + 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g', + 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g', + 'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a', + 'Samsung Galaxy Tab S2' => 'samsung-galaxy-tab-s2', + 'Samsung Galaxy Tab S3' => 'samsung-galaxy-tab-s3', + 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4', + 'Samsung Galaxy Tab S5e' => 'samsung-galaxy-tab-s5e', + 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6', + 'Samsung Galaxy Tab S7' => 'samsung-galaxy-tab-s7', + 'Samsung Galaxy Watch' => 'samsung-galaxy-watch', + 'Samsung Galaxy Watch3' => 'samsung-galaxy-watch-3', + 'Samsung Galaxy Watch Active 2' => 'samsung-galaxy-watch-active2', + 'Samsung Galaxy Z Flip' => 'galaxy-z-flip', + 'Samsung Gear' => 'samsung-gear', + 'Samsung Gear S3' => 'samsung-gear-s3', + 'Samsung Gear VR' => 'samsung-gear-vr', + 'Sandales' => 'sandales', + 'SanDisk' => 'sandisk', + 'Sanitaires et robinetterie' => 'sanitaires-robinetterie', + 'Santé & Cosmétiques' => 'sante-et-cosmetiques', + 'Sapins de Noël' => 'sapins-noel', + 'Savons' => 'savons', + 'Scanners' => 'scanners', + 'Scanners A3' => 'scanners-a3', + 'Scanners A4' => 'scanners-a4', + 'Scies' => 'scies', + 'Scooters' => 'scooters', + 'Seagate' => 'seagate', + 'Sécateurs' => 'secateurs', + 'Sèche-cheveux' => 'seche-cheveux', + 'Sèche-linge' => 'seche-linge', + 'Seiko' => 'seiko', + 'Séjours' => 'sejours', + 'Sekiro: Shadows Die Twice' => 'sekiro', + 'Semis & graines' => 'semis-et-graines', + 'Sennheiser' => 'sennheiser', + 'Senseo' => 'senseo', + 'Séries TV' => 'series-tv', + 'Service & réparation auto-moto' => 'service-reparation-auto-moto', + 'Services' => 'services-divers', + 'Services auto' => 'services-auto', + 'Services de livraison' => 'services-livraisons', + 'Services moto' => 'services-moto', + 'Services photo' => 'services-photo', + 'Serviettes' => 'serviettes', + 'Serviettes hygiéniques' => 'serviettes-hygieniques', + 'Sextoys' => 'sextoys', + 'Shadow of the Colossus' => 'shadow-of-the-colossus', + 'Shadow of the Tomb Raider' => 'shadow-tomb-raider', + 'Shalimar' => 'shalimar', + 'Shampooings & soins' => 'shampooings-et-soins', + 'Shenmue' => 'shenmue', + 'Shenmue I & II' => 'shenmue-i-ii', + 'Shenmue III' => 'shenmue-iii', + 'Shorts' => 'shorts', + 'Shorts de bain' => 'shorts-de-bain', + 'Sièges auto' => 'sieges-auto', + 'Siemens' => 'siemens', + 'Skates & longboards' => 'skates-et-longboards', + 'Skechers' => 'sketchers', + 'Ski' => 'ski', + 'Skyrim' => 'skyrim', + 'Slips & boxers' => 'slips-et-boxers', + 'Smartphones' => 'smartphones', + 'Smartphones à moins de 100€' => 'smartphones-moins-de-100', + 'Smartphones à moins de 200€' => 'smartphones-moins-de-200', + 'Smartphones Android' => 'smartphones-android', + 'Smartphones Asus' => 'smartphones-asus', + 'Smartphones Google' => 'smartphones-google', + 'Smartphones Honor' => 'smartphones-honor', + 'Smartphones HTC' => 'smartphones-htc', + 'Smartphones Huawei' => 'smartphones-huawei', + 'Smartphones Lenovo Motorola' => 'smartphones-lenovo-motorola', + 'Smartphones LG' => 'smartphones-lg', + 'Smartphones Nokia' => 'smartphones-nokia', + 'Smartphones OnePlus' => 'smartphones-oneplus', + 'Smartphones Oppo' => 'smartphones-oppo', + 'Smartphones Realme' => 'smartphones-realme', + 'Smartphones Samsung' => 'smartphones-samsung', + 'Smartphones Sony' => 'smartphones-sony', + 'Smartphones Xiaomi' => 'smartphones-xiaomi', + 'Smartphones ZTE' => 'smartphones-zte', + 'Smart TV' => 'smart-tv', + 'Sneakers' => 'sneakers', + 'SodaStream' => 'sodastream', + 'Sofas gonflable' => 'sofas-gonflable', + 'Soin barbe et rasage' => 'soin-barbe-rasage', + 'Soin de la peau' => 'soin-peau', + 'Soin des cheveux' => 'soin-des-cheveux', + 'Soin des ongles' => 'soin-ongles', + 'Soins dentaires' => 'soins-dentaires', + 'Sonos' => 'sonos', + 'Sonos Beam' => 'sonos-beam', + 'Sonos Move' => 'sonos-move', + 'Sonos One' => 'sonos-one', + 'Sonos PLAY:1' => 'sonos-play-1', + 'Sonos PLAY:3' => 'sonos-play-3', + 'Sonos PLAY:5' => 'sonos-play-5', + 'Sonos PLAYBAR' => 'sonos-playbar', + 'Sony' => 'sony', + 'Sony PlayStation VR' => 'sony-playstation-vr', + 'Sony Pulse 3D sans fil' => 'casque-audio-sony-pulse-3d', + 'Sony WF-1000XM3' => 'sony-wf-1000xm3', + 'Sony WH-1000XM3' => 'sony-wh-1000xm3', + 'Sony WH-1000XM4' => 'sony-wh-1000xm4', + 'Sony Xperia XA1' => 'sony-xperia-xa1', + 'Sony Xperia X Compact' => 'sony-xperia-x-compact', + 'Sony Xperia XZ1' => 'sony-xperia-xz1', + 'Sony Xperia XZ1 Compact' => 'sony-xperia-xz1-compact', + 'Sony Xperia XZ Premium' => 'sony-xperia-xz-premium', + 'Sony Xperia Z3' => 'sony-xperia-z3', + 'Soulcalibur' => 'soulcalibur', + 'Souris' => 'souris', + 'Souris gamer' => 'souris-gamer', + 'Souris Logitech' => 'souris-logitech', + 'Souris sans fil' => 'souris-sans-fil', + 'Sous-vêtements' => 'sous-vetements', + 'Sous-vêtements de sport' => 'sous-vetements-de-sport', + 'South Park' => 'south-park', + 'Soutiens-gorge' => 'soutiens-gorge', + 'Spas' => 'spa', + 'Spectacles' => 'spectacles', + 'Spectacles & Billetterie' => 'sorties', + 'Spectacles comiques' => 'spectacles-comiques', + 'Spectacles pour enfants' => 'spectacles-pour-enfants', + 'Sports & plein air' => 'sports-plein-air', + 'Sports collectifs' => 'sports-collectifs', + 'Sports nautiques' => 'sports-nautiques', + 'Sportswear' => 'sportswear', + 'Spotify' => 'spotify', + 'SSD' => 'ssd', + 'Star Wars: Jedi Fallen Order' => 'star-wars-jedi-fallen-order', + 'Star Wars: Squadrons' => 'star-wars-squadrons', + 'Star Wars Battlefront' => 'star-wars-battlefront', + 'Stations météo' => 'stations-meteo', + 'Stickers muraux' => 'stickers-muraux', + 'Stihl' => 'stihl', + 'Stockage externe' => 'stockage', + 'Streaming' => 'streaming', + 'Streaming musical' => 'streaming-musical', + 'Streaming vidéo' => 'streaming-video', + 'Stylos' => 'stylos', + 'Sucettes' => 'sucettes', + 'Super Mario' => 'super-mario', + 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars', + 'Super Mario Maker 2' => 'super-mario-maker-2', + 'Super Mario Party' => 'super-mario-party', + 'Super Smash Bros. Ultimate' => 'super-smash-bros-ultimate', + 'Support GPS & smartphone' => 'support-gps-et-smartphone', + 'Supports TV' => 'supports-tv', + 'Surface Pro 4' => 'surface-pro-4', + 'Surgelés' => 'surgeles', + 'Surveillance' => 'surveillance', + 'Suspensions' => 'suspensions', + 'Swatch' => 'swatch', + 'Switch réseau' => 'switch-reseau', + 'Systèmes d'exploitation' => 'systemes-d-exploitation', + 'Systèmes multiroom' => 'systemes-multiroom', + 'T-shirts' => 't-shirts', + 'Tables' => 'tables', + 'Tables à langer' => 'tables-a-langer', + 'Tables à repasser' => 'tables-a-repasser', + 'Tables basses' => 'tables-basses', + 'Tables de camping' => 'tables-de-camping', + 'Tables de mixage' => 'tables-de-mixage', + 'Tables de ping-pong' => 'tables-ping-pong', + 'Tablettes' => 'tablettes', + 'Tablettes graphiques' => 'tablettes-graphiques', + 'Tablettes graphiques Huion' => 'huion', + 'Tablettes graphiques Wacom' => 'wacom', + 'Tablettes Huawei' => 'tablettes-huawei', + 'Tablettes Lenovo' => 'tablettes-lenovo', + 'Tablettes Microsoft Surface' => 'tablettes-microsoft-surface', + 'Tablettes Samsung' => 'tablettes-samsung', + 'Tablettes Xiaomi' => 'tablettes-xiaomi', + 'Tampons' => 'tampons', + 'Tapis' => 'tapis', + 'Tapis de souris' => 'tapis-de-souris', + 'Tassimo' => 'tassimo', + 'Taxis' => 'taxis', + 'Tefal' => 'tefal', + 'Tekken' => 'tekken', + 'Tekken 7' => 'tekken-7', + 'Télécommandes' => 'telecommandes', + 'Téléphones fixes' => 'telephones-fixes', + 'Téléphonie' => 'telephonie', + 'Téléviseurs' => 'televiseurs', + 'Tentes' => 'tentes', + 'Tentes Quechua' => 'tentes-quechua', + 'Têtes de brosse à dents de rechange' => 'tetes-de-brosse-a-dents-de-rechange', + 'Théâtre' => 'theatre', + 'The Last of Us' => 'the-last-of-us', + 'The Last of Us Part II' => 'the-last-of-us-part-2', + 'The Legend of Zelda' => 'the-legend-of-zelda', + 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild', + 'The Legend of Zelda: Link's Awakening' => 'legend-of-zelda-link-s-awakening', + 'The Legend of Zelda: Skyward Sword HD' => 'the-legend-of-zelda-skyward-sword-hd', + 'Thermomètres' => 'thermometres', + 'Thermomix' => 'thermomix', + 'Thermostats connectés' => 'thermostat-connecte', + 'Thés' => 'thes', + 'Thés glacés' => 'thes-glaces', + 'The Walking dead' => 'the-walking-dead', + 'The Witcher' => 'the-witcher', + 'The Witcher 3' => 'the-witcher-3', + 'Time's Up!' => 'time-s-up', + 'Tokyo Laundry' => 'tokyo-laundry', + 'Tomb Raider' => 'tomb-raider', + 'Tom Clancy's' => 'tom-clancy-s', + 'Tom Clancy's Ghost Recon: Wildlands' => 'tom-clancy-s-ghost-recon-wildlands', + 'Tom Clancy's Ghost Recon Breakpoint' => 'tom-clancy-s-ghost-recon-breakpoint', + 'Tom Clancy's The Division' => 'tom-clancy-s-the-division', + 'TomTom' => 'tomtom', + 'Tondeuses' => 'tondeuses', + 'Tondeuses à gazon' => 'tondeuses-a-gazon', + 'Toner' => 'toner', + 'Tongs' => 'tongs', + 'Torchons' => 'torchons', + 'Toshiba' => 'toshiba', + 'Total War' => 'total-war', + 'Total War: Warhammer' => 'total-war-warhammer', + 'Total War: Warhammer II' => 'total-war-warhammer-ii', + 'Tournevis' => 'tournevis-et-visseuses', + 'TP-Link' => 'tp-link', + 'Trains & Bus' => 'trains-bus', + 'Trampolines' => 'trampolines', + 'Transats & cosys' => 'transats-et-cosys', + 'Transport bébé' => 'poussettes', + 'Transport d'animaux' => 'transport-d-animaux', + 'Transports en commun' => 'transports-en-commun', + 'Transports urbains' => 'transports-urbains', + 'Travaux & matériaux' => 'travaux-materiaux', + 'Trépieds' => 'trepieds', + 'Trixie' => 'trixie', + 'Tronçonneuses' => 'tronconneuses', + 'Tropico' => 'tropico', + 'Tropico 6' => 'tropico-6', + 'Trottinettes' => 'trottinettes', + 'Trottinettes électriques' => 'trottinettes-electriques', + 'Trottinettes électriques en libre-service' => 'location-trottinettes-electriques', + 'Trottinettes Xiaomi' => 'trottinettes-xiaomi', + 'TV & Vidéo' => 'tv-video', + 'TV 4K' => 'tv-4k', + 'TV 40'' à 64''' => 'tv-40-pouces-a-64-pouces', + 'TV 65'' et plus' => 'tv-65-pouces-et-plus', + 'TV Hisense' => 'tv-hisense', + 'TV LG' => 'tv-lg', + 'TV OLED' => 'tv-oled', + 'TV Panasonic' => 'tv-panasonic', + 'TV Philips' => 'tv-philips', + 'TV Samsung' => 'tv-samsung', + 'TV Samsung QLED' => 'tv-samsung-qled', + 'TV Samsung The Frame' => 'tv-samsung-the-frame', + 'TV Sony' => 'tv-sony', + 'TV TCL' => 'tv-tcl', + 'TV Toshiba' => 'tv-toshiba', + 'TV Xiaomi' => 'tv-xiaomi', + 'UE Boom 2' => 'ue-boom-2', + 'UE Boom 3' => 'ue-boom-3', + 'UE Megaboom' => 'ue-megaboom', + 'UE Megaboom 3' => 'ue-megaboom-3', + 'UE Wonderboom' => 'ue-wonderboom', + 'Ultraportables' => 'ultraportables', + 'Uncharted' => 'uncharted', + 'Uncharted 4' => 'uncharted-4', + 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy', + 'Under Armour' => 'under-armour', + 'Until Dawn' => 'until-dawn', + 'Ustensiles de cuisine' => 'ustensiles-de-cuisine', + 'Ustensiles de cuisson' => 'ustensiles-de-cuisson', + 'Vacances et séjours' => 'vacances-sejours', + 'Vaisselle' => 'vaisselle', + 'Valises' => 'valises', + 'Valises cabine' => 'valises-cabine', + 'Valises rigides' => 'valises-rigides', + 'Vans Old Skool' => 'vans-old-skool', + 'Variétés & revues' => 'varietes-et-revues', + 'Vases' => 'vases', + 'Veet' => 'veet', + 'Veilleuses' => 'veilleuses', + 'Vélos' => 'velos', + 'Vélos d'appartement' => 'velos-d-appartement', + 'Vélos électriques' => 'velos-electriques', + 'Ventilateurs' => 'ventilateurs', + 'Ventirad' => 'ventirad', + 'Vernis à ongles' => 'vernis-a-ongles', + 'Verres' => 'verres', + 'Vestes' => 'vestes', + 'Vestes polaires' => 'vestes-polaires', + 'Vêtements d'été' => 'vetements-d-ete', + 'Vêtements d'hiver' => 'vetements-d-hiver', + 'Vêtements de grossesse' => 'vetements-de-grossesse', + 'Vêtements de montagne' => 'vetements-techniques', + 'Vêtements de running' => 'vetements-de-running', + 'Vêtements de ski' => 'vetements-de-ski', + 'Vêtements de sport' => 'vetements-de-sport', + 'Vêtements pour bébé' => 'vetements-pour-bebe', + 'Vidéoprojecteurs' => 'projecteurs', + 'Vidéoprojecteurs 3D' => 'videoprojecteurs-3d', + 'Vidéoprojecteurs Acer' => 'videoprojecteurs-acer', + 'Vidéoprojecteurs BenQ' => 'videoprojecteurs-benq', + 'Vidéoprojecteurs Epson' => 'videoprojecteurs-epson', + 'Vidéoprojecteurs HD' => 'videoprojecteurs-hd', + 'Vidéoprojecteurs LG' => 'videoprojecteurs-lg', + 'Vidéoprojecteurs Optoma' => 'videoprojecteurs-optoma', + 'Vins' => 'vins', + 'Visites & patrimoine' => 'visites-et-patrimoine', + 'Visseuses' => 'visseuses', + 'VOD' => 'vod', + 'Voitures & motos' => 'voitures-motos', + 'Voitures télécommandées' => 'voitures-telecommandees', + 'Volants' => 'volants-de-course', + 'Vols' => 'billets-d-avion', + 'Voyages' => 'voyages', + 'Voyages & loisirs' => 'le-laboratoire-des-voyages-loisirs', + 'VPN' => 'vpn', + 'VTC' => 'vtc', + 'VTT' => 'vtt', + 'Wacom Cintiq' => 'cintiq', + 'Watch Dogs' => 'watch-dogs', + 'Watch Dogs 2' => 'watch-dogs-2', + 'Watch Dogs: Legion' => 'watch-dogs-legion', + 'Watercooling' => 'watercooling', + 'WD (Western Digital)' => 'western-digital', + 'Wearables' => 'wearables', + 'Webcams' => 'webcams', + 'Whey' => 'whey', + 'Whirlpool' => 'whirlpool', + 'Whiskas' => 'whiskas', + 'Whisky' => 'whisky', + 'Wiko' => 'wiko', + 'Wilkinson Sword Hydro 5' => 'wilkinson-sword-hydro-5', + 'Windows' => 'windows', + 'WindScribe' => 'windscribe', + 'Wolfenstein' => 'wolfenstein', + 'Wolfenstein II: The New Colossus' => 'wolfenstein-ii-the-new-colossus', + 'Xbox' => 'xbox', + 'Xbox Game Pass' => 'xbox-game-pass', + 'Xbox Live' => 'xbox-live', + 'XCOM' => 'xcom', + 'XCOM 2' => 'xcom-2', + 'Xiaomi' => 'xiaomi', + 'Xiaomi AirDots' => 'xiaomi-airdots', + 'Xiaomi Black Shark' => 'xiaomi-black-shark', + 'Xiaomi Black Shark 2' => 'xiaomi-black-shark-2', + 'Xiaomi Mi6' => 'xiaomi-mi6', + 'Xiaomi Mi8' => 'xiaomi-mi8', + 'Xiaomi Mi8 Lite' => 'xiaomi-mi8-lite', + 'Xiaomi Mi8 Pro' => 'xiaomi-mi8-pro', + 'Xiaomi Mi8 SE' => 'xoaimi-mi8-se', + 'Xiaomi Mi9' => 'xiaomi-mi9', + 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite', + 'Xiaomi Mi 9 Pro' => 'xiaomi-mi-9-pro', + 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se', + 'Xiaomi Mi 9T' => 'xiaomi-mi-9t', + 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro', + 'Xiaomi Mi 10' => 'xiaomi-mi-10', + 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite', + 'Xiaomi Mi 10 Pro' => 'xiaomi-mi-10-pro', + 'Xiaomi Mi 10T' => 'xiaomi-mi-10t', + 'Xiaomi Mi 10T Lite' => 'xiaomi-mi-10t-lite', + 'Xiaomi Mi 10T Pro' => 'xiaomi-mi-10t-pro', + 'Xiaomi Mi 11' => 'xiaomi-mi-11', + 'Xiaomi Mi 11 Lite' => 'xiaomi-mi-11-lite', + 'Xiaomi Mi A1' => 'xiaomi-mi-a1', + 'Xiaomi Mi A2' => 'xiaomi-mi-a2', + 'Xiaomi Mi A2 Lite' => 'xiaomi-mi-a2-lite', + 'Xiaomi Mi Airdots Pro' => 'xiaomi-mi-airdots-pro', + 'Xiaomi Mi Band' => 'xiaomi-mi-band', + 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4', + 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5', + 'Xiaomi Mi Band 6' => 'xiaomi-mi-band-6', + 'Xiaomi Mi Box' => 'xiaomi-mi-box', + 'Xiaomi Mi Electric Scooter M365' => 'xiaomi-mi-electric-scooter-m365', + 'Xiaomi Mi Max' => 'xiaomi-mi-max', + 'Xiaomi Mi Mix' => 'xiaomi-mi-mix', + 'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2', + 'Xiaomi Mi Note 10' => 'xiaomi-mi-note-10', + 'Xiaomi Mi Note 10 Pro' => 'xiaomi-mi-note-10-pro', + 'Xiaomi Mi Pad 3' => 'xiaomi-mi-pad-3', + 'Xiaomi Mi Watch' => 'xiaomi-mi-watch', + 'Xiaomi Pocophone F1' => 'xiaomi-pocophone-f1', + 'Xiaomi Redmi 4A' => 'xiaomi-redmi-4a', + 'Xiaomi Redmi 4X' => 'xiaomi-redmi-4x', + 'Xiaomi Redmi 7' => 'xiaomi-redmi-7', + 'Xiaomi Redmi 9' => 'xiaomi-redmi-9', + 'Xiaomi Redmi AirDots' => 'xiaomi-redmi-airdots', + 'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4', + 'Xiaomi Redmi Note 5' => 'xiaomi-redmi-note-5', + 'Xiaomi Redmi Note 6' => 'xiaomi-redmi-note-6', + 'Xiaomi Redmi Note 7' => 'xiaomi-redmi-note-7', + 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8', + 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro', + 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9', + 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro', + 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s', + 'Xiaomi Redmi Note 10' => 'xiaomi-redmi-note-10', + 'Xiaomi Redmi Note 10 Pro' => 'xiaomi-redmi-10-pro', + 'Xiaomi Smart Home' => 'xiaomi-smart-home', + 'Yamaha' => 'yamaha', + 'Yeelight' => 'xiaomi-yeelight', + 'Yoshi's Crafted World' => 'yoshi-crafted-world', + 'Zoos' => 'zoos', + ] + ], + 'order' => [ + 'name' => 'Trier par', + 'type' => 'list', + 'title' => 'Ordre de tri des deals', + 'values' => [ + 'Du deal le plus Hot au moins Hot' => '-hot', + 'Du deal le plus récent au plus ancien' => '-nouveaux', + ] + ] + ], + 'Surveillance Discussion' => [ + 'url' => [ + 'name' => 'URL de la discussion', + 'type' => 'text', + 'required' => true, + 'title' => 'URL discussion à surveiller: https://www.dealabs.com/discussions/titre-1234', + 'exampleValue' => 'https://www.dealabs.com/discussions/jeux-steam-gratuits-gleam-woobox-etc-1071415', + ], - 'only_with_url' => array( - 'name' => 'Exclure les commentaires sans URL', - 'type' => 'checkbox', - 'title' => 'Exclure les commentaires ne contenant pas d\'URL dans le flux', - 'defaultValue' => false, - ) + 'only_with_url' => [ + 'name' => 'Exclure les commentaires sans URL', + 'type' => 'checkbox', + 'title' => 'Exclure les commentaires ne contenant pas d\'URL dans le flux', + 'defaultValue' => false, + ] - ) - - ); - - public $lang = array( - 'bridge-uri' => SELF::URI, - 'bridge-name' => SELF::NAME, - 'context-keyword' => 'Recherche par Mot(s) clé(s)', - 'context-group' => 'Deals par groupe', - 'context-talk' => 'Surveillance Discussion', - 'uri-group' => 'groupe/', - 'request-error' => 'Impossible de joindre Dealabs', - 'thread-error' => 'Impossible de déterminer l\'ID de la discussion. Vérifiez l\'URL que vous avez entré', - 'no-results' => 'Il n'y a rien à afficher pour le moment :(', - 'relative-date-indicator' => array( - 'il y a', - ), - 'price' => 'Prix', - 'shipping' => 'Livraison', - 'origin' => 'Origine', - 'discount' => 'Réduction', - 'title-keyword' => 'Recherche', - 'title-group' => 'Groupe', - 'title-talk' => 'Surveillance Discussion', - 'local-months' => array( - 'janvier', - 'février', - 'mars', - 'avril', - 'mai', - 'juin', - 'juillet', - 'août', - 'septembre', - 'octobre', - 'novembre', - 'décembre' - ), - 'local-time-relative' => array( - 'il y a ', - 'min', - 'h', - 'jour', - 'jours', - 'mois', - 'ans', - 'et ' - ), - 'date-prefixes' => array( - 'Actualisé ', - ), - 'relative-date-alt-prefixes' => array( - 'Actualisé ', - ), - 'relative-date-ignore-suffix' => array( - ), - - 'localdeal' => array( - 'Local', - 'Pays d\'expédition' - ), - ); + ] + ]; + public $lang = [ + 'bridge-uri' => self::URI, + 'bridge-name' => self::NAME, + 'context-keyword' => 'Recherche par Mot(s) clé(s)', + 'context-group' => 'Deals par groupe', + 'context-talk' => 'Surveillance Discussion', + 'uri-group' => 'groupe/', + 'request-error' => 'Impossible de joindre Dealabs', + 'thread-error' => 'Impossible de déterminer l\'ID de la discussion. Vérifiez l\'URL que vous avez entré', + 'no-results' => 'Il n'y a rien à afficher pour le moment :(', + 'relative-date-indicator' => [ + 'il y a', + ], + 'price' => 'Prix', + 'shipping' => 'Livraison', + 'origin' => 'Origine', + 'discount' => 'Réduction', + 'title-keyword' => 'Recherche', + 'title-group' => 'Groupe', + 'title-talk' => 'Surveillance Discussion', + 'local-months' => [ + 'janvier', + 'février', + 'mars', + 'avril', + 'mai', + 'juin', + 'juillet', + 'août', + 'septembre', + 'octobre', + 'novembre', + 'décembre' + ], + 'local-time-relative' => [ + 'il y a ', + 'min', + 'h', + 'jour', + 'jours', + 'mois', + 'ans', + 'et ' + ], + 'date-prefixes' => [ + 'Actualisé ', + ], + 'relative-date-alt-prefixes' => [ + 'Actualisé ', + ], + 'relative-date-ignore-suffix' => [ + ], + 'localdeal' => [ + 'Local', + 'Pays d\'expédition' + ], + ]; } diff --git a/bridges/DemoBridge.php b/bridges/DemoBridge.php index f48b4510..06ec4e1e 100644 --- a/bridges/DemoBridge.php +++ b/bridges/DemoBridge.php @@ -1,46 +1,47 @@ <?php -class DemoBridge extends BridgeAbstract { - const MAINTAINER = 'teromene'; - const NAME = 'DemoBridge'; - const URI = 'http://github.com/rss-bridge/rss-bridge'; - const DESCRIPTION = 'Bridge used for demos'; +class DemoBridge extends BridgeAbstract +{ + const MAINTAINER = 'teromene'; + const NAME = 'DemoBridge'; + const URI = 'http://github.com/rss-bridge/rss-bridge'; + const DESCRIPTION = 'Bridge used for demos'; - const PARAMETERS = array( - 'testCheckbox' => array( - 'testCheckbox' => array( - 'type' => 'checkbox', - 'name' => 'test des checkbox' - ) - ), - 'testList' => array( - 'testList' => array( - 'type' => 'list', - 'name' => 'test des listes', - 'values' => array( - 'Test' => 'test', - 'Test 2' => 'test2' - ) - ) - ), - 'testNumber' => array( - 'testNumber' => array( - 'type' => 'number', - 'name' => 'test des numéros', - 'exampleValue' => '1515632' - ) - ) - ); + const PARAMETERS = [ + 'testCheckbox' => [ + 'testCheckbox' => [ + 'type' => 'checkbox', + 'name' => 'test des checkbox' + ] + ], + 'testList' => [ + 'testList' => [ + 'type' => 'list', + 'name' => 'test des listes', + 'values' => [ + 'Test' => 'test', + 'Test 2' => 'test2' + ] + ] + ], + 'testNumber' => [ + 'testNumber' => [ + 'type' => 'number', + 'name' => 'test des numéros', + 'exampleValue' => '1515632' + ] + ] + ]; - public function collectData(){ + public function collectData() + { + $item = []; + $item['author'] = 'Me!'; + $item['title'] = 'Test'; + $item['content'] = 'Awesome content !'; + $item['id'] = 'Lalala'; + $item['uri'] = 'http://example.com/test'; - $item = array(); - $item['author'] = 'Me!'; - $item['title'] = 'Test'; - $item['content'] = 'Awesome content !'; - $item['id'] = 'Lalala'; - $item['uri'] = 'http://example.com/test'; - - $this->items[] = $item; - } + $this->items[] = $item; + } } diff --git a/bridges/DerpibooruBridge.php b/bridges/DerpibooruBridge.php index 8fb2777c..e06e0eff 100644 --- a/bridges/DerpibooruBridge.php +++ b/bridges/DerpibooruBridge.php @@ -1,116 +1,122 @@ <?php -class DerpibooruBridge extends BridgeAbstract { - const NAME = 'Derpibooru Bridge'; - const URI = 'https://derpibooru.org/'; - const DESCRIPTION = 'Returns newest images from a Derpibooru search'; - const CACHE_TIMEOUT = 300; // 5min - const MAINTAINER = 'Roliga'; - const PARAMETERS = array( - array( - 'f' => array( - 'name' => 'Filter', - 'type' => 'list', - 'values' => array( - 'Everything' => 56027, - '18+ R34' => 37432, - 'Legacy Default' => 37431, - '18+ Dark' => 37429, - 'Maximum Spoilers' => 37430, - 'Default' => 100073 - ), - 'defaultValue' => 56027 +class DerpibooruBridge extends BridgeAbstract +{ + const NAME = 'Derpibooru Bridge'; + const URI = 'https://derpibooru.org/'; + const DESCRIPTION = 'Returns newest images from a Derpibooru search'; + const CACHE_TIMEOUT = 300; // 5min + const MAINTAINER = 'Roliga'; - ), - 'q' => array( - 'name' => 'Query', - 'required' => true, - 'exampleValue' => 'dog', - ) - ) - ); + const PARAMETERS = [ + [ + 'f' => [ + 'name' => 'Filter', + 'type' => 'list', + 'values' => [ + 'Everything' => 56027, + '18+ R34' => 37432, + 'Legacy Default' => 37431, + '18+ Dark' => 37429, + 'Maximum Spoilers' => 37430, + 'Default' => 100073 + ], + 'defaultValue' => 56027 - public function detectParameters($url){ - $params = array(); + ], + 'q' => [ + 'name' => 'Query', + 'required' => true, + 'exampleValue' => 'dog', + ] + ] + ]; - // Search page e.g. https://derpibooru.org/search?q=cute - $regex = '/^(https?:\/\/)?(www\.)?derpibooru.org\/search.+q=([^\/&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['q'] = urldecode($matches[3]); - return $params; - } + public function detectParameters($url) + { + $params = []; - // Tag page, e.g. https://derpibooru.org/tags/artist-colon-devinian - $regex = '/^(https?:\/\/)?(www\.)?derpibooru.org\/tags\/([^\/&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['q'] = str_replace('-colon-', ':', urldecode($matches[3])); - return $params; - } + // Search page e.g. https://derpibooru.org/search?q=cute + $regex = '/^(https?:\/\/)?(www\.)?derpibooru.org\/search.+q=([^\/&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['q'] = urldecode($matches[3]); + return $params; + } - return null; - } + // Tag page, e.g. https://derpibooru.org/tags/artist-colon-devinian + $regex = '/^(https?:\/\/)?(www\.)?derpibooru.org\/tags\/([^\/&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['q'] = str_replace('-colon-', ':', urldecode($matches[3])); + return $params; + } - public function getName(){ - if(!is_null($this->getInput('q'))) { - return 'Derpibooru search for: ' - . $this->getInput('q'); - } else { - return parent::getName(); - } - } + return null; + } - public function getURI(){ - if(!is_null($this->getInput('f')) && !is_null($this->getInput('q'))) { - return self::URI - . 'search?filter_id=' - . urlencode($this->getInput('f')) - . '&q=' - . urlencode($this->getInput('q')); - } else { - return parent::getURI(); - } - } + public function getName() + { + if (!is_null($this->getInput('q'))) { + return 'Derpibooru search for: ' + . $this->getInput('q'); + } else { + return parent::getName(); + } + } - public function collectData(){ - $queryJson = json_decode(getContents( - self::URI - . 'api/v1/json/search/images?filter_id=' - . urlencode($this->getInput('f')) - . '&q=' - . urlencode($this->getInput('q')) - )); + public function getURI() + { + if (!is_null($this->getInput('f')) && !is_null($this->getInput('q'))) { + return self::URI + . 'search?filter_id=' + . urlencode($this->getInput('f')) + . '&q=' + . urlencode($this->getInput('q')); + } else { + return parent::getURI(); + } + } - foreach($queryJson->images as $post) { - $item = array(); + public function collectData() + { + $queryJson = json_decode(getContents( + self::URI + . 'api/v1/json/search/images?filter_id=' + . urlencode($this->getInput('f')) + . '&q=' + . urlencode($this->getInput('q')) + )); - $postUri = self::URI . $post->id; + foreach ($queryJson->images as $post) { + $item = []; - $item['uri'] = $postUri; - $item['title'] = $post->name; - $item['timestamp'] = strtotime($post->created_at); - $item['author'] = $post->uploader; - $item['enclosures'] = array($post->view_url); - $item['categories'] = $post->tags; + $postUri = self::URI . $post->id; - $item['content'] = '<p><a href="' // image preview - . $postUri - . '"><img src="' - . $post->representations->medium - . '"></a></p><p>' // description - . $post->description - . '</p><p><b>Size:</b> ' // image size - . $post->width - . 'x' - . $post->height; - // source link - if ($post->source_url != null) { - $item['content'] .= '<br><b>Source:</b> <a href="' - . $post->source_url - . '">' - . $post->source_url - . '</a></p>'; - }; - $this->items[] = $item; - } - } + $item['uri'] = $postUri; + $item['title'] = $post->name; + $item['timestamp'] = strtotime($post->created_at); + $item['author'] = $post->uploader; + $item['enclosures'] = [$post->view_url]; + $item['categories'] = $post->tags; + + $item['content'] = '<p><a href="' // image preview + . $postUri + . '"><img src="' + . $post->representations->medium + . '"></a></p><p>' // description + . $post->description + . '</p><p><b>Size:</b> ' // image size + . $post->width + . 'x' + . $post->height; + // source link + if ($post->source_url != null) { + $item['content'] .= '<br><b>Source:</b> <a href="' + . $post->source_url + . '">' + . $post->source_url + . '</a></p>'; + }; + $this->items[] = $item; + } + } } diff --git a/bridges/DesoutterBridge.php b/bridges/DesoutterBridge.php index e594240d..5331ff35 100644 --- a/bridges/DesoutterBridge.php +++ b/bridges/DesoutterBridge.php @@ -1,243 +1,250 @@ <?php -class DesoutterBridge extends BridgeAbstract { - - const CATEGORY_NEWS = 'News & Events'; - const CATEGORY_INDUSTRY = 'Industry 4.0 News'; - - const NAME = 'Desoutter Bridge'; - const URI = 'https://www.desouttertools.com'; - const DESCRIPTION = 'Returns feeds for news from Desoutter'; - const MAINTAINER = 'logmanoriginal'; - const CACHE_TIMEOUT = 86400; // 24 hours - - const PARAMETERS = array( - self::CATEGORY_NEWS => array( - 'news_lang' => array( - 'name' => 'Language', - 'type' => 'list', - 'title' => 'Select your language', - 'defaultValue' => 'https://www.desouttertools.com/about-desoutter/news-events', - 'values' => array( - 'Corporate' - => 'https://www.desouttertools.com/about-desoutter/news-events', - 'Česko' - => 'https://www.desouttertools.cz/o-desoutter/aktuality-udalsoti', - 'Deutschland' - => 'https://www.desoutter.de/ueber-desoutter/news-events', - 'España' - => 'https://www.desouttertools.es/sobre-desoutter/noticias-eventos', - 'México' - => 'https://www.desouttertools.mx/acerca-desoutter/noticias-eventos', - 'France' - => 'https://www.desouttertools.fr/a-propos-de-desoutter/actualites-evenements', - 'Magyarország' - => 'https://www.desouttertools.hu/a-desoutter-vallalatrol/hirek-esemenyek', - 'Italia' - => 'https://www.desouttertools.it/su-desoutter/news-eventi', - '日本' - => 'https://www.desouttertools.jp/desotanituite/niyusu-ibento', - '대한민국' - => 'https://www.desouttertools.co.kr/desoteoe-daehaeseo/nyuseu-mic-ibenteu', - 'Polska' - => 'https://www.desouttertools.pl/o-desoutter/aktualnosci-wydarzenia', - 'Brasil' - => 'https://www.desouttertools.com.br/sobre-desoutter/noti%C2%ADcias-eventos', - 'Portugal' - => 'https://www.desouttertools.pt/sobre-desoutter/notIcias-eventos', - 'România' - => 'https://www.desouttertools.ro/despre-desoutter/noutati-evenimente', - 'Российская Федерация' - => 'https://www.desouttertools.com.ru/o-desoutter/novosti-mieropriiatiia', - 'Slovensko' - => 'https://www.desouttertools.sk/o-spolocnosti-desoutter/novinky-udalosti', - 'Slovenija' - => 'https://www.desouttertools.si/o-druzbi-desoutter/novice-dogodki', - 'Sverige' - => 'https://www.desouttertools.se/om-desoutter/nyheter-evenemang', - 'Türkiye' - => 'https://www.desoutter.com.tr/desoutter-hakkinda/haberler-etkinlikler', - '中国' - => 'https://www.desouttertools.com.cn/guan-yu-ma-tou/xin-wen-he-huo-dong', - ) - ), - ), - self::CATEGORY_INDUSTRY => array( - 'industry_lang' => array( - 'name' => 'Language', - 'type' => 'list', - 'title' => 'Select your language', - 'defaultValue' => 'Corporate', - 'values' => array( - 'Corporate' - => 'https://www.desouttertools.com/industry-4-0/news', - 'Česko' - => 'https://www.desouttertools.cz/prumysl-4-0/novinky', - 'Deutschland' - => 'https://www.desoutter.de/industrie-4-0/news', - 'España' - => 'https://www.desouttertools.es/industria-4-0/noticias', - 'México' - => 'https://www.desouttertools.mx/industria-4-0/noticias', - 'France' - => 'https://www.desouttertools.fr/industrie-4-0/actualites', - 'Magyarország' - => 'https://www.desouttertools.hu/industry-4-0/hirek', - 'Italia' - => 'https://www.desouttertools.it/industry-4-0/news', - '日本' - => 'https://www.desouttertools.jp/industry-4-0/news', - '대한민국' - => 'https://www.desouttertools.co.kr/industry-4-0/news', - 'Polska' - => 'https://www.desouttertools.pl/przemysl-4-0/wiadomosci', - 'Brasil' - => 'https://www.desouttertools.com.br/industria-4-0/noticias', - 'Portugal' - => 'https://www.desouttertools.pt/industria-4-0/noticias', - 'România' - => 'https://www.desouttertools.ro/industry-4-0/noutati', - 'Российская Федерация' - => 'https://www.desouttertools.com.ru/industry-4-0/news', - 'Slovensko' - => 'https://www.desouttertools.sk/priemysel-4-0/novinky', - 'Slovenija' - => 'https://www.desouttertools.si/industrija-4-0/novice', - 'Sverige' - => 'https://www.desouttertools.se/industri-4-0/nyheter', - 'Türkiye' - => 'https://www.desoutter.com.tr/endustri-4-0/haberler', - '中国' - => 'https://www.desouttertools.com.cn/industry-4-0/news', - ) - ), - ), - 'global' => array( - 'full' => array( - 'name' => 'Load full articles', - 'type' => 'checkbox', - 'title' => 'Enable to load the full article for each item' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 3, - 'title' => "Maximum number of items to return in the feed.\n0 = unlimited" - ) - ) - ); - - private $title; - - public function getURI() { - switch($this->queriedContext) { - case self::CATEGORY_NEWS: - return $this->getInput('news_lang') ?: parent::getURI(); - case self::CATEGORY_INDUSTRY: - return $this->getInput('industry_lang') ?: parent::getURI(); - } - - return parent::getURI(); - } - - public function getName() { - return isset($this->title) ? $this->title . ' - ' . parent::getName() : parent::getName(); - } - - public function collectData() { - - // Uncomment to generate list of languages automtically (dev mode) - /* - switch($this->queriedContext) { - case self::CATEGORY_NEWS: - $this->extractNewsLanguages(); die; - case self::CATEGORY_INDUSTRY: - $this->extractIndustryLanguages(); die; - } - */ - - $html = getSimpleHTMLDOM($this->getURI()); - - $html = defaultLinkTo($html, $this->getURI()); - - $this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES); - - $limit = $this->getInput('limit') ?: 0; - - foreach($html->find('article') as $article) { - $item = array(); - - $item['uri'] = $article->find('a', 0)->href; - $item['title'] = $article->find('a[title]', 0)->title; - - if($this->getInput('full')) { - $item['content'] = $this->getFullNewsArticle($item['uri']); - } else { - $item['content'] = $article->find('div.tile-body p', 0)->plaintext; - } - - $this->items[] = $item; - - if ($limit > 0 && count($this->items) >= $limit) break; - } - - } - - private function getFullNewsArticle($uri) { - $html = getSimpleHTMLDOMCached($uri); - - $html = defaultLinkTo($html, $this->getURI()); - - return $html->find('section.article', 0); - } - - /** - * Generates a HTML page with a PHP formatted array of languages, - * pointing to the corresponding news pages. Implementation is based - * on the 'Corporate' site. - * @return void - */ - private function extractNewsLanguages() { - $html = getSimpleHTMLDOMCached('https://www.desouttertools.com/about-desoutter/news-events'); - - $html = defaultLinkTo($html, static::URI); - - $items = $html->find('ul[class="dropdown-menu"] li'); - - $list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/about-desoutter/news-events',\n"; - foreach($items as $item) { - $lang = trim($item->plaintext); - $uri = $item->find('a', 0)->href; +class DesoutterBridge extends BridgeAbstract +{ + const CATEGORY_NEWS = 'News & Events'; + const CATEGORY_INDUSTRY = 'Industry 4.0 News'; + + const NAME = 'Desoutter Bridge'; + const URI = 'https://www.desouttertools.com'; + const DESCRIPTION = 'Returns feeds for news from Desoutter'; + const MAINTAINER = 'logmanoriginal'; + const CACHE_TIMEOUT = 86400; // 24 hours + + const PARAMETERS = [ + self::CATEGORY_NEWS => [ + 'news_lang' => [ + 'name' => 'Language', + 'type' => 'list', + 'title' => 'Select your language', + 'defaultValue' => 'https://www.desouttertools.com/about-desoutter/news-events', + 'values' => [ + 'Corporate' + => 'https://www.desouttertools.com/about-desoutter/news-events', + 'Česko' + => 'https://www.desouttertools.cz/o-desoutter/aktuality-udalsoti', + 'Deutschland' + => 'https://www.desoutter.de/ueber-desoutter/news-events', + 'España' + => 'https://www.desouttertools.es/sobre-desoutter/noticias-eventos', + 'México' + => 'https://www.desouttertools.mx/acerca-desoutter/noticias-eventos', + 'France' + => 'https://www.desouttertools.fr/a-propos-de-desoutter/actualites-evenements', + 'Magyarország' + => 'https://www.desouttertools.hu/a-desoutter-vallalatrol/hirek-esemenyek', + 'Italia' + => 'https://www.desouttertools.it/su-desoutter/news-eventi', + '日本' + => 'https://www.desouttertools.jp/desotanituite/niyusu-ibento', + '대한민국' + => 'https://www.desouttertools.co.kr/desoteoe-daehaeseo/nyuseu-mic-ibenteu', + 'Polska' + => 'https://www.desouttertools.pl/o-desoutter/aktualnosci-wydarzenia', + 'Brasil' + => 'https://www.desouttertools.com.br/sobre-desoutter/noti%C2%ADcias-eventos', + 'Portugal' + => 'https://www.desouttertools.pt/sobre-desoutter/notIcias-eventos', + 'România' + => 'https://www.desouttertools.ro/despre-desoutter/noutati-evenimente', + 'Российская Федерация' + => 'https://www.desouttertools.com.ru/o-desoutter/novosti-mieropriiatiia', + 'Slovensko' + => 'https://www.desouttertools.sk/o-spolocnosti-desoutter/novinky-udalosti', + 'Slovenija' + => 'https://www.desouttertools.si/o-druzbi-desoutter/novice-dogodki', + 'Sverige' + => 'https://www.desouttertools.se/om-desoutter/nyheter-evenemang', + 'Türkiye' + => 'https://www.desoutter.com.tr/desoutter-hakkinda/haberler-etkinlikler', + '中国' + => 'https://www.desouttertools.com.cn/guan-yu-ma-tou/xin-wen-he-huo-dong', + ] + ], + ], + self::CATEGORY_INDUSTRY => [ + 'industry_lang' => [ + 'name' => 'Language', + 'type' => 'list', + 'title' => 'Select your language', + 'defaultValue' => 'Corporate', + 'values' => [ + 'Corporate' + => 'https://www.desouttertools.com/industry-4-0/news', + 'Česko' + => 'https://www.desouttertools.cz/prumysl-4-0/novinky', + 'Deutschland' + => 'https://www.desoutter.de/industrie-4-0/news', + 'España' + => 'https://www.desouttertools.es/industria-4-0/noticias', + 'México' + => 'https://www.desouttertools.mx/industria-4-0/noticias', + 'France' + => 'https://www.desouttertools.fr/industrie-4-0/actualites', + 'Magyarország' + => 'https://www.desouttertools.hu/industry-4-0/hirek', + 'Italia' + => 'https://www.desouttertools.it/industry-4-0/news', + '日本' + => 'https://www.desouttertools.jp/industry-4-0/news', + '대한민국' + => 'https://www.desouttertools.co.kr/industry-4-0/news', + 'Polska' + => 'https://www.desouttertools.pl/przemysl-4-0/wiadomosci', + 'Brasil' + => 'https://www.desouttertools.com.br/industria-4-0/noticias', + 'Portugal' + => 'https://www.desouttertools.pt/industria-4-0/noticias', + 'România' + => 'https://www.desouttertools.ro/industry-4-0/noutati', + 'Российская Федерация' + => 'https://www.desouttertools.com.ru/industry-4-0/news', + 'Slovensko' + => 'https://www.desouttertools.sk/priemysel-4-0/novinky', + 'Slovenija' + => 'https://www.desouttertools.si/industrija-4-0/novice', + 'Sverige' + => 'https://www.desouttertools.se/industri-4-0/nyheter', + 'Türkiye' + => 'https://www.desoutter.com.tr/endustri-4-0/haberler', + '中国' + => 'https://www.desouttertools.com.cn/industry-4-0/news', + ] + ], + ], + 'global' => [ + 'full' => [ + 'name' => 'Load full articles', + 'type' => 'checkbox', + 'title' => 'Enable to load the full article for each item' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 3, + 'title' => "Maximum number of items to return in the feed.\n0 = unlimited" + ] + ] + ]; + + private $title; + + public function getURI() + { + switch ($this->queriedContext) { + case self::CATEGORY_NEWS: + return $this->getInput('news_lang') ?: parent::getURI(); + case self::CATEGORY_INDUSTRY: + return $this->getInput('industry_lang') ?: parent::getURI(); + } + + return parent::getURI(); + } + + public function getName() + { + return isset($this->title) ? $this->title . ' - ' . parent::getName() : parent::getName(); + } + + public function collectData() + { + // Uncomment to generate list of languages automtically (dev mode) + /* + switch($this->queriedContext) { + case self::CATEGORY_NEWS: + $this->extractNewsLanguages(); die; + case self::CATEGORY_INDUSTRY: + $this->extractIndustryLanguages(); die; + } + */ + + $html = getSimpleHTMLDOM($this->getURI()); + + $html = defaultLinkTo($html, $this->getURI()); + + $this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES); + + $limit = $this->getInput('limit') ?: 0; + + foreach ($html->find('article') as $article) { + $item = []; + + $item['uri'] = $article->find('a', 0)->href; + $item['title'] = $article->find('a[title]', 0)->title; + + if ($this->getInput('full')) { + $item['content'] = $this->getFullNewsArticle($item['uri']); + } else { + $item['content'] = $article->find('div.tile-body p', 0)->plaintext; + } + + $this->items[] = $item; + + if ($limit > 0 && count($this->items) >= $limit) { + break; + } + } + } + + private function getFullNewsArticle($uri) + { + $html = getSimpleHTMLDOMCached($uri); + + $html = defaultLinkTo($html, $this->getURI()); + + return $html->find('section.article', 0); + } + + /** + * Generates a HTML page with a PHP formatted array of languages, + * pointing to the corresponding news pages. Implementation is based + * on the 'Corporate' site. + * @return void + */ + private function extractNewsLanguages() + { + $html = getSimpleHTMLDOMCached('https://www.desouttertools.com/about-desoutter/news-events'); + + $html = defaultLinkTo($html, static::URI); + + $items = $html->find('ul[class="dropdown-menu"] li'); + + $list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/about-desoutter/news-events',\n"; + + foreach ($items as $item) { + $lang = trim($item->plaintext); + $uri = $item->find('a', 0)->href; + + $list .= "\t'{$lang}'\n\t=> '{$uri}',\n"; + } + + echo $list; + } - $list .= "\t'{$lang}'\n\t=> '{$uri}',\n"; - } + /** + * Generates a HTML page with a PHP formatted array of languages, + * pointing to the corresponding news pages. Implementation is based + * on the 'Corporate' site. + * @return void + */ + private function extractIndustryLanguages() + { + $html = getSimpleHTMLDOMCached('https://www.desouttertools.com/industry-4-0/news'); - echo $list; - } + $html = defaultLinkTo($html, static::URI); - /** - * Generates a HTML page with a PHP formatted array of languages, - * pointing to the corresponding news pages. Implementation is based - * on the 'Corporate' site. - * @return void - */ - private function extractIndustryLanguages() { - $html = getSimpleHTMLDOMCached('https://www.desouttertools.com/industry-4-0/news'); + $items = $html->find('ul[class="dropdown-menu"] li'); - $html = defaultLinkTo($html, static::URI); + $list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/industry-4-0/news',\n"; - $items = $html->find('ul[class="dropdown-menu"] li'); + foreach ($items as $item) { + $lang = trim($item->plaintext); + $uri = $item->find('a', 0)->href; - $list = "\t'Corporate'\n\t=> 'https://www.desouttertools.com/industry-4-0/news',\n"; + $list .= "\t'{$lang}'\n\t=> '{$uri}',\n"; + } - foreach($items as $item) { - $lang = trim($item->plaintext); - $uri = $item->find('a', 0)->href; - - $list .= "\t'{$lang}'\n\t=> '{$uri}',\n"; - } - - echo $list; - } + echo $list; + } } diff --git a/bridges/DevToBridge.php b/bridges/DevToBridge.php index f449d70a..3940fff2 100644 --- a/bridges/DevToBridge.php +++ b/bridges/DevToBridge.php @@ -1,107 +1,113 @@ <?php -class DevToBridge extends BridgeAbstract { - - const CONTEXT_BY_TAG = 'By tag'; - - const NAME = 'dev.to Bridge'; - const URI = 'https://dev.to'; - const DESCRIPTION = 'Returns feeds for tags'; - const MAINTAINER = 'logmanoriginal'; - const CACHE_TIMEOUT = 10800; // 15 min. - - const PARAMETERS = array( - self::CONTEXT_BY_TAG => array( - 'tag' => array( - 'name' => 'Tag', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert your tag', - 'exampleValue' => 'python' - ), - 'full' => array( - 'name' => 'Full article', - 'type' => 'checkbox', - 'required' => false, - 'title' => 'Enable to receive the full article for each item' - ) - ) - ); - - public function getURI() { - switch($this->queriedContext) { - case self::CONTEXT_BY_TAG: - if($tag = $this->getInput('tag')) { - return static::URI . '/t/' . urlencode($tag); - } - break; - } - - return parent::getURI(); - } - - public function getIcon() { - return 'https://practicaldev-herokuapp-com.freetls.fastly.net/assets/ + +class DevToBridge extends BridgeAbstract +{ + const CONTEXT_BY_TAG = 'By tag'; + + const NAME = 'dev.to Bridge'; + const URI = 'https://dev.to'; + const DESCRIPTION = 'Returns feeds for tags'; + const MAINTAINER = 'logmanoriginal'; + const CACHE_TIMEOUT = 10800; // 15 min. + + const PARAMETERS = [ + self::CONTEXT_BY_TAG => [ + 'tag' => [ + 'name' => 'Tag', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert your tag', + 'exampleValue' => 'python' + ], + 'full' => [ + 'name' => 'Full article', + 'type' => 'checkbox', + 'required' => false, + 'title' => 'Enable to receive the full article for each item' + ] + ] + ]; + + public function getURI() + { + switch ($this->queriedContext) { + case self::CONTEXT_BY_TAG: + if ($tag = $this->getInput('tag')) { + return static::URI . '/t/' . urlencode($tag); + } + break; + } + + return parent::getURI(); + } + + public function getIcon() + { + return 'https://practicaldev-herokuapp-com.freetls.fastly.net/assets/ apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png'; - } + } - public function collectData() { - $html = getSimpleHTMLDOMCached($this->getURI()); + public function collectData() + { + $html = getSimpleHTMLDOMCached($this->getURI()); - $html = defaultLinkTo($html, static::URI); + $html = defaultLinkTo($html, static::URI); - $articles = $html->find('div.crayons-story') - or returnServerError('Could not find articles!'); + $articles = $html->find('div.crayons-story') + or returnServerError('Could not find articles!'); - foreach($articles as $article) { - $item = array(); + foreach ($articles as $article) { + $item = []; - $item['uri'] = $article->find('a[id*=article-link]', 0)->href; - $item['title'] = $article->find('h2 > a', 0)->plaintext; + $item['uri'] = $article->find('a[id*=article-link]', 0)->href; + $item['title'] = $article->find('h2 > a', 0)->plaintext; - $item['timestamp'] = $article->find('time', 0)->datetime; - $item['author'] = $article->find('a.crayons-story__secondary.fw-medium', 0)->plaintext; + $item['timestamp'] = $article->find('time', 0)->datetime; + $item['author'] = $article->find('a.crayons-story__secondary.fw-medium', 0)->plaintext; - // Profile image - $item['enclosures'] = array($article->find('img', 0)->src); + // Profile image + $item['enclosures'] = [$article->find('img', 0)->src]; - if($this->getInput('full')) { - $fullArticle = $this->getFullArticle($item['uri']); - $item['content'] = <<<EOD + if ($this->getInput('full')) { + $fullArticle = $this->getFullArticle($item['uri']); + $item['content'] = <<<EOD <p>{$fullArticle}</p> EOD; - } else { - $item['content'] = <<<EOD + } else { + $item['content'] = <<<EOD <img src="{$item['enclosures'][0]}" alt="{$item['author']}"> <p>{$item['title']}</p> EOD; - } + } - // categories - foreach ($article->find('a.crayons-tag') as $tag) { - $item['categories'][] = str_replace('#', '', $tag->plaintext); - } + // categories + foreach ($article->find('a.crayons-tag') as $tag) { + $item['categories'][] = str_replace('#', '', $tag->plaintext); + } - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getName() { - if (!is_null($this->getInput('tag'))) { - return ucfirst($this->getInput('tag')) . ' - dev.to'; - } + public function getName() + { + if (!is_null($this->getInput('tag'))) { + return ucfirst($this->getInput('tag')) . ' - dev.to'; + } - return parent::getName(); - } + return parent::getName(); + } - private function getFullArticle($url) { - $html = getSimpleHTMLDOMCached($url); + private function getFullArticle($url) + { + $html = getSimpleHTMLDOMCached($url); - $html = defaultLinkTo($html, static::URI); + $html = defaultLinkTo($html, static::URI); - if ($html->find('div.crayons-article__cover', 0)) { - return $html->find('div.crayons-article__cover', 0) . $html->find('[id="article-body"]', 0); - } + if ($html->find('div.crayons-article__cover', 0)) { + return $html->find('div.crayons-article__cover', 0) . $html->find('[id="article-body"]', 0); + } - return $html->find('[id="article-body"]', 0); - } + return $html->find('[id="article-body"]', 0); + } } diff --git a/bridges/DeveloppezDotComBridge.php b/bridges/DeveloppezDotComBridge.php index 1d3244b0..d0d54d0a 100644 --- a/bridges/DeveloppezDotComBridge.php +++ b/bridges/DeveloppezDotComBridge.php @@ -2,406 +2,405 @@ class DeveloppezDotComBridge extends FeedExpander { + const MAINTAINER = 'Binnette'; + const NAME = 'Developpez.com Actus (FR)'; + const URI = 'https://www.developpez.com/'; + const DOMAIN = '.developpez.com/'; + const RSS_URL = 'index/rss'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns complete posts from developpez.com'; + // Encodings used by Developpez.com in their articles body + const ENCONDINGS = ['Windows-1252', 'UTF-8']; + const PARAMETERS = [ + [ + 'limit' => [ + 'name' => 'Max items', + 'type' => 'number', + 'defaultValue' => 5, + ], + // list of the differents RSS availables + 'domain' => [ + 'type' => 'list', + 'name' => 'Domaine', + 'title' => 'Chosissez un sous-domaine', + 'values' => [ + '= Domaine principal =' => 'www', + '4d' => '4d', + 'abbyy' => 'abbyy', + 'access' => 'access', + 'agile' => 'agile', + 'ajax' => 'ajax', + 'algo' => 'algo', + 'alm' => 'alm', + 'android' => 'android', + 'apache' => 'apache', + 'applications' => 'applications', + 'arduino' => 'arduino', + 'asm' => 'asm', + 'asp' => 'asp', + 'aspose' => 'aspose', + 'bacasable' => 'bacasable', + 'big-data' => 'big-data', + 'bpm' => 'bpm', + 'bsd' => 'bsd', + 'business-intelligence' => 'business-intelligence', + 'c' => 'c', + 'cloud-computing' => 'cloud-computing', + 'club' => 'club', + 'cms' => 'cms', + 'cpp' => 'cpp', + 'crm' => 'crm', + 'css' => 'css', + 'd' => 'd', + 'dart' => 'dart', + 'data-science' => 'data-science', + 'db2' => 'db2', + 'delphi' => 'delphi', + 'dotnet' => 'dotnet', + 'droit' => 'droit', + 'eclipse' => 'eclipse', + 'edi' => 'edi', + 'embarque' => 'embarque', + 'emploi' => 'emploi', + 'etudes' => 'etudes', + 'excel' => 'excel', + 'firebird' => 'firebird', + 'flash' => 'flash', + 'go' => 'go', + 'green-it' => 'green-it', + 'gtk' => 'gtk', + 'hardware' => 'hardware', + 'hpc' => 'hpc', + 'humour' => 'humour', + 'ibmcloud' => 'ibmcloud', + 'intelligence-artificielle' => 'intelligence-artificielle', + 'interbase' => 'interbase', + 'ios' => 'ios', + 'java' => 'java', + 'javascript' => 'javascript', + 'javaweb' => 'javaweb', + 'jetbrains' => 'jetbrains', + 'jeux' => 'jeux', + 'kotlin' => 'kotlin', + 'labview' => 'labview', + 'laravel' => 'laravel', + 'latex' => 'latex', + 'lazarus' => 'lazarus', + 'linux' => 'linux', + 'mac' => 'mac', + 'matlab' => 'matlab', + 'megaoffice' => 'megaoffice', + 'merise' => 'merise', + 'microsoft' => 'microsoft', + 'mobiles' => 'mobiles', + 'mongodb' => 'mongodb', + 'mysql' => 'mysql', + 'netbeans' => 'netbeans', + 'nodejs' => 'nodejs', + 'nosql' => 'nosql', + 'objective-c' => 'objective-c', + 'office' => 'office', + 'open-source' => 'open-source', + 'openoffice-libreoffice' => 'openoffice-libreoffice', + 'oracle' => 'oracle', + 'outlook' => 'outlook', + 'pascal' => 'pascal', + 'perl' => 'perl', + 'php' => 'php', + 'portail-emploi' => 'portail-emploi', + 'portail-projets' => 'portail-projets', + 'postgresql' => 'postgresql', + 'powerpoint' => 'powerpoint', + 'preprod-emploi' => 'preprod-emploi', + 'programmation' => 'programmation', + 'project' => 'project', + 'purebasic' => 'purebasic', + 'pyqt' => 'pyqt', + 'python' => 'python', + 'qt-creator' => 'qt-creator', + 'qt' => 'qt', + 'r' => 'r', + 'raspberry-pi' => 'raspberry-pi', + 'reseau' => 'reseau', + 'ruby' => 'ruby', + 'rust' => 'rust', + 'sap' => 'sap', + 'sas' => 'sas', + 'scilab' => 'scilab', + 'securite' => 'securite', + 'sgbd' => 'sgbd', + 'sharepoint' => 'sharepoint', + 'solutions-entreprise' => 'solutions-entreprise', + 'spring' => 'spring', + 'sqlserver' => 'sqlserver', + 'stages' => 'stages', + 'supervision' => 'supervision', + 'swift' => 'swift', + 'sybase' => 'sybase', + 'symfony' => 'symfony', + 'systeme' => 'systeme', + 'talend' => 'talend', + 'typescript' => 'typescript', + 'uml' => 'uml', + 'unix' => 'unix', + 'vb' => 'vb', + 'vba' => 'vba', + 'virtualisation' => 'virtualisation', + 'visualstudio' => 'visualstudio', + 'web-semantique' => 'web-semantique', + 'web' => 'web', + 'webmarketing' => 'webmarketing', + 'wind' => 'wind', + 'windows-azure' => 'windows-azure', + 'windows' => 'windows', + 'windowsphone' => 'windowsphone', + 'word' => 'word', + 'xhtml' => 'xhtml', + 'xml' => 'xml', + 'zend-framework' => 'zend-framework' + ], + ] + ] + ]; - const MAINTAINER = 'Binnette'; - const NAME = 'Developpez.com Actus (FR)'; - const URI = 'https://www.developpez.com/'; - const DOMAIN = '.developpez.com/'; - const RSS_URL = 'index/rss'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns complete posts from developpez.com'; - // Encodings used by Developpez.com in their articles body - const ENCONDINGS = array('Windows-1252', 'UTF-8'); - const PARAMETERS = array( - array( - 'limit' => array( - 'name' => 'Max items', - 'type' => 'number', - 'defaultValue' => 5, - ), - // list of the differents RSS availables - 'domain' => array( - 'type' => 'list', - 'name' => 'Domaine', - 'title' => 'Chosissez un sous-domaine', - 'values' => array( - '= Domaine principal =' => 'www', - '4d' => '4d', - 'abbyy' => 'abbyy', - 'access' => 'access', - 'agile' => 'agile', - 'ajax' => 'ajax', - 'algo' => 'algo', - 'alm' => 'alm', - 'android' => 'android', - 'apache' => 'apache', - 'applications' => 'applications', - 'arduino' => 'arduino', - 'asm' => 'asm', - 'asp' => 'asp', - 'aspose' => 'aspose', - 'bacasable' => 'bacasable', - 'big-data' => 'big-data', - 'bpm' => 'bpm', - 'bsd' => 'bsd', - 'business-intelligence' => 'business-intelligence', - 'c' => 'c', - 'cloud-computing' => 'cloud-computing', - 'club' => 'club', - 'cms' => 'cms', - 'cpp' => 'cpp', - 'crm' => 'crm', - 'css' => 'css', - 'd' => 'd', - 'dart' => 'dart', - 'data-science' => 'data-science', - 'db2' => 'db2', - 'delphi' => 'delphi', - 'dotnet' => 'dotnet', - 'droit' => 'droit', - 'eclipse' => 'eclipse', - 'edi' => 'edi', - 'embarque' => 'embarque', - 'emploi' => 'emploi', - 'etudes' => 'etudes', - 'excel' => 'excel', - 'firebird' => 'firebird', - 'flash' => 'flash', - 'go' => 'go', - 'green-it' => 'green-it', - 'gtk' => 'gtk', - 'hardware' => 'hardware', - 'hpc' => 'hpc', - 'humour' => 'humour', - 'ibmcloud' => 'ibmcloud', - 'intelligence-artificielle' => 'intelligence-artificielle', - 'interbase' => 'interbase', - 'ios' => 'ios', - 'java' => 'java', - 'javascript' => 'javascript', - 'javaweb' => 'javaweb', - 'jetbrains' => 'jetbrains', - 'jeux' => 'jeux', - 'kotlin' => 'kotlin', - 'labview' => 'labview', - 'laravel' => 'laravel', - 'latex' => 'latex', - 'lazarus' => 'lazarus', - 'linux' => 'linux', - 'mac' => 'mac', - 'matlab' => 'matlab', - 'megaoffice' => 'megaoffice', - 'merise' => 'merise', - 'microsoft' => 'microsoft', - 'mobiles' => 'mobiles', - 'mongodb' => 'mongodb', - 'mysql' => 'mysql', - 'netbeans' => 'netbeans', - 'nodejs' => 'nodejs', - 'nosql' => 'nosql', - 'objective-c' => 'objective-c', - 'office' => 'office', - 'open-source' => 'open-source', - 'openoffice-libreoffice' => 'openoffice-libreoffice', - 'oracle' => 'oracle', - 'outlook' => 'outlook', - 'pascal' => 'pascal', - 'perl' => 'perl', - 'php' => 'php', - 'portail-emploi' => 'portail-emploi', - 'portail-projets' => 'portail-projets', - 'postgresql' => 'postgresql', - 'powerpoint' => 'powerpoint', - 'preprod-emploi' => 'preprod-emploi', - 'programmation' => 'programmation', - 'project' => 'project', - 'purebasic' => 'purebasic', - 'pyqt' => 'pyqt', - 'python' => 'python', - 'qt-creator' => 'qt-creator', - 'qt' => 'qt', - 'r' => 'r', - 'raspberry-pi' => 'raspberry-pi', - 'reseau' => 'reseau', - 'ruby' => 'ruby', - 'rust' => 'rust', - 'sap' => 'sap', - 'sas' => 'sas', - 'scilab' => 'scilab', - 'securite' => 'securite', - 'sgbd' => 'sgbd', - 'sharepoint' => 'sharepoint', - 'solutions-entreprise' => 'solutions-entreprise', - 'spring' => 'spring', - 'sqlserver' => 'sqlserver', - 'stages' => 'stages', - 'supervision' => 'supervision', - 'swift' => 'swift', - 'sybase' => 'sybase', - 'symfony' => 'symfony', - 'systeme' => 'systeme', - 'talend' => 'talend', - 'typescript' => 'typescript', - 'uml' => 'uml', - 'unix' => 'unix', - 'vb' => 'vb', - 'vba' => 'vba', - 'virtualisation' => 'virtualisation', - 'visualstudio' => 'visualstudio', - 'web-semantique' => 'web-semantique', - 'web' => 'web', - 'webmarketing' => 'webmarketing', - 'wind' => 'wind', - 'windows-azure' => 'windows-azure', - 'windows' => 'windows', - 'windowsphone' => 'windowsphone', - 'word' => 'word', - 'xhtml' => 'xhtml', - 'xml' => 'xml', - 'zend-framework' => 'zend-framework' - ), - ) - ) - ); + /** + * Return the RSS url for selected domain + */ + private function getRssUrl() + { + $domain = $this->getInput('domain'); + if (!empty($domain)) { + return 'https://' . $domain . self::DOMAIN . self::RSS_URL; + } - /** - * Return the RSS url for selected domain - */ - private function getRssUrl() - { - $domain = $this->getInput('domain'); - if (!empty($domain)) { - return 'https://' . $domain . self::DOMAIN . self::RSS_URL; - } + return self::URI . self::RSS_URL; + } - return self::URI . self::RSS_URL; - } + /** + * Grabs the RSS item from Developpez.com + */ + public function collectData() + { + $url = $this->getRssUrl(); + $this->collectExpandableDatas($url, 20); + } - /** - * Grabs the RSS item from Developpez.com - */ - public function collectData() - { - $url = $this->getRssUrl(); - $this->collectExpandableDatas($url, 20); - } + /** + * Parse the content of every RSS item. And will try to get the full article + * pointed by the item URL intead of the default abstract. + */ + protected function parseItem($newsItem) + { + if (count($this->items) >= $this->getInput('limit')) { + return null; + } - /** - * Parse the content of every RSS item. And will try to get the full article - * pointed by the item URL intead of the default abstract. - */ - protected function parseItem($newsItem) - { - if (count($this->items) >= $this->getInput('limit')) { - return null; - } + // This function parse each entry in the RSS with the default parse + $item = parent::parseItem($newsItem); - // This function parse each entry in the RSS with the default parse - $item = parent::parseItem($newsItem); + // There is a bug in Developpez RSS, coma are writtent as '~?' in the + // title, so I have to fix it manually + $item['title'] = $this->fixComaInTitle($item['title']); - // There is a bug in Developpez RSS, coma are writtent as '~?' in the - // title, so I have to fix it manually - $item['title'] = $this->fixComaInTitle($item['title']); + // We get the content of the full article behind the RSS item URL + $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']); - // We get the content of the full article behind the RSS item URL - $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']); + // Here we call our custom parser + $fullText = $this->extractFullText($articleHTMLContent); + if (!is_null($fullText)) { + // if we manage to parse the page behind the url of the RSS item + // then we set it as the new content. Otherwise we keep the default + // content to avoid RSS Bridge to return an empty item + $item['content'] = $fullText; + } - // Here we call our custom parser - $fullText = $this->extractFullText($articleHTMLContent); - if (!is_null($fullText)) { - // if we manage to parse the page behind the url of the RSS item - // then we set it as the new content. Otherwise we keep the default - // content to avoid RSS Bridge to return an empty item - $item['content'] = $fullText; - } + // Now we will attach video url in item + $videosUrl = $this->getAllVideoUrl($articleHTMLContent); + if (!empty($videosUrl)) { + $item['enclosures'] = array_merge($item['enclosures'], $videosUrl); + } - // Now we will attach video url in item - $videosUrl = $this->getAllVideoUrl($articleHTMLContent); - if (!empty($videosUrl)) { - $item['enclosures'] = array_merge($item['enclosures'], $videosUrl); - } + // Now we can look for the blog writer/creator + $author = $articleHTMLContent->find('[itemprop="creator"]', 0); + if (!empty($author)) { + $item['author'] = $author->outertext; + } - // Now we can look for the blog writer/creator - $author = $articleHTMLContent->find('[itemprop="creator"]', 0); - if (!empty($author)) { - $item['author'] = $author->outertext; - } + return $item; + } - return $item; - } + /** + * Replace '~?' by a proper coma ',' + */ + private function fixComaInTitle($txt) + { + return str_replace('~?', ',', $txt); + } - /** - * Replace '~?' by a proper coma ',' - */ - private function fixComaInTitle($txt) - { - return str_replace('~?', ',', $txt); - } + /** + * Return the full article pointed by the url in the RSS item + * Since Developpez.com only provides a short abstract of the article, we + * use the url to retrieve the complete article and return it as the content + */ + private function extractFullText($articleHTMLContent) + { + // All blog entry contains a div with the class 'content'. This div + // contains the complete blog article. But the RSS can also return + // announcement and not a blog article. So the next if, should take + // care of the "non blog" entry + $divArticleEntry = $articleHTMLContent->find('div.content', 0); + if (is_null($divArticleEntry)) { + // Didn't find the div with class content. It is probably not a blog + // entry. It is probably just an announcement for an ebook, a PDF, + // etc. So we can use the default RSS item content. + return null; + } - /** - * Return the full article pointed by the url in the RSS item - * Since Developpez.com only provides a short abstract of the article, we - * use the url to retrieve the complete article and return it as the content - */ - private function extractFullText($articleHTMLContent) - { - // All blog entry contains a div with the class 'content'. This div - // contains the complete blog article. But the RSS can also return - // announcement and not a blog article. So the next if, should take - // care of the "non blog" entry - $divArticleEntry = $articleHTMLContent->find('div.content', 0); - if (is_null($divArticleEntry)) { - // Didn't find the div with class content. It is probably not a blog - // entry. It is probably just an announcement for an ebook, a PDF, - // etc. So we can use the default RSS item content. - return null; - } + // The following code is a bit hacky, but I really manage to get the + // full content of articles without any encoding issues. What is very + // weird and ugly in Developpez.com is the fact the some paragraphs of + // the article will be encoded as UTF-8 and some other paragraphs will + // be encoded as Windows-1252. So we can NOT decode the full article + // with only one encoding. We have to check every paragraph and + // determine its encoding - // The following code is a bit hacky, but I really manage to get the - // full content of articles without any encoding issues. What is very - // weird and ugly in Developpez.com is the fact the some paragraphs of - // the article will be encoded as UTF-8 and some other paragraphs will - // be encoded as Windows-1252. So we can NOT decode the full article - // with only one encoding. We have to check every paragraph and - // determine its encoding + // This contains all the 'paragraphs' of the article. It includes the + // pictures, the text and the links at the bottom of the article + $paragraphs = $divArticleEntry->nodes; + // This will store the complete decoded content + $fullText = ''; - // This contains all the 'paragraphs' of the article. It includes the - // pictures, the text and the links at the bottom of the article - $paragraphs = $divArticleEntry->nodes; - // This will store the complete decoded content - $fullText = ''; + // For each paragraph, we will identify the encoding, then decode it + // and finally store the decoded content in $text + foreach ($paragraphs as $paragraph) { + // We have to recreate a new DOM document from the current node + // otherwise the find function will look in the complet article and + // not only in the current paragraph. This is an ugly behavior of + // the library Simple HTML DOM Parser... + $html = str_get_html($paragraph->outertext); + $fullText .= $this->decodeParagraph($html); + } - // For each paragraph, we will identify the encoding, then decode it - // and finally store the decoded content in $text - foreach ($paragraphs as $paragraph) { - // We have to recreate a new DOM document from the current node - // otherwise the find function will look in the complet article and - // not only in the current paragraph. This is an ugly behavior of - // the library Simple HTML DOM Parser... - $html = str_get_html($paragraph->outertext); - $fullText .= $this->decodeParagraph($html); - } + // Finally we return the full 'well' enconded content of the article + return $fullText; + } - // Finally we return the full 'well' enconded content of the article - return $fullText; - } - - /** - * - */ - private function decodeParagraph($p) - { - // First we check if this paragraph is a video - $videoUrl = $this->getVideoUrl($p); - if (!empty($videoUrl)) { - // If this is a video, we just return a link to the video - // 📺 => 🎞️ - return '<p> + /** + * + */ + private function decodeParagraph($p) + { + // First we check if this paragraph is a video + $videoUrl = $this->getVideoUrl($p); + if (!empty($videoUrl)) { + // If this is a video, we just return a link to the video + // 📺 => 🎞️ + return '<p> <b>📺 <a href="' . $videoUrl . '">Voir la vidéo</a></b> </p>'; - } + } - // We take outertext to get the complete paragraph not only the text - // inside it. That way we still graph block <img> and so on. - $pTxt = $p->outertext; - // This will store the decoded text if we manage to decode it - $decodedTxt = ''; + // We take outertext to get the complete paragraph not only the text + // inside it. That way we still graph block <img> and so on. + $pTxt = $p->outertext; + // This will store the decoded text if we manage to decode it + $decodedTxt = ''; - // This is the only way to properly decode each paragraph. I tried - // many stuffs but this is the only working way I found. - foreach (self::ENCONDINGS as $enc) { - // We check the encoding of the current paragraph - if (mb_check_encoding($pTxt, $enc)) { - // If the encoding is well recognized, we can convert from - // this encoding to UTF-8 - $decodedTxt = iconv($enc, 'UTF-8', $pTxt); - } - } + // This is the only way to properly decode each paragraph. I tried + // many stuffs but this is the only working way I found. + foreach (self::ENCONDINGS as $enc) { + // We check the encoding of the current paragraph + if (mb_check_encoding($pTxt, $enc)) { + // If the encoding is well recognized, we can convert from + // this encoding to UTF-8 + $decodedTxt = iconv($enc, 'UTF-8', $pTxt); + } + } - // We should not trim the strings to avoid the <a> to be glued to the - // text like: the software<a href="...">started</a>to... - if (!empty($decodedTxt)) { - // We manage to decode the text, so we take the decoded version - return $this->formatParagraph($decodedTxt); - } else { - // Otherwise we take the non decoded version and hope it will - // be displayed not too ugly in the fulltext content - return $this->formatParagraph($pTxt); - } - } + // We should not trim the strings to avoid the <a> to be glued to the + // text like: the software<a href="...">started</a>to... + if (!empty($decodedTxt)) { + // We manage to decode the text, so we take the decoded version + return $this->formatParagraph($decodedTxt); + } else { + // Otherwise we take the non decoded version and hope it will + // be displayed not too ugly in the fulltext content + return $this->formatParagraph($pTxt); + } + } - /** - * Return true in $txt is a HTML tag and not plain text - */ - private function isHtmlTagNotTxt($txt) - { - $html = str_get_html($txt); - return $html && $html->root && count($html->root->children) > 0; - } + /** + * Return true in $txt is a HTML tag and not plain text + */ + private function isHtmlTagNotTxt($txt) + { + $html = str_get_html($txt); + return $html && $html->root && count($html->root->children) > 0; + } - /** - * Will add a space before paragraph when needed - */ - private function formatParagraph($txt) - { - // If the paragraph is an html tag, we add a space before - if ($this->isHtmlTagNotTxt($txt)) { - // the first element is an html tag and not a text, so we can add a - // space before it - return ' ' . $txt; - } - // If the text start with word (not punctation), we had a space - $pattern = '/^\w/'; - if (preg_match($pattern, $txt)) { - return ' ' . $txt; - } - return $txt; - } + /** + * Will add a space before paragraph when needed + */ + private function formatParagraph($txt) + { + // If the paragraph is an html tag, we add a space before + if ($this->isHtmlTagNotTxt($txt)) { + // the first element is an html tag and not a text, so we can add a + // space before it + return ' ' . $txt; + } + // If the text start with word (not punctation), we had a space + $pattern = '/^\w/'; + if (preg_match($pattern, $txt)) { + return ' ' . $txt; + } + return $txt; + } - /** - * Retrieve all video url in the article - */ - private function getAllVideoUrl($item) - { - // Array of video url - $url = array(); + /** + * Retrieve all video url in the article + */ + private function getAllVideoUrl($item) + { + // Array of video url + $url = []; - // Developpez use a div with the class video-container - $divsVideo = $item->find('div.video-container'); - if (empty($divsVideo)) { - return $url; - } + // Developpez use a div with the class video-container + $divsVideo = $item->find('div.video-container'); + if (empty($divsVideo)) { + return $url; + } - // get the url of the video - foreach ($divsVideo as $div) { - $html = str_get_html($div->outertext); - $url[] = $this->getVideoUrl($html); - } + // get the url of the video + foreach ($divsVideo as $div) { + $html = str_get_html($div->outertext); + $url[] = $this->getVideoUrl($html); + } - return $url; - } + return $url; + } - /** - * Retrieve URL video. We have to check for the src of an iframe - * Work for Youtube. Will have to test for other video platform - */ - private function getVideoUrl($p) - { - $divVideo = $p->find('div.video-container', 0); - if (empty($divVideo)) { - return null; - } - $iframe = $divVideo->find('iframe', 0); - if (empty($iframe)) { - return null; - } - $src = trim($iframe->getAttribute('src')); - if (empty($src)) { - return null; - } - if (str_starts_with($src, '//')) { - $src = 'https:' . $src; - } - return $src; - } + /** + * Retrieve URL video. We have to check for the src of an iframe + * Work for Youtube. Will have to test for other video platform + */ + private function getVideoUrl($p) + { + $divVideo = $p->find('div.video-container', 0); + if (empty($divVideo)) { + return null; + } + $iframe = $divVideo->find('iframe', 0); + if (empty($iframe)) { + return null; + } + $src = trim($iframe->getAttribute('src')); + if (empty($src)) { + return null; + } + if (str_starts_with($src, '//')) { + $src = 'https:' . $src; + } + return $src; + } } diff --git a/bridges/DiarioDeNoticiasBridge.php b/bridges/DiarioDeNoticiasBridge.php index b3ab85a1..ac51eb44 100644 --- a/bridges/DiarioDeNoticiasBridge.php +++ b/bridges/DiarioDeNoticiasBridge.php @@ -1,84 +1,89 @@ <?php -class DiarioDeNoticiasBridge extends BridgeAbstract { - const NAME = 'Diário de Notícias (PT)'; - const URI = 'https://dn.pt'; - const DESCRIPTION = 'Diário de Notícias (DN.PT)'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - 'Tag' => array( - 'n' => array( - 'name' => 'Tag Name', - 'required' => true, - 'exampleValue' => 'rogerio-casanova', - ) - ) - ); - const MONPT = array( - 'jan', - 'fev', - 'mar', - 'abr', - 'mai', - 'jun', - 'jul', - 'ago', - 'set', - 'out', - 'nov', - 'dez', - ); +class DiarioDeNoticiasBridge extends BridgeAbstract +{ + const NAME = 'Diário de Notícias (PT)'; + const URI = 'https://dn.pt'; + const DESCRIPTION = 'Diário de Notícias (DN.PT)'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + 'Tag' => [ + 'n' => [ + 'name' => 'Tag Name', + 'required' => true, + 'exampleValue' => 'rogerio-casanova', + ] + ] + ]; - public function getIcon() { - return 'https://static.globalnoticias.pt/dn/common/images/favicons/favicon-128.png'; - } + const MONPT = [ + 'jan', + 'fev', + 'mar', + 'abr', + 'mai', + 'jun', + 'jul', + 'ago', + 'set', + 'out', + 'nov', + 'dez', + ]; - public function getName() { - switch($this->queriedContext) { - case 'Tag': - $name = self::NAME . ' | Tag | ' . $this->getInput('n'); - break; - default: - $name = self::NAME; - } - return $name; - } + public function getIcon() + { + return 'https://static.globalnoticias.pt/dn/common/images/favicons/favicon-128.png'; + } - public function getURI() { - switch($this->queriedContext) { - case 'Tag': - $url = self::URI . '/tag/' . $this->getInput('n') . '.html'; - break; - default: - $url = self::URI; - } - return $url; - } + public function getName() + { + switch ($this->queriedContext) { + case 'Tag': + $name = self::NAME . ' | Tag | ' . $this->getInput('n'); + break; + default: + $name = self::NAME; + } + return $name; + } - public function collectData() { - $archives = self::getURI(); - $html = getSimpleHTMLDOMCached($archives); + public function getURI() + { + switch ($this->queriedContext) { + case 'Tag': + $url = self::URI . '/tag/' . $this->getInput('n') . '.html'; + break; + default: + $url = self::URI; + } + return $url; + } - foreach($html->find('article') as $element) { - $item = array(); + public function collectData() + { + $archives = self::getURI(); + $html = getSimpleHTMLDOMCached($archives); - $title = $element->find('.t-am-title', 0); - $link = $element->find('a.t-am-text', 0); + foreach ($html->find('article') as $element) { + $item = []; - $item['title'] = $title->plaintext; - $item['uri'] = self::URI . $link->href; + $title = $element->find('.t-am-title', 0); + $link = $element->find('a.t-am-text', 0); - $snippet = $element->find('.t-am-lead', 0); - if ($snippet) { - $item['content'] = $snippet->plaintext; - } - preg_match('|edicao-do-dia\\/(?P<day>\d\d)-(?P<monpt>\w\w\w)-(?P<year>\d\d\d\d)|', $link->href, $d); - if ($d) { - $item['timestamp'] = sprintf('%s-%s-%s', $d['year'], array_search($d['monpt'], self::MONPT) + 1, $d['day']); - } + $item['title'] = $title->plaintext; + $item['uri'] = self::URI . $link->href; - $this->items[] = $item; - } + $snippet = $element->find('.t-am-lead', 0); + if ($snippet) { + $item['content'] = $snippet->plaintext; + } + preg_match('|edicao-do-dia\\/(?P<day>\d\d)-(?P<monpt>\w\w\w)-(?P<year>\d\d\d\d)|', $link->href, $d); + if ($d) { + $item['timestamp'] = sprintf('%s-%s-%s', $d['year'], array_search($d['monpt'], self::MONPT) + 1, $d['day']); + } - } + $this->items[] = $item; + } + } } diff --git a/bridges/DiarioDoAlentejoBridge.php b/bridges/DiarioDoAlentejoBridge.php index 6e43b876..9b82b49f 100644 --- a/bridges/DiarioDoAlentejoBridge.php +++ b/bridges/DiarioDoAlentejoBridge.php @@ -1,59 +1,68 @@ <?php -class DiarioDoAlentejoBridge extends BridgeAbstract { - const MAINTAINER = 'somini'; - const NAME = 'Diário do Alentejo'; - const URI = 'https://www.diariodoalentejo.pt'; - const DESCRIPTION = 'Semanário Regionalista Independente'; - const CACHE_TIMEOUT = 28800; // 8h - - /* This is used to hack around obtaining a timestamp. It's just a list of Month names in Portuguese ... */ - const PT_MONTH_NAMES = array( - 'janeiro', - 'fevereiro', - 'março', - 'abril', - 'maio', - 'junho', - 'julho', - 'agosto', - 'setembro', - 'outubro', - 'novembro', - 'dezembro'); - - public function getIcon() { - return 'https://www.diariodoalentejo.pt/images/favicon/apple-touch-icon.png'; - } - - public function collectData(){ - /* This is slow as molasses (>30s!), keep the cache timeout high to avoid killing the host */ - $html = getSimpleHTMLDOMCached($this->getURI() . '/pt/noticias-listagem.aspx'); - - foreach($html->find('.list_news .item') as $element) { - $item = array(); - - $item_link = $element->find('.body h2.title a', 0); - /* Another broken URL, see also `bridges/ComboiosDePortugalBridge.php` */ - $item['uri'] = self::URI . implode('/', array_map('urlencode', explode('/', $item_link->href))); - $item['title'] = $item_link->innertext; - - $item['timestamp'] = str_ireplace( - array_map(function($name) { return ' ' . $name . ' '; }, self::PT_MONTH_NAMES), - array_map(function($num) { return sprintf('-%02d-', $num); }, range(1, sizeof(self::PT_MONTH_NAMES))), - $element->find('span.date', 0)->innertext); - - /* Fix the Image URL */ - $item_image = $element->find('img.thumb', 0); - $item_image->src = preg_replace('/.*&img=([^&]+).*/', '\1', $item_image->getAttribute('data-src')); - - /* Content: */ - /* - Image */ - /* - Category */ - $content = $item_image . - '<center>' . $element->find('a.category', 0) . '</center>'; - $item['content'] = defaultLinkTo($content, self::URI); - - $this->items[] = $item; - } - } + +class DiarioDoAlentejoBridge extends BridgeAbstract +{ + const MAINTAINER = 'somini'; + const NAME = 'Diário do Alentejo'; + const URI = 'https://www.diariodoalentejo.pt'; + const DESCRIPTION = 'Semanário Regionalista Independente'; + const CACHE_TIMEOUT = 28800; // 8h + + /* This is used to hack around obtaining a timestamp. It's just a list of Month names in Portuguese ... */ + const PT_MONTH_NAMES = [ + 'janeiro', + 'fevereiro', + 'março', + 'abril', + 'maio', + 'junho', + 'julho', + 'agosto', + 'setembro', + 'outubro', + 'novembro', + 'dezembro']; + + public function getIcon() + { + return 'https://www.diariodoalentejo.pt/images/favicon/apple-touch-icon.png'; + } + + public function collectData() + { + /* This is slow as molasses (>30s!), keep the cache timeout high to avoid killing the host */ + $html = getSimpleHTMLDOMCached($this->getURI() . '/pt/noticias-listagem.aspx'); + + foreach ($html->find('.list_news .item') as $element) { + $item = []; + + $item_link = $element->find('.body h2.title a', 0); + /* Another broken URL, see also `bridges/ComboiosDePortugalBridge.php` */ + $item['uri'] = self::URI . implode('/', array_map('urlencode', explode('/', $item_link->href))); + $item['title'] = $item_link->innertext; + + $item['timestamp'] = str_ireplace( + array_map(function ($name) { + return ' ' . $name . ' '; + }, self::PT_MONTH_NAMES), + array_map(function ($num) { + return sprintf('-%02d-', $num); + }, range(1, sizeof(self::PT_MONTH_NAMES))), + $element->find('span.date', 0)->innertext + ); + + /* Fix the Image URL */ + $item_image = $element->find('img.thumb', 0); + $item_image->src = preg_replace('/.*&img=([^&]+).*/', '\1', $item_image->getAttribute('data-src')); + + /* Content: */ + /* - Image */ + /* - Category */ + $content = $item_image . + '<center>' . $element->find('a.category', 0) . '</center>'; + $item['content'] = defaultLinkTo($content, self::URI); + + $this->items[] = $item; + } + } } diff --git a/bridges/DiceBridge.php b/bridges/DiceBridge.php index ced793fe..5b764aef 100644 --- a/bridges/DiceBridge.php +++ b/bridges/DiceBridge.php @@ -1,123 +1,127 @@ <?php -class DiceBridge extends BridgeAbstract { - const MAINTAINER = 'rogerdc'; - const NAME = 'Dice Unofficial RSS'; - const URI = 'https://www.dice.com/'; - const DESCRIPTION = 'The Unofficial Dice RSS'; - // const CACHE_TIMEOUT = 86400; // 1 day +class DiceBridge extends BridgeAbstract +{ + const MAINTAINER = 'rogerdc'; + const NAME = 'Dice Unofficial RSS'; + const URI = 'https://www.dice.com/'; + const DESCRIPTION = 'The Unofficial Dice RSS'; + // const CACHE_TIMEOUT = 86400; // 1 day - const PARAMETERS = array(array( - 'for_one' => array( - 'name' => 'With at least one of the words', - 'required' => false, - ), - 'for_all' => array( - 'name' => 'With all of the words', - 'required' => false, - ), - 'for_exact' => array( - 'name' => 'With the exact phrase', - 'required' => false, - ), - 'for_none' => array( - 'name' => 'With none of these words', - 'required' => false, - ), - 'for_jt' => array( - 'name' => 'Within job title', - 'required' => false, - ), - 'for_com' => array( - 'name' => 'Within company name', - 'required' => false, - ), - 'for_loc' => array( - 'name' => 'City, State, or ZIP code', - 'required' => false, - ), - 'radius' => array( - 'name' => 'Radius in miles', - 'type' => 'list', - 'required' => false, - 'values' => array( - 'Exact Location' => 'El', - 'Within 5 miles' => '5', - 'Within 10 miles' => '10', - 'Within 20 miles' => '20', - 'Within 30 miles' => '0', - 'Within 40 miles' => '40', - 'Within 50 miles' => '50', - 'Within 75 miles' => '75', - 'Within 100 miles' => '100', - ), - 'defaultValue' => '0', - ), - 'jtype' => array( - 'name' => 'Job type', - 'type' => 'list', - 'required' => false, - 'values' => array( - 'Full-Time' => 'Full Time', - 'Part-Time' => 'Part Time', - 'Contract - Independent' => 'Contract Independent', - 'Contract - W2' => 'Contract W2', - 'Contract to Hire - Independent' => 'C2H Independent', - 'Contract to Hire - W2' => 'C2H W2', - 'Third Party - Contract - Corp-to-Corp' => 'Contract Corp-To-Corp', - 'Third Party - Contract to Hire - Corp-to-Corp' => 'C2H Corp-To-Corp', - ), - 'defaultValue' => 'Full Time', - ), - 'telecommute' => array( - 'name' => 'Telecommute', - 'type' => 'checkbox', - ), - )); + const PARAMETERS = [[ + 'for_one' => [ + 'name' => 'With at least one of the words', + 'required' => false, + ], + 'for_all' => [ + 'name' => 'With all of the words', + 'required' => false, + ], + 'for_exact' => [ + 'name' => 'With the exact phrase', + 'required' => false, + ], + 'for_none' => [ + 'name' => 'With none of these words', + 'required' => false, + ], + 'for_jt' => [ + 'name' => 'Within job title', + 'required' => false, + ], + 'for_com' => [ + 'name' => 'Within company name', + 'required' => false, + ], + 'for_loc' => [ + 'name' => 'City, State, or ZIP code', + 'required' => false, + ], + 'radius' => [ + 'name' => 'Radius in miles', + 'type' => 'list', + 'required' => false, + 'values' => [ + 'Exact Location' => 'El', + 'Within 5 miles' => '5', + 'Within 10 miles' => '10', + 'Within 20 miles' => '20', + 'Within 30 miles' => '0', + 'Within 40 miles' => '40', + 'Within 50 miles' => '50', + 'Within 75 miles' => '75', + 'Within 100 miles' => '100', + ], + 'defaultValue' => '0', + ], + 'jtype' => [ + 'name' => 'Job type', + 'type' => 'list', + 'required' => false, + 'values' => [ + 'Full-Time' => 'Full Time', + 'Part-Time' => 'Part Time', + 'Contract - Independent' => 'Contract Independent', + 'Contract - W2' => 'Contract W2', + 'Contract to Hire - Independent' => 'C2H Independent', + 'Contract to Hire - W2' => 'C2H W2', + 'Third Party - Contract - Corp-to-Corp' => 'Contract Corp-To-Corp', + 'Third Party - Contract to Hire - Corp-to-Corp' => 'C2H Corp-To-Corp', + ], + 'defaultValue' => 'Full Time', + ], + 'telecommute' => [ + 'name' => 'Telecommute', + 'type' => 'checkbox', + ], + ]]; - public function getIcon() { - return 'https://assets.dice.com/techpro/img/favicons/favicon.ico'; - } + public function getIcon() + { + return 'https://assets.dice.com/techpro/img/favicons/favicon.ico'; + } - public function collectData() { - $uri = 'https://www.dice.com/jobs/advancedResult.html'; - $uri .= '?for_one=' . urlencode($this->getInput('for_one')); - $uri .= '&for_all=' . urlencode($this->getInput('for_all')); - $uri .= '&for_exact=' . urlencode($this->getInput('for_exact')); - $uri .= '&for_none=' . urlencode($this->getInput('for_none')); - $uri .= '&for_jt=' . urlencode($this->getInput('for_jt')); - $uri .= '&for_com=' . urlencode($this->getInput('for_com')); - $uri .= '&for_loc=' . urlencode($this->getInput('for_loc')); - if ($this->getInput('jtype')) { - $uri .= '&jtype=' . urlencode($this->getInput('jtype')); - } - $uri .= '&sort=date&limit=100'; - $uri .= '&radius=' . urlencode($this->getInput('radius')); - if ($this->getInput('telecommute')) { - $uri .= '&telecommute=true'; - } + public function collectData() + { + $uri = 'https://www.dice.com/jobs/advancedResult.html'; + $uri .= '?for_one=' . urlencode($this->getInput('for_one')); + $uri .= '&for_all=' . urlencode($this->getInput('for_all')); + $uri .= '&for_exact=' . urlencode($this->getInput('for_exact')); + $uri .= '&for_none=' . urlencode($this->getInput('for_none')); + $uri .= '&for_jt=' . urlencode($this->getInput('for_jt')); + $uri .= '&for_com=' . urlencode($this->getInput('for_com')); + $uri .= '&for_loc=' . urlencode($this->getInput('for_loc')); + if ($this->getInput('jtype')) { + $uri .= '&jtype=' . urlencode($this->getInput('jtype')); + } + $uri .= '&sort=date&limit=100'; + $uri .= '&radius=' . urlencode($this->getInput('radius')); + if ($this->getInput('telecommute')) { + $uri .= '&telecommute=true'; + } - $html = getSimpleHTMLDOM($uri); - foreach($html->find('div.complete-serp-result-div') as $element) { - $item = array(); - // Title - $masterLink = $element->find('a[id^=position]', 0); - $item['title'] = $masterLink->title; - // URL - $uri = $masterLink->href; - // $uri = substr($uri, 0, strrpos($uri, '?')); - $item['uri'] = substr($uri, 0, strrpos($uri, '?')); - // ID - $item['id'] = $masterLink->value; - // Image - $image = $element->find('img', 0); - if ($image) - $item['image'] = $image->getAttribute('src'); - // Content - $shortdesc = $element->find('.shortdesc', '0'); - $shortdesc = ($shortdesc) ? $shortdesc->innertext : ''; - $item['content'] = $shortdesc; - $this->items[] = $item; - } - } + $html = getSimpleHTMLDOM($uri); + foreach ($html->find('div.complete-serp-result-div') as $element) { + $item = []; + // Title + $masterLink = $element->find('a[id^=position]', 0); + $item['title'] = $masterLink->title; + // URL + $uri = $masterLink->href; + // $uri = substr($uri, 0, strrpos($uri, '?')); + $item['uri'] = substr($uri, 0, strrpos($uri, '?')); + // ID + $item['id'] = $masterLink->value; + // Image + $image = $element->find('img', 0); + if ($image) { + $item['image'] = $image->getAttribute('src'); + } + // Content + $shortdesc = $element->find('.shortdesc', '0'); + $shortdesc = ($shortdesc) ? $shortdesc->innertext : ''; + $item['content'] = $shortdesc; + $this->items[] = $item; + } + } } diff --git a/bridges/DilbertBridge.php b/bridges/DilbertBridge.php index 827355d5..cd509ea4 100644 --- a/bridges/DilbertBridge.php +++ b/bridges/DilbertBridge.php @@ -1,35 +1,36 @@ <?php -class DilbertBridge extends BridgeAbstract { - const MAINTAINER = 'kranack'; - const NAME = 'Dilbert Daily Strip'; - const URI = 'https://dilbert.com'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'The Unofficial Dilbert Daily Comic Strip'; +class DilbertBridge extends BridgeAbstract +{ + const MAINTAINER = 'kranack'; + const NAME = 'Dilbert Daily Strip'; + const URI = 'https://dilbert.com'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'The Unofficial Dilbert Daily Comic Strip'; - public function collectData(){ + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $html = getSimpleHTMLDOM(self::URI); + foreach ($html->find('section.comic-item') as $element) { + $img = $element->find('img', 0); + $link = $element->find('a', 0); + $comic = $img->src; + $title = $img->alt; + $url = $link->href; + $date = substr(strrchr($url, '/'), 1); + if (empty($title)) { + $title = 'Dilbert Comic Strip on ' . $date; + } + $date = strtotime($date); - foreach($html->find('section.comic-item') as $element) { - - $img = $element->find('img', 0); - $link = $element->find('a', 0); - $comic = $img->src; - $title = $img->alt; - $url = $link->href; - $date = substr(strrchr($url, '/'), 1); - if (empty($title)) - $title = 'Dilbert Comic Strip on ' . $date; - $date = strtotime($date); - - $item = array(); - $item['uri'] = $url; - $item['title'] = $title; - $item['author'] = 'Scott Adams'; - $item['timestamp'] = $date; - $item['content'] = '<img src="' . $comic . '" alt="' . $img->alt . '" />'; - $this->items[] = $item; - } - } + $item = []; + $item['uri'] = $url; + $item['title'] = $title; + $item['author'] = 'Scott Adams'; + $item['timestamp'] = $date; + $item['content'] = '<img src="' . $comic . '" alt="' . $img->alt . '" />'; + $this->items[] = $item; + } + } } diff --git a/bridges/DiscogsBridge.php b/bridges/DiscogsBridge.php index df94a030..ba011924 100644 --- a/bridges/DiscogsBridge.php +++ b/bridges/DiscogsBridge.php @@ -1,120 +1,114 @@ <?php -class DiscogsBridge extends BridgeAbstract { - - const MAINTAINER = 'teromene'; - const NAME = 'DiscogsBridge'; - const URI = 'https://www.discogs.com/'; - const DESCRIPTION = 'Returns releases from discogs'; - const PARAMETERS = array( - 'Artist Releases' => array( - 'artistid' => array( - 'name' => 'Artist ID', - 'type' => 'number', - 'required' => true, - 'exampleValue' => '28104', - 'title' => 'Only the ID from an artist page. EG /artist/28104-Aesop-Rock is 28104' - ) - ), - 'Label Releases' => array( - 'labelid' => array( - 'name' => 'Label ID', - 'type' => 'number', - 'required' => true, - 'exampleValue' => '8201', - 'title' => 'Only the ID from a label page. EG /label/8201-Rhymesayers-Entertainment is 8201' - ) - ), - 'User Wantlist' => array( - 'username_wantlist' => array( - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'TheBlindMaster', - ) - ), - 'User Folder' => array( - 'username_folder' => array( - 'name' => 'Username', - 'type' => 'text', - ), - 'folderid' => array( - 'name' => 'Folder ID', - 'type' => 'number', - ) - ) - ); - - public function collectData() { - - if(!empty($this->getInput('artistid')) || !empty($this->getInput('labelid'))) { - - if(!empty($this->getInput('artistid'))) { - $data = getContents('https://api.discogs.com/artists/' - . $this->getInput('artistid') - . '/releases?sort=year&sort_order=desc'); - } elseif(!empty($this->getInput('labelid'))) { - $data = getContents('https://api.discogs.com/labels/' - . $this->getInput('labelid') - . '/releases?sort=year&sort_order=desc'); - } - - $jsonData = json_decode($data, true); - foreach($jsonData['releases'] as $release) { - - $item = array(); - $item['author'] = $release['artist']; - $item['title'] = $release['title']; - $item['id'] = $release['id']; - $resId = array_key_exists('main_release', $release) ? $release['main_release'] : $release['id']; - $item['uri'] = self::URI . $this->getInput('artistid') . '/release/' . $resId; - - if(isset($release['year'])) { - $item['timestamp'] = DateTime::createFromFormat('Y', $release['year'])->getTimestamp(); - } - - $item['content'] = $item['author'] . ' - ' . $item['title']; - $this->items[] = $item; - } - - } elseif(!empty($this->getInput('username_wantlist')) || !empty($this->getInput('username_folder'))) { - - if(!empty($this->getInput('username_wantlist'))) { - $data = getContents('https://api.discogs.com/users/' - . $this->getInput('username_wantlist') - . '/wants?sort=added&sort_order=desc'); - $jsonData = json_decode($data, true)['wants']; - - } elseif(!empty($this->getInput('username_folder'))) { - $data = getContents('https://api.discogs.com/users/' - . $this->getInput('username_folder') - . '/collection/folders/' - . $this->getInput('folderid') - . '/releases?sort=added&sort_order=desc'); - $jsonData = json_decode($data, true)['releases']; - } - foreach($jsonData as $element) { - - $infos = $element['basic_information']; - $item = array(); - $item['title'] = $infos['title']; - $item['author'] = $infos['artists'][0]['name']; - $item['id'] = $infos['artists'][0]['id']; - $item['uri'] = self::URI . $infos['artists'][0]['id'] . '/release/' . $infos['id']; - $item['timestamp'] = strtotime($element['date_added']); - $item['content'] = $item['author'] . ' - ' . $item['title']; - $this->items[] = $item; - - } - } - - } - - public function getURI() { - return self::URI; - } - - public function getName() { - return static::NAME; - } +class DiscogsBridge extends BridgeAbstract +{ + const MAINTAINER = 'teromene'; + const NAME = 'DiscogsBridge'; + const URI = 'https://www.discogs.com/'; + const DESCRIPTION = 'Returns releases from discogs'; + const PARAMETERS = [ + 'Artist Releases' => [ + 'artistid' => [ + 'name' => 'Artist ID', + 'type' => 'number', + 'required' => true, + 'exampleValue' => '28104', + 'title' => 'Only the ID from an artist page. EG /artist/28104-Aesop-Rock is 28104' + ] + ], + 'Label Releases' => [ + 'labelid' => [ + 'name' => 'Label ID', + 'type' => 'number', + 'required' => true, + 'exampleValue' => '8201', + 'title' => 'Only the ID from a label page. EG /label/8201-Rhymesayers-Entertainment is 8201' + ] + ], + 'User Wantlist' => [ + 'username_wantlist' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'TheBlindMaster', + ] + ], + 'User Folder' => [ + 'username_folder' => [ + 'name' => 'Username', + 'type' => 'text', + ], + 'folderid' => [ + 'name' => 'Folder ID', + 'type' => 'number', + ] + ] + ]; + + public function collectData() + { + if (!empty($this->getInput('artistid')) || !empty($this->getInput('labelid'))) { + if (!empty($this->getInput('artistid'))) { + $data = getContents('https://api.discogs.com/artists/' + . $this->getInput('artistid') + . '/releases?sort=year&sort_order=desc'); + } elseif (!empty($this->getInput('labelid'))) { + $data = getContents('https://api.discogs.com/labels/' + . $this->getInput('labelid') + . '/releases?sort=year&sort_order=desc'); + } + + $jsonData = json_decode($data, true); + foreach ($jsonData['releases'] as $release) { + $item = []; + $item['author'] = $release['artist']; + $item['title'] = $release['title']; + $item['id'] = $release['id']; + $resId = array_key_exists('main_release', $release) ? $release['main_release'] : $release['id']; + $item['uri'] = self::URI . $this->getInput('artistid') . '/release/' . $resId; + + if (isset($release['year'])) { + $item['timestamp'] = DateTime::createFromFormat('Y', $release['year'])->getTimestamp(); + } + + $item['content'] = $item['author'] . ' - ' . $item['title']; + $this->items[] = $item; + } + } elseif (!empty($this->getInput('username_wantlist')) || !empty($this->getInput('username_folder'))) { + if (!empty($this->getInput('username_wantlist'))) { + $data = getContents('https://api.discogs.com/users/' + . $this->getInput('username_wantlist') + . '/wants?sort=added&sort_order=desc'); + $jsonData = json_decode($data, true)['wants']; + } elseif (!empty($this->getInput('username_folder'))) { + $data = getContents('https://api.discogs.com/users/' + . $this->getInput('username_folder') + . '/collection/folders/' + . $this->getInput('folderid') + . '/releases?sort=added&sort_order=desc'); + $jsonData = json_decode($data, true)['releases']; + } + foreach ($jsonData as $element) { + $infos = $element['basic_information']; + $item = []; + $item['title'] = $infos['title']; + $item['author'] = $infos['artists'][0]['name']; + $item['id'] = $infos['artists'][0]['id']; + $item['uri'] = self::URI . $infos['artists'][0]['id'] . '/release/' . $infos['id']; + $item['timestamp'] = strtotime($element['date_added']); + $item['content'] = $item['author'] . ' - ' . $item['title']; + $this->items[] = $item; + } + } + } + + public function getURI() + { + return self::URI; + } + + public function getName() + { + return static::NAME; + } } diff --git a/bridges/DockerHubBridge.php b/bridges/DockerHubBridge.php index 343f832e..4ee72f5d 100644 --- a/bridges/DockerHubBridge.php +++ b/bridges/DockerHubBridge.php @@ -1,77 +1,81 @@ <?php -class DockerHubBridge extends BridgeAbstract { - const NAME = 'Docker Hub Bridge'; - const URI = 'https://hub.docker.com'; - const DESCRIPTION = 'Returns new images for a container'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - 'User Submitted Image' => array( - 'user' => array( - 'name' => 'User', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'rssbridge', - ), - 'repo' => array( - 'name' => 'Repository', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'rss-bridge', - ) - ), - 'Official Image' => array( - 'repo' => array( - 'name' => 'Repository', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'postgres', - ) - ), - ); - - const CACHE_TIMEOUT = 3600; // 1 hour - - private $apiURL = 'https://hub.docker.com/v2/repositories/'; - private $imageUrlRegex = '/hub\.docker\.com\/r\/([\w]+)\/([\w-]+)\/?/'; - private $officialImageUrlRegex = '/hub\.docker\.com\/_\/([\w-]+)\/?/'; - - public function detectParameters($url) { - $params = array(); - - // user submitted image - if(preg_match($this->imageUrlRegex, $url, $matches)) { - $params['context'] = 'User Submitted Image'; - $params['user'] = $matches[1]; - $params['repo'] = $matches[2]; - return $params; - } - - // official image - if(preg_match($this->officialImageUrlRegex, $url, $matches)) { - $params['context'] = 'Official Image'; - $params['repo'] = $matches[1]; - return $params; - } - - return null; - } - - public function collectData() { - $json = getContents($this->getApiUrl()); - - $data = json_decode($json, false); - - foreach ($data->results as $result) { - $item = array(); - - $lastPushed = date('Y-m-d H:i:s', strtotime($result->tag_last_pushed)); - - $item['title'] = $result->name; - $item['uid'] = $result->id; - $item['uri'] = $this->getTagUrl($result->name); - $item['author'] = $result->last_updater_username; - $item['timestamp'] = $result->tag_last_pushed; - $item['content'] = <<<EOD + +class DockerHubBridge extends BridgeAbstract +{ + const NAME = 'Docker Hub Bridge'; + const URI = 'https://hub.docker.com'; + const DESCRIPTION = 'Returns new images for a container'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + 'User Submitted Image' => [ + 'user' => [ + 'name' => 'User', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'rssbridge', + ], + 'repo' => [ + 'name' => 'Repository', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'rss-bridge', + ] + ], + 'Official Image' => [ + 'repo' => [ + 'name' => 'Repository', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'postgres', + ] + ], + ]; + + const CACHE_TIMEOUT = 3600; // 1 hour + + private $apiURL = 'https://hub.docker.com/v2/repositories/'; + private $imageUrlRegex = '/hub\.docker\.com\/r\/([\w]+)\/([\w-]+)\/?/'; + private $officialImageUrlRegex = '/hub\.docker\.com\/_\/([\w-]+)\/?/'; + + public function detectParameters($url) + { + $params = []; + + // user submitted image + if (preg_match($this->imageUrlRegex, $url, $matches)) { + $params['context'] = 'User Submitted Image'; + $params['user'] = $matches[1]; + $params['repo'] = $matches[2]; + return $params; + } + + // official image + if (preg_match($this->officialImageUrlRegex, $url, $matches)) { + $params['context'] = 'Official Image'; + $params['repo'] = $matches[1]; + return $params; + } + + return null; + } + + public function collectData() + { + $json = getContents($this->getApiUrl()); + + $data = json_decode($json, false); + + foreach ($data->results as $result) { + $item = []; + + $lastPushed = date('Y-m-d H:i:s', strtotime($result->tag_last_pushed)); + + $item['title'] = $result->name; + $item['uid'] = $result->id; + $item['uri'] = $this->getTagUrl($result->name); + $item['author'] = $result->last_updater_username; + $item['timestamp'] = $result->tag_last_pushed; + $item['content'] = <<<EOD <Strong>Tag</strong><br> <p>{$result->name}</p> <Strong>Last pushed</strong><br> @@ -80,86 +84,93 @@ class DockerHubBridge extends BridgeAbstract { {$this->getImages($result)} EOD; - $this->items[] = $item; - } - - } - - public function getURI() { - if ($this->queriedContext === 'Official Image') { - return self::URI . '/_/' . $this->getRepo(); - } - - if ($this->getInput('repo')) { - return self::URI . '/r/' . $this->getRepo(); - } - - return parent::getURI(); - } - - public function getName() { - if ($this->getInput('repo')) { - return $this->getRepo() . ' - Docker Hub'; - } - - return parent::getName(); - } - - private function getRepo() { - if ($this->queriedContext === 'Official Image') { - return $this->getInput('repo'); - } - - return $this->getInput('user') . '/' . $this->getInput('repo'); - } - - private function getApiUrl() { - if ($this->queriedContext === 'Official Image') { - return $this->apiURL . 'library/' . $this->getRepo() . '/tags/?page_size=25&page=1'; - } - - return $this->apiURL . $this->getRepo() . '/tags/?page_size=25&page=1'; - } - - private function getLayerUrl($name, $digest) { - if ($this->queriedContext === 'Official Image') { - return self::URI . '/layers/' . $this->getRepo() . '/library/' . - $this->getRepo() . '/' . $name . '/images/' . $digest; - } - - return self::URI . '/layers/' . $this->getRepo() . '/' . $name . '/images/' . $digest; - } - - private function getTagUrl($name) { - if ($this->queriedContext === 'Official Image') { - return self::URI . '/_/' . $this->getRepo() . '?tab=tags&name=' . $name; - } - - return self::URI . '/r/' . $this->getRepo() . '/tags?name=' . $name; - } - - private function getImages($result) { - $html = <<<EOD + $this->items[] = $item; + } + } + + public function getURI() + { + if ($this->queriedContext === 'Official Image') { + return self::URI . '/_/' . $this->getRepo(); + } + + if ($this->getInput('repo')) { + return self::URI . '/r/' . $this->getRepo(); + } + + return parent::getURI(); + } + + public function getName() + { + if ($this->getInput('repo')) { + return $this->getRepo() . ' - Docker Hub'; + } + + return parent::getName(); + } + + private function getRepo() + { + if ($this->queriedContext === 'Official Image') { + return $this->getInput('repo'); + } + + return $this->getInput('user') . '/' . $this->getInput('repo'); + } + + private function getApiUrl() + { + if ($this->queriedContext === 'Official Image') { + return $this->apiURL . 'library/' . $this->getRepo() . '/tags/?page_size=25&page=1'; + } + + return $this->apiURL . $this->getRepo() . '/tags/?page_size=25&page=1'; + } + + private function getLayerUrl($name, $digest) + { + if ($this->queriedContext === 'Official Image') { + return self::URI . '/layers/' . $this->getRepo() . '/library/' . + $this->getRepo() . '/' . $name . '/images/' . $digest; + } + + return self::URI . '/layers/' . $this->getRepo() . '/' . $name . '/images/' . $digest; + } + + private function getTagUrl($name) + { + if ($this->queriedContext === 'Official Image') { + return self::URI . '/_/' . $this->getRepo() . '?tab=tags&name=' . $name; + } + + return self::URI . '/r/' . $this->getRepo() . '/tags?name=' . $name; + } + + private function getImages($result) + { + $html = <<<EOD <table style="width:300px;"><thead><tr><th>Digest</th><th>OS/architecture</th></tr></thead></tbody> EOD; - foreach ($result->images as $image) { - $layersUrl = $this->getLayerUrl($result->name, $image->digest); - $id = $this->getShortDigestId($image->digest); + foreach ($result->images as $image) { + $layersUrl = $this->getLayerUrl($result->name, $image->digest); + $id = $this->getShortDigestId($image->digest); - $html .= <<<EOD + $html .= <<<EOD <tr> <td><a href="{$layersUrl}">{$id}</a></td> <td>{$image->os}/{$image->architecture}</td> </tr> EOD; - } + } - return $html . '</tbody></table>'; - } + return $html . '</tbody></table>'; + } - private function getShortDigestId($digest) { - $parts = explode(':', $digest); - return substr($parts[1], 0, 12); - } + private function getShortDigestId($digest) + { + $parts = explode(':', $digest); + return substr($parts[1], 0, 12); + } } diff --git a/bridges/DonnonsBridge.php b/bridges/DonnonsBridge.php index e499baae..a33a1013 100644 --- a/bridges/DonnonsBridge.php +++ b/bridges/DonnonsBridge.php @@ -1,79 +1,82 @@ <?php + /** * Retourne les dons d'une recherche filtrée sur le site Donnons.org * Example: https://donnons.org/Sport/Ile-de-France */ -class DonnonsBridge extends BridgeAbstract { - - const MAINTAINER = 'Binnette'; - const NAME = 'Donnons.org'; - const URI = 'https://donnons.org'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Retourne les dons depuis le site Donnons.org.'; - - const PARAMETERS = array( - array( - 'q' => array( - 'name' => 'Url de recherche', - 'required' => true, - 'exampleValue' => '/Sport/Ile-de-France', - 'pattern' => '\/.*', - 'title' => 'Faites une recherche sur le site. Puis copiez ici la fin de l’url. Doit commencer par /', - ), - 'p' => array( - 'name' => 'Nombre de pages à scanner', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 5, - 'title' => 'Indique le nombre de pages de donnons.org qui seront scannées' - ) - ) - ); - - public function collectData() { - $pages = $this->getInput('p'); - - for($i = 1; $i <= $pages; $i++) { - $this->collectDataByPage($i); - } - } - - private function collectDataByPage($page) { - $uri = $this->getPageURI($page); - - $html = getSimpleHTMLDOM($uri); - - $searchDiv = $html->find('div[id=search]', 0); - - if(!is_null($searchDiv)) { - $elements = $searchDiv->find('a.lst-annonce'); - foreach($elements as $element) { - $item = array(); - - // Lien vers le don - $item['uri'] = self::URI . $element->href; - // Id de l'objet - $item['uid'] = $element->getAttribute('data-id'); - - // Grab info from json - $jsonString = $element->find('script', 0)->innertext; - $json = json_decode($jsonString, true); - - $name = $json['name']; - $category = $json['category']; - $date = $json['availabilityStarts']; - $description = $json['description']; - $city = $json['availableAtOrFrom']['address']['addressLocality']; - $region = $json['availableAtOrFrom']['address']['addressRegion']; - - // Grab info from HTML - $imageSrc = $element->find('img.ima-center', 0)->getAttribute('src'); - // Use large image instead of small one - $imageSrc = str_replace('/xs/', '/lg/', $imageSrc); - $image = self::URI . $imageSrc; - $author = $element->find('div.avatar-holder', 0)->plaintext; - - $content = ' +class DonnonsBridge extends BridgeAbstract +{ + const MAINTAINER = 'Binnette'; + const NAME = 'Donnons.org'; + const URI = 'https://donnons.org'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Retourne les dons depuis le site Donnons.org.'; + + const PARAMETERS = [ + [ + 'q' => [ + 'name' => 'Url de recherche', + 'required' => true, + 'exampleValue' => '/Sport/Ile-de-France', + 'pattern' => '\/.*', + 'title' => 'Faites une recherche sur le site. Puis copiez ici la fin de l’url. Doit commencer par /', + ], + 'p' => [ + 'name' => 'Nombre de pages à scanner', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 5, + 'title' => 'Indique le nombre de pages de donnons.org qui seront scannées' + ] + ] + ]; + + public function collectData() + { + $pages = $this->getInput('p'); + + for ($i = 1; $i <= $pages; $i++) { + $this->collectDataByPage($i); + } + } + + private function collectDataByPage($page) + { + $uri = $this->getPageURI($page); + + $html = getSimpleHTMLDOM($uri); + + $searchDiv = $html->find('div[id=search]', 0); + + if (!is_null($searchDiv)) { + $elements = $searchDiv->find('a.lst-annonce'); + foreach ($elements as $element) { + $item = []; + + // Lien vers le don + $item['uri'] = self::URI . $element->href; + // Id de l'objet + $item['uid'] = $element->getAttribute('data-id'); + + // Grab info from json + $jsonString = $element->find('script', 0)->innertext; + $json = json_decode($jsonString, true); + + $name = $json['name']; + $category = $json['category']; + $date = $json['availabilityStarts']; + $description = $json['description']; + $city = $json['availableAtOrFrom']['address']['addressLocality']; + $region = $json['availableAtOrFrom']['address']['addressRegion']; + + // Grab info from HTML + $imageSrc = $element->find('img.ima-center', 0)->getAttribute('src'); + // Use large image instead of small one + $imageSrc = str_replace('/xs/', '/lg/', $imageSrc); + $image = self::URI . $imageSrc; + $author = $element->find('div.avatar-holder', 0)->plaintext; + + $content = ' <img style="margin-right:1em;" src="' . $image . '"> <div> <h1>' . $name . '</h1> @@ -84,42 +87,45 @@ class DonnonsBridge extends BridgeAbstract { </div> '; - // Titre du don - $item['title'] = '[' . $category . '] ' . $name; - $item['timestamp'] = $date; - $item['author'] = $author; - $item['content'] = $content; - $item['enclosures'] = array($image); - - $this->items[] = $item; - } - } - } - - private function getPageURI($page) { - $uri = $this->getURI(); - $haveQueryParams = strpos($uri, '?') !== false; - - if($haveQueryParams) { - return $uri . '&page=' . $page; - } else { - return $uri . '?page=' . $page; - } - } - - public function getURI() { - if(!is_null($this->getInput('q'))) { - return self::URI . $this->getInput('q'); - } - - return parent::getURI(); - } - - public function getName() { - if(!is_null($this->getInput('q'))) { - return 'Donnons.org - ' . $this->getInput('q'); - } - - return parent::getName(); - } + // Titre du don + $item['title'] = '[' . $category . '] ' . $name; + $item['timestamp'] = $date; + $item['author'] = $author; + $item['content'] = $content; + $item['enclosures'] = [$image]; + + $this->items[] = $item; + } + } + } + + private function getPageURI($page) + { + $uri = $this->getURI(); + $haveQueryParams = strpos($uri, '?') !== false; + + if ($haveQueryParams) { + return $uri . '&page=' . $page; + } else { + return $uri . '?page=' . $page; + } + } + + public function getURI() + { + if (!is_null($this->getInput('q'))) { + return self::URI . $this->getInput('q'); + } + + return parent::getURI(); + } + + public function getName() + { + if (!is_null($this->getInput('q'))) { + return 'Donnons.org - ' . $this->getInput('q'); + } + + return parent::getName(); + } } diff --git a/bridges/DribbbleBridge.php b/bridges/DribbbleBridge.php index 0bb0eee6..3957c9de 100644 --- a/bridges/DribbbleBridge.php +++ b/bridges/DribbbleBridge.php @@ -1,103 +1,110 @@ <?php -class DribbbleBridge extends BridgeAbstract { - const MAINTAINER = 'quentinus95'; - const NAME = 'Dribbble popular shots'; - const URI = 'https://dribbble.com'; - const CACHE_TIMEOUT = 1800; - const DESCRIPTION = 'Returns the newest popular shots from Dribbble.'; - - public function getIcon() { - return 'https://cdn.dribbble.com/assets/ +class DribbbleBridge extends BridgeAbstract +{ + const MAINTAINER = 'quentinus95'; + const NAME = 'Dribbble popular shots'; + const URI = 'https://dribbble.com'; + const CACHE_TIMEOUT = 1800; + const DESCRIPTION = 'Returns the newest popular shots from Dribbble.'; + + public function getIcon() + { + return 'https://cdn.dribbble.com/assets/ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico'; - } - - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI); - - $json = $this->loadEmbeddedJsonData($html); - - foreach($html->find('li[id^="screenshot-"]') as $shot) { - $item = array(); - - $additional_data = $this->findJsonForShot($shot, $json); - if ($additional_data === null) { - $item['uri'] = self::URI . $shot->find('a', 0)->href; - $item['title'] = $shot->find('.shot-title', 0)->plaintext; - } else { - $item['timestamp'] = strtotime($additional_data['published_at']); - $item['uri'] = self::URI . $additional_data['path']; - $item['title'] = $additional_data['title']; - } - - $item['author'] = trim($shot->find('.user-information .display-name', 0)->plaintext); - - $description = $shot->find('.comment', 0); - $item['content'] = $description === null ? '' : $description->plaintext; - - $preview_path = $shot->find('figure img', 1)->attr['data-srcset']; - $item['content'] .= $this->getImageTag($preview_path, $item['title']); - $item['enclosures'] = array($this->getFullSizeImagePath($preview_path)); - - $this->items[] = $item; - } - } - - private function loadEmbeddedJsonData($html){ - $json = array(); - $scripts = $html->find('script'); - - foreach($scripts as $script) { - if(strpos($script->innertext, 'newestShots') !== false) { - // fix single quotes - $script->innertext = preg_replace('/\'(.*)\'(,?)$/im', '"\1"\2', $script->innertext); - - // fix JavaScript JSON (why do they not adhere to the standard?) - $script->innertext = preg_replace('/^(\s*)(\w+):/im', '\1"\2":', $script->innertext); - - // fix relative dates, so they are recognized by strtotime - $script->innertext = preg_replace('/"about ([0-9]+ hours? ago)"(,?)$/im', '"\1"\2', $script->innertext); - - // find beginning of JSON array - $start = strpos($script->innertext, '['); - - // find end of JSON array, compensate for missing character! - $end = strpos($script->innertext, '];') + 1; - - // convert JSON to PHP array - $json = json_decode(substr($script->innertext, $start, $end - $start), true); - break; - } - } - - return $json; - } - - private function findJsonForShot($shot, $json){ - foreach($json as $element) { - if(strpos($shot->getAttribute('id'), (string)$element['id']) !== false) { - return $element; - } - } - - return null; - } - - private function getImageTag($preview_path, $title){ - return sprintf( - '<br /> <a href="%s"><img srcset="%s" alt="%s" /></a>', - $this->getFullSizeImagePath($preview_path), - $preview_path, - $title - ); - } - - private function getFullSizeImagePath($preview_path){ - // Get last image from srcset - $src_set_urls = explode(',', $preview_path); - $url = end($src_set_urls); - $url = explode(' ', $url)[1]; - - return htmlspecialchars_decode($url); - } + } + + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); + + $json = $this->loadEmbeddedJsonData($html); + + foreach ($html->find('li[id^="screenshot-"]') as $shot) { + $item = []; + + $additional_data = $this->findJsonForShot($shot, $json); + if ($additional_data === null) { + $item['uri'] = self::URI . $shot->find('a', 0)->href; + $item['title'] = $shot->find('.shot-title', 0)->plaintext; + } else { + $item['timestamp'] = strtotime($additional_data['published_at']); + $item['uri'] = self::URI . $additional_data['path']; + $item['title'] = $additional_data['title']; + } + + $item['author'] = trim($shot->find('.user-information .display-name', 0)->plaintext); + + $description = $shot->find('.comment', 0); + $item['content'] = $description === null ? '' : $description->plaintext; + + $preview_path = $shot->find('figure img', 1)->attr['data-srcset']; + $item['content'] .= $this->getImageTag($preview_path, $item['title']); + $item['enclosures'] = [$this->getFullSizeImagePath($preview_path)]; + + $this->items[] = $item; + } + } + + private function loadEmbeddedJsonData($html) + { + $json = []; + $scripts = $html->find('script'); + + foreach ($scripts as $script) { + if (strpos($script->innertext, 'newestShots') !== false) { + // fix single quotes + $script->innertext = preg_replace('/\'(.*)\'(,?)$/im', '"\1"\2', $script->innertext); + + // fix JavaScript JSON (why do they not adhere to the standard?) + $script->innertext = preg_replace('/^(\s*)(\w+):/im', '\1"\2":', $script->innertext); + + // fix relative dates, so they are recognized by strtotime + $script->innertext = preg_replace('/"about ([0-9]+ hours? ago)"(,?)$/im', '"\1"\2', $script->innertext); + + // find beginning of JSON array + $start = strpos($script->innertext, '['); + + // find end of JSON array, compensate for missing character! + $end = strpos($script->innertext, '];') + 1; + + // convert JSON to PHP array + $json = json_decode(substr($script->innertext, $start, $end - $start), true); + break; + } + } + + return $json; + } + + private function findJsonForShot($shot, $json) + { + foreach ($json as $element) { + if (strpos($shot->getAttribute('id'), (string)$element['id']) !== false) { + return $element; + } + } + + return null; + } + + private function getImageTag($preview_path, $title) + { + return sprintf( + '<br /> <a href="%s"><img srcset="%s" alt="%s" /></a>', + $this->getFullSizeImagePath($preview_path), + $preview_path, + $title + ); + } + + private function getFullSizeImagePath($preview_path) + { + // Get last image from srcset + $src_set_urls = explode(',', $preview_path); + $url = end($src_set_urls); + $url = explode(' ', $url)[1]; + + return htmlspecialchars_decode($url); + } } diff --git a/bridges/Drive2ruBridge.php b/bridges/Drive2ruBridge.php index 60df97d7..00e9e957 100644 --- a/bridges/Drive2ruBridge.php +++ b/bridges/Drive2ruBridge.php @@ -1,205 +1,232 @@ <?php -class Drive2ruBridge extends BridgeAbstract { - const MAINTAINER = 'dotter-ak'; - const NAME = 'Drive2.ru'; - const URI = 'https://drive2.ru/'; - const DESCRIPTION = 'Лента новостей и тестдрайвов, бортжурналов по выбранной марке или модели + +class Drive2ruBridge extends BridgeAbstract +{ + const MAINTAINER = 'dotter-ak'; + const NAME = 'Drive2.ru'; + const URI = 'https://drive2.ru/'; + const DESCRIPTION = 'Лента новостей и тестдрайвов, бортжурналов по выбранной марке или модели (также работает с фильтром по категориям), блогов пользователей и публикаций по темам.'; - const PARAMETERS = array( - 'Новости и тест-драйвы' => array(), - 'Бортжурналы (По модели или марке)' => array( - 'url' => array( - 'name' => 'Ссылка на страницу с бортжурналом', - 'type' => 'text', - 'required' => true, - 'title' => 'Например: https://www.drive2.ru/experience/suzuki/g4895/', - 'exampleValue' => 'https://www.drive2.ru/experience/suzuki/g4895/' - ), - ), - 'Личные блоги' => array( - 'username' => array( - 'name' => 'Никнейм пользователя на сайте', - 'type' => 'text', - 'required' => true, - 'title' => 'Например: Mickey', - 'exampleValue' => 'Mickey' - ) - ), - 'Публикации по темам (Стоит почитать)' => array( - 'topic' => array( - 'name' => 'Темы', - 'type' => 'list', - 'values' => array( - 'Автозвук' => '16', - 'Автомобильный дизайн' => '10', - 'Автоспорт' => '11', - 'Автошоу, музеи, выставки' => '12', - 'Безопасность' => '18', - 'Беспилотные автомобили' => '15', - 'Видеосюжеты' => '20', - 'Вне дорог' => '21', - 'Встречи' => '22', - 'Выбор и покупка машины' => '23', - 'Гаджеты' => '30', - 'Гибридные машины' => '32', - 'Грузовики, автобусы, спецтехника' => '31', - 'Доработка интерьера' => '35', - 'Законодательство' => '40', - 'История автомобилестроения' => '50', - 'Мототехника' => '60', - 'Новые модели и концепты' => '85', - 'Обучение вождению' => '70', - 'Путешествия' => '80', - 'Ремонт и обслуживание' => '90', - 'Реставрация ретро-авто' => '91', - 'Сделай сам' => '104', - 'Смешное' => '103', - 'Спорткары' => '102', - 'Стайлинг' => '101', - 'Тест-драйвы' => '110', - 'Тюнинг' => '111', - 'Фотосессии' => '120', - 'Шины и диски' => '140', - 'Электрика' => '130', - 'Электромобили' => '131' - ), - 'defaultValue' => '16', - ) - ), - 'global' => array( - 'full_articles' => array( - 'name' => 'Загружать в ленту полный текст', - 'type' => 'checkbox' - ) - ) - ); + const PARAMETERS = [ + 'Новости и тест-драйвы' => [], + 'Бортжурналы (По модели или марке)' => [ + 'url' => [ + 'name' => 'Ссылка на страницу с бортжурналом', + 'type' => 'text', + 'required' => true, + 'title' => 'Например: https://www.drive2.ru/experience/suzuki/g4895/', + 'exampleValue' => 'https://www.drive2.ru/experience/suzuki/g4895/' + ], + ], + 'Личные блоги' => [ + 'username' => [ + 'name' => 'Никнейм пользователя на сайте', + 'type' => 'text', + 'required' => true, + 'title' => 'Например: Mickey', + 'exampleValue' => 'Mickey' + ] + ], + 'Публикации по темам (Стоит почитать)' => [ + 'topic' => [ + 'name' => 'Темы', + 'type' => 'list', + 'values' => [ + 'Автозвук' => '16', + 'Автомобильный дизайн' => '10', + 'Автоспорт' => '11', + 'Автошоу, музеи, выставки' => '12', + 'Безопасность' => '18', + 'Беспилотные автомобили' => '15', + 'Видеосюжеты' => '20', + 'Вне дорог' => '21', + 'Встречи' => '22', + 'Выбор и покупка машины' => '23', + 'Гаджеты' => '30', + 'Гибридные машины' => '32', + 'Грузовики, автобусы, спецтехника' => '31', + 'Доработка интерьера' => '35', + 'Законодательство' => '40', + 'История автомобилестроения' => '50', + 'Мототехника' => '60', + 'Новые модели и концепты' => '85', + 'Обучение вождению' => '70', + 'Путешествия' => '80', + 'Ремонт и обслуживание' => '90', + 'Реставрация ретро-авто' => '91', + 'Сделай сам' => '104', + 'Смешное' => '103', + 'Спорткары' => '102', + 'Стайлинг' => '101', + 'Тест-драйвы' => '110', + 'Тюнинг' => '111', + 'Фотосессии' => '120', + 'Шины и диски' => '140', + 'Электрика' => '130', + 'Электромобили' => '131' + ], + 'defaultValue' => '16', + ] + ], + 'global' => [ + 'full_articles' => [ + 'name' => 'Загружать в ленту полный текст', + 'type' => 'checkbox' + ] + ] + ]; - private $title; + private $title; - private function getUserContent($url) { - $html = getSimpleHTMLDOM($url); - $this->title = $html->find('title', 0)->innertext; - $articles = $html->find('div.js-entity'); - foreach ($articles as $article) { - $item = array(); - $item['title'] = $article->find('a.c-link--text', 0)->plaintext; - $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 0)->href); - if($this->getInput('full_articles')) { - $item['content'] = $this->addCommentsLink( - $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.c-post__body', 0))->innertext, - $item['uri'] - ); - } else { - $item['content'] = $this->addReadMoreLink($article->find('div.c-post-preview__lead', 0), $item['uri']); - } - $item['author'] = $article->find('a.c-username--wrap', 0)->plaintext; - if (!is_null($article->find('img', 1))) $item['enclosures'][] = $article->find('img', 1)->src; - $this->items[] = $item; - } - } + private function getUserContent($url) + { + $html = getSimpleHTMLDOM($url); + $this->title = $html->find('title', 0)->innertext; + $articles = $html->find('div.js-entity'); + foreach ($articles as $article) { + $item = []; + $item['title'] = $article->find('a.c-link--text', 0)->plaintext; + $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 0)->href); + if ($this->getInput('full_articles')) { + $item['content'] = $this->addCommentsLink( + $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.c-post__body', 0))->innertext, + $item['uri'] + ); + } else { + $item['content'] = $this->addReadMoreLink($article->find('div.c-post-preview__lead', 0), $item['uri']); + } + $item['author'] = $article->find('a.c-username--wrap', 0)->plaintext; + if (!is_null($article->find('img', 1))) { + $item['enclosures'][] = $article->find('img', 1)->src; + } + $this->items[] = $item; + } + } - private function getLogbooksContent($url) { - $html = getSimpleHTMLDOM($url); - $this->title = $html->find('title', 0)->innertext; - $articles = $html->find('div.js-entity'); - foreach ($articles as $article) { - $item = array(); - $item['title'] = $article->find('a.c-link--text', 1)->plaintext; - $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 1)->href); - if($this->getInput('full_articles')) { - $item['content'] = $this->addCommentsLink( - $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.c-post__body', 0))->innertext, - $item['uri'] - ); - } else { - $item['content'] = $this->addReadMoreLink($article->find('div.c-post-preview__lead', 0), $item['uri']); - } - $item['author'] = $article->find('a.c-username--wrap', 0)->plaintext; - if (!is_null($article->find('img', 1))) $item['enclosures'][] = $article->find('img', 1)->src; - $this->items[] = $item; - } - } + private function getLogbooksContent($url) + { + $html = getSimpleHTMLDOM($url); + $this->title = $html->find('title', 0)->innertext; + $articles = $html->find('div.js-entity'); + foreach ($articles as $article) { + $item = []; + $item['title'] = $article->find('a.c-link--text', 1)->plaintext; + $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 1)->href); + if ($this->getInput('full_articles')) { + $item['content'] = $this->addCommentsLink( + $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.c-post__body', 0))->innertext, + $item['uri'] + ); + } else { + $item['content'] = $this->addReadMoreLink($article->find('div.c-post-preview__lead', 0), $item['uri']); + } + $item['author'] = $article->find('a.c-username--wrap', 0)->plaintext; + if (!is_null($article->find('img', 1))) { + $item['enclosures'][] = $article->find('img', 1)->src; + } + $this->items[] = $item; + } + } - private function getNews() { - $html = getSimpleHTMLDOM('https://www.drive2.ru/editorial/'); - $this->title = $html->find('title', 0)->innertext; - $articles = $html->find('div.c-article-card'); - foreach ($articles as $article) { - $item = array(); - $item['title'] = $article->find('a.c-link--text', 0)->plaintext; - $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 0)->href); - if($this->getInput('full_articles')) { - $item['content'] = $this->addCommentsLink( - $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.article', 0))->innertext, - $item['uri'] - ); - } else { - $item['content'] = $this->addReadMoreLink($article->find('div.c-article-card__lead', 0), $item['uri']); - } - $item['author'] = 'Новости и тест-драйвы на Drive2.ru'; - if (!is_null($article->find('img', 0))) $item['enclosures'][] = $article->find('img', 0)->src; - $this->items[] = $item; - } - } + private function getNews() + { + $html = getSimpleHTMLDOM('https://www.drive2.ru/editorial/'); + $this->title = $html->find('title', 0)->innertext; + $articles = $html->find('div.c-article-card'); + foreach ($articles as $article) { + $item = []; + $item['title'] = $article->find('a.c-link--text', 0)->plaintext; + $item['uri'] = urljoin(self::URI, $article->find('a.c-link--text', 0)->href); + if ($this->getInput('full_articles')) { + $item['content'] = $this->addCommentsLink( + $this->adjustContent(getSimpleHTMLDomCached($item['uri'])->find('div.article', 0))->innertext, + $item['uri'] + ); + } else { + $item['content'] = $this->addReadMoreLink($article->find('div.c-article-card__lead', 0), $item['uri']); + } + $item['author'] = 'Новости и тест-драйвы на Drive2.ru'; + if (!is_null($article->find('img', 0))) { + $item['enclosures'][] = $article->find('img', 0)->src; + } + $this->items[] = $item; + } + } - private function adjustContent($content) { - foreach ($content->find('div.o-group') as $node) - $node->outertext = ''; - foreach($content->find('div, span') as $attrs) - foreach ($attrs->getAllAttributes() as $attr => $val) - $attrs->removeAttribute($attr); - foreach ($content->getElementsByTagName('figcaption') as $attrs) - $attrs->setAttribute( - 'style', - 'font-style: italic; font-size: small; margin: 0 100px 75px;'); - foreach ($content->find('script') as $node) - $node->outertext = ''; - foreach ($content->find('iframe') as $node) { - preg_match('/embed\/(.*?)\?/', $node->src, $match); - $node->outertext = '<a href="https://www.youtube.com/watch?v=' . $match[1] . - '">https://www.youtube.com/watch?v=' . $match[1] . '</a>'; - } - return $content; - } + private function adjustContent($content) + { + foreach ($content->find('div.o-group') as $node) { + $node->outertext = ''; + } + foreach ($content->find('div, span') as $attrs) { + foreach ($attrs->getAllAttributes() as $attr => $val) { + $attrs->removeAttribute($attr); + } + } + foreach ($content->getElementsByTagName('figcaption') as $attrs) { + $attrs->setAttribute( + 'style', + 'font-style: italic; font-size: small; margin: 0 100px 75px;' + ); + } + foreach ($content->find('script') as $node) { + $node->outertext = ''; + } + foreach ($content->find('iframe') as $node) { + preg_match('/embed\/(.*?)\?/', $node->src, $match); + $node->outertext = '<a href="https://www.youtube.com/watch?v=' . $match[1] . + '">https://www.youtube.com/watch?v=' . $match[1] . '</a>'; + } + return $content; + } - private function addCommentsLink ($content, $url) { - return $content . '<br><a href="' . $url . '#comments">Перейти к комментариям</a>'; - } + private function addCommentsLink($content, $url) + { + return $content . '<br><a href="' . $url . '#comments">Перейти к комментариям</a>'; + } - private function addReadMoreLink ($content, $url) { - if (!is_null($content)) - return preg_replace('!\s+!', ' ', str_replace('Читать дальше', '', $content->plaintext)) . - '<br><a href="' . $url . '">Читать далее</a>'; - else return ''; - } + private function addReadMoreLink($content, $url) + { + if (!is_null($content)) { + return preg_replace('!\s+!', ' ', str_replace('Читать дальше', '', $content->plaintext)) . + '<br><a href="' . $url . '">Читать далее</a>'; + } else { + return ''; + } + } - public function collectData() { - switch($this->queriedContext) { - default: - case 'Новости и тест-драйвы': - $this->getNews(); - break; - case 'Бортжурналы (По модели или марке)': - if (!preg_match('/^https:\/\/www.drive2.ru\/experience/', $this->getInput('url'))) - returnServerError('Invalid url'); - $this->getLogbooksContent($this->getInput('url')); - break; - case 'Личные блоги': - if (!preg_match('/^[a-zA-Z0-9-]{3,16}$/', $this->getInput('username'))) - returnServerError('Invalid username'); - $this->getUserContent('https://www.drive2.ru/users/' . $this->getInput('username')); - break; - case 'Публикации по темам (Стоит почитать)': - $this->getUserContent('https://www.drive2.ru/topics/' . $this->getInput('topic')); - break; - } - } + public function collectData() + { + switch ($this->queriedContext) { + default: + case 'Новости и тест-драйвы': + $this->getNews(); + break; + case 'Бортжурналы (По модели или марке)': + if (!preg_match('/^https:\/\/www.drive2.ru\/experience/', $this->getInput('url'))) { + returnServerError('Invalid url'); + } + $this->getLogbooksContent($this->getInput('url')); + break; + case 'Личные блоги': + if (!preg_match('/^[a-zA-Z0-9-]{3,16}$/', $this->getInput('username'))) { + returnServerError('Invalid username'); + } + $this->getUserContent('https://www.drive2.ru/users/' . $this->getInput('username')); + break; + case 'Публикации по темам (Стоит почитать)': + $this->getUserContent('https://www.drive2.ru/topics/' . $this->getInput('topic')); + break; + } + } - public function getName() { - return $this->title ?: parent::getName(); - } + public function getName() + { + return $this->title ?: parent::getName(); + } - public function getIcon() { - return 'https://www.drive2.ru/favicon.ico'; - } + public function getIcon() + { + return 'https://www.drive2.ru/favicon.ico'; + } } diff --git a/bridges/DuckDuckGoBridge.php b/bridges/DuckDuckGoBridge.php index 378996da..5edf248b 100644 --- a/bridges/DuckDuckGoBridge.php +++ b/bridges/DuckDuckGoBridge.php @@ -1,42 +1,44 @@ <?php -class DuckDuckGoBridge extends BridgeAbstract { - const MAINTAINER = 'Astalaseven'; - const NAME = 'DuckDuckGo'; - const URI = 'https://duckduckgo.com/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Returns results from DuckDuckGo.'; +class DuckDuckGoBridge extends BridgeAbstract +{ + const MAINTAINER = 'Astalaseven'; + const NAME = 'DuckDuckGo'; + const URI = 'https://duckduckgo.com/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Returns results from DuckDuckGo.'; - const SORT_DATE = '+sort:date'; - const SORT_RELEVANCE = ''; + const SORT_DATE = '+sort:date'; + const SORT_RELEVANCE = ''; - const PARAMETERS = array( array( - 'u' => array( - 'name' => 'keyword', - 'exampleValue' => 'duck', - 'required' => true - ), - 'sort' => array( - 'name' => 'sort by', - 'type' => 'list', - 'required' => false, - 'values' => array( - 'date' => self::SORT_DATE, - 'relevance' => self::SORT_RELEVANCE - ), - 'defaultValue' => self::SORT_DATE - ) - )); + const PARAMETERS = [ [ + 'u' => [ + 'name' => 'keyword', + 'exampleValue' => 'duck', + 'required' => true + ], + 'sort' => [ + 'name' => 'sort by', + 'type' => 'list', + 'required' => false, + 'values' => [ + 'date' => self::SORT_DATE, + 'relevance' => self::SORT_RELEVANCE + ], + 'defaultValue' => self::SORT_DATE + ] + ]]; - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI . 'html/?kd=-1&q=' . $this->getInput('u') . $this->getInput('sort')); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI . 'html/?kd=-1&q=' . $this->getInput('u') . $this->getInput('sort')); - foreach($html->find('div.result') as $element) { - $item = array(); - $item['uri'] = $element->find('a.result__a', 0)->href; - $item['title'] = $element->find('h2.result__title', 0)->plaintext; - $item['content'] = $element->find('a.result__snippet', 0)->plaintext; - $this->items[] = $item; - } - } + foreach ($html->find('div.result') as $element) { + $item = []; + $item['uri'] = $element->find('a.result__a', 0)->href; + $item['title'] = $element->find('h2.result__title', 0)->plaintext; + $item['content'] = $element->find('a.result__snippet', 0)->plaintext; + $this->items[] = $item; + } + } } diff --git a/bridges/EZTVBridge.php b/bridges/EZTVBridge.php index 956776ed..cf969cb5 100644 --- a/bridges/EZTVBridge.php +++ b/bridges/EZTVBridge.php @@ -1,111 +1,118 @@ <?php -class EZTVBridge extends BridgeAbstract { - const MAINTAINER = 'alexAubin'; - const NAME = 'EZTV'; - const URI = 'https://eztv.re/'; - const DESCRIPTION = 'Returns list of torrents for specific show(s) +class EZTVBridge extends BridgeAbstract +{ + const MAINTAINER = 'alexAubin'; + const NAME = 'EZTV'; + const URI = 'https://eztv.re/'; + const DESCRIPTION = 'Returns list of torrents for specific show(s) on EZTV. Get IMDB IDs from IMDB.'; - const PARAMETERS = array( - array( - 'ids' => array( - 'name' => 'Show IMDB IDs', - 'exampleValue' => '8740790,1733785', - 'required' => true, - 'title' => 'One or more IMDB show IDs (can be found in the IMDB show URL)' - ), - 'no480' => array( - 'name' => 'No 480p', - 'type' => 'checkbox', - 'title' => 'Activate to exclude 480p torrents' - ), - 'no720' => array( - 'name' => 'No 720p', - 'type' => 'checkbox', - 'title' => 'Activate to exclude 720p torrents' - ), - 'no1080' => array( - 'name' => 'No 1080p', - 'type' => 'checkbox', - 'title' => 'Activate to exclude 1080p torrents' - ), - 'no2160' => array( - 'name' => 'No 2160p', - 'type' => 'checkbox', - 'title' => 'Activate to exclude 2160p torrents' - ), - 'noUnknownRes' => array( - 'name' => 'No Unknown resolution', - 'type' => 'checkbox', - 'title' => 'Activate to exclude unknown resolution torrents' - ), - ) - ); + const PARAMETERS = [ + [ + 'ids' => [ + 'name' => 'Show IMDB IDs', + 'exampleValue' => '8740790,1733785', + 'required' => true, + 'title' => 'One or more IMDB show IDs (can be found in the IMDB show URL)' + ], + 'no480' => [ + 'name' => 'No 480p', + 'type' => 'checkbox', + 'title' => 'Activate to exclude 480p torrents' + ], + 'no720' => [ + 'name' => 'No 720p', + 'type' => 'checkbox', + 'title' => 'Activate to exclude 720p torrents' + ], + 'no1080' => [ + 'name' => 'No 1080p', + 'type' => 'checkbox', + 'title' => 'Activate to exclude 1080p torrents' + ], + 'no2160' => [ + 'name' => 'No 2160p', + 'type' => 'checkbox', + 'title' => 'Activate to exclude 2160p torrents' + ], + 'noUnknownRes' => [ + 'name' => 'No Unknown resolution', + 'type' => 'checkbox', + 'title' => 'Activate to exclude unknown resolution torrents' + ], + ] + ]; - // Shamelessly lifted from https://stackoverflow.com/a/2510459 - protected function formatBytes($bytes, $precision = 2) { - $units = array('B', 'KB', 'MB', 'GB', 'TB'); + // Shamelessly lifted from https://stackoverflow.com/a/2510459 + protected function formatBytes($bytes, $precision = 2) + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); - $bytes /= pow(1024, $pow); + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + $bytes /= pow(1024, $pow); - return round($bytes, $precision) . ' ' . $units[$pow]; - } + return round($bytes, $precision) . ' ' . $units[$pow]; + } - protected function getItemFromTorrent($torrent){ - $item = array(); - $item['uri'] = $torrent->episode_url; - $item['author'] = $torrent->imdb_id; - $item['timestamp'] = date('d F Y H:i:s', $torrent->date_released_unix); - $item['title'] = $torrent->title; - $item['enclosures'][] = $torrent->torrent_url; + protected function getItemFromTorrent($torrent) + { + $item = []; + $item['uri'] = $torrent->episode_url; + $item['author'] = $torrent->imdb_id; + $item['timestamp'] = date('d F Y H:i:s', $torrent->date_released_unix); + $item['title'] = $torrent->title; + $item['enclosures'][] = $torrent->torrent_url; - $thumbnailUri = 'https:' . $torrent->small_screenshot; - $torrentSize = $this->formatBytes($torrent->size_bytes); + $thumbnailUri = 'https:' . $torrent->small_screenshot; + $torrentSize = $this->formatBytes($torrent->size_bytes); - $item['content'] = $torrent->filename . '<br>File size: ' - . $torrentSize . '<br><a href="' . $torrent->magnet_url - . '">magnet link</a><br><a href="' . $torrent->torrent_url - . '">torrent link</a><br><img src="' . $thumbnailUri . '" />'; + $item['content'] = $torrent->filename . '<br>File size: ' + . $torrentSize . '<br><a href="' . $torrent->magnet_url + . '">magnet link</a><br><a href="' . $torrent->torrent_url + . '">torrent link</a><br><img src="' . $thumbnailUri . '" />'; - return $item; - } + return $item; + } - private static function compareDate($torrent1, $torrent2) { - return (strtotime($torrent1['timestamp']) < strtotime($torrent2['timestamp']) ? 1 : -1); - } + private static function compareDate($torrent1, $torrent2) + { + return (strtotime($torrent1['timestamp']) < strtotime($torrent2['timestamp']) ? 1 : -1); + } - public function collectData(){ - $showIds = explode(',', $this->getInput('ids')); + public function collectData() + { + $showIds = explode(',', $this->getInput('ids')); - foreach($showIds as $showId) { - $eztvUri = $this->getURI() . 'api/get-torrents?imdb_id=' . $showId; - $content = getContents($eztvUri); - $torrents = json_decode($content)->torrents; - foreach($torrents as $torrent) { - $title = $torrent->title; - $regex480 = '/480p/'; - $regex720 = '/720p/'; - $regex1080 = '/1080p/'; - $regex2160 = '/2160p/'; - $regexUnknown = '/(480p|720p|1080p|2160p)/'; - // Skip unwanted resolution torrents - if ((preg_match($regex480, $title) === 1 && $this->getInput('no480')) - || (preg_match($regex720, $title) === 1 && $this->getInput('no720')) - || (preg_match($regex1080, $title) === 1 && $this->getInput('no1080')) - || (preg_match($regex2160, $title) === 1 && $this->getInput('no2160')) - || (preg_match($regexUnknown, $title) !== 1 && $this->getInput('noUnknownRes'))) { - continue; - } + foreach ($showIds as $showId) { + $eztvUri = $this->getURI() . 'api/get-torrents?imdb_id=' . $showId; + $content = getContents($eztvUri); + $torrents = json_decode($content)->torrents; + foreach ($torrents as $torrent) { + $title = $torrent->title; + $regex480 = '/480p/'; + $regex720 = '/720p/'; + $regex1080 = '/1080p/'; + $regex2160 = '/2160p/'; + $regexUnknown = '/(480p|720p|1080p|2160p)/'; + // Skip unwanted resolution torrents + if ( + (preg_match($regex480, $title) === 1 && $this->getInput('no480')) + || (preg_match($regex720, $title) === 1 && $this->getInput('no720')) + || (preg_match($regex1080, $title) === 1 && $this->getInput('no1080')) + || (preg_match($regex2160, $title) === 1 && $this->getInput('no2160')) + || (preg_match($regexUnknown, $title) !== 1 && $this->getInput('noUnknownRes')) + ) { + continue; + } - $this->items[] = $this->getItemFromTorrent($torrent); - } - } + $this->items[] = $this->getItemFromTorrent($torrent); + } + } - // Sort all torrents in array by date - usort($this->items, array('EZTVBridge', 'compareDate')); - } + // Sort all torrents in array by date + usort($this->items, ['EZTVBridge', 'compareDate']); + } } diff --git a/bridges/EconomistBridge.php b/bridges/EconomistBridge.php index 973711a5..79314a0d 100644 --- a/bridges/EconomistBridge.php +++ b/bridges/EconomistBridge.php @@ -1,143 +1,149 @@ <?php -class EconomistBridge extends FeedExpander { - const MAINTAINER = 'bockiii'; - const NAME = 'Economist Bridge'; - const URI = 'https://www.economist.com/'; - const CACHE_TIMEOUT = 3600; //1hour - const DESCRIPTION = 'Returns the latest articles for the selected category'; +class EconomistBridge extends FeedExpander +{ + const MAINTAINER = 'bockiii'; + const NAME = 'Economist Bridge'; + const URI = 'https://www.economist.com/'; + const CACHE_TIMEOUT = 3600; //1hour + const DESCRIPTION = 'Returns the latest articles for the selected category'; - const PARAMETERS = array( - 'global' => array( - 'limit' => array( - 'name' => 'Feed Item Limit', - 'required' => true, - 'type' => 'number', - 'defaultValue' => 10, - 'title' => 'Maximum number of returned feed items. Maximum 30, default 10' - ) - ), - 'Topics' => array( - 'topic' => array( - 'name' => 'Topics', - 'type' => 'list', - 'title' => 'Select a Topic', - 'defaultValue' => 'latest', - 'values' => array( - 'Latest' => 'latest', - 'The world this week' => 'the-world-this-week', - 'Letters' => 'letters', - 'Leaders' => 'leaders', - 'Briefings' => 'briefing', - 'Special reports' => 'special-report', - 'Britain' => 'britain', - 'Europe' => 'europe', - 'United States' => 'united-states', - 'The Americas' => 'the-americas', - 'Middle East and Africa' => 'middle-east-and-africa', - 'Asia' => 'asia', - 'China' => 'china', - 'International' => 'international', - 'Business' => 'business', - 'Finance and economics' => 'finance-and-economics', - 'Science and technology' => 'science-and-technology', - 'Books and arts' => 'books-and-arts', - 'Obituaries' => 'obituary', - 'Graphic detail' => 'graphic-detail', - 'Indicators' => 'economic-and-financial-indicators', - ) - ) - ), - 'Blogs' => array( - 'blog' => array( - 'name' => 'Blogs', - 'type' => 'list', - 'title' => 'Select a Blog', - 'values' => array( - 'Bagehots notebook' => 'bagehots-notebook', - 'Bartleby' => 'bartleby', - 'Buttonwoods notebook' => 'buttonwoods-notebook', - 'Charlemagnes notebook' => 'charlemagnes-notebook', - 'Democracy in America' => 'democracy-in-america', - 'Erasmus' => 'erasmus', - 'Free exchange' => 'free-exchange', - 'Game theory' => 'game-theory', - 'Gulliver' => 'gulliver', - 'Kaffeeklatsch' => 'kaffeeklatsch', - 'Prospero' => 'prospero', - 'The Economist Explains' => 'the-economist-explains', - ) - ) - ) - ); + const PARAMETERS = [ + 'global' => [ + 'limit' => [ + 'name' => 'Feed Item Limit', + 'required' => true, + 'type' => 'number', + 'defaultValue' => 10, + 'title' => 'Maximum number of returned feed items. Maximum 30, default 10' + ] + ], + 'Topics' => [ + 'topic' => [ + 'name' => 'Topics', + 'type' => 'list', + 'title' => 'Select a Topic', + 'defaultValue' => 'latest', + 'values' => [ + 'Latest' => 'latest', + 'The world this week' => 'the-world-this-week', + 'Letters' => 'letters', + 'Leaders' => 'leaders', + 'Briefings' => 'briefing', + 'Special reports' => 'special-report', + 'Britain' => 'britain', + 'Europe' => 'europe', + 'United States' => 'united-states', + 'The Americas' => 'the-americas', + 'Middle East and Africa' => 'middle-east-and-africa', + 'Asia' => 'asia', + 'China' => 'china', + 'International' => 'international', + 'Business' => 'business', + 'Finance and economics' => 'finance-and-economics', + 'Science and technology' => 'science-and-technology', + 'Books and arts' => 'books-and-arts', + 'Obituaries' => 'obituary', + 'Graphic detail' => 'graphic-detail', + 'Indicators' => 'economic-and-financial-indicators', + ] + ] + ], + 'Blogs' => [ + 'blog' => [ + 'name' => 'Blogs', + 'type' => 'list', + 'title' => 'Select a Blog', + 'values' => [ + 'Bagehots notebook' => 'bagehots-notebook', + 'Bartleby' => 'bartleby', + 'Buttonwoods notebook' => 'buttonwoods-notebook', + 'Charlemagnes notebook' => 'charlemagnes-notebook', + 'Democracy in America' => 'democracy-in-america', + 'Erasmus' => 'erasmus', + 'Free exchange' => 'free-exchange', + 'Game theory' => 'game-theory', + 'Gulliver' => 'gulliver', + 'Kaffeeklatsch' => 'kaffeeklatsch', + 'Prospero' => 'prospero', + 'The Economist Explains' => 'the-economist-explains', + ] + ] + ] + ]; - public function collectData(){ - // get if topics or blogs were selected and store the selected category - switch ($this->queriedContext) { - case 'Topics': - $category = $this->getInput('topic'); - break; - case 'Blogs': - $category = $this->getInput('blog'); - break; - default: - $category = 'latest'; - } - // limit the returned articles to 30 at max - if ((int)$this->getInput('limit') <= 30) { - $limit = (int)$this->getInput('limit'); - } else { - $limit = 30; - } + public function collectData() + { + // get if topics or blogs were selected and store the selected category + switch ($this->queriedContext) { + case 'Topics': + $category = $this->getInput('topic'); + break; + case 'Blogs': + $category = $this->getInput('blog'); + break; + default: + $category = 'latest'; + } + // limit the returned articles to 30 at max + if ((int)$this->getInput('limit') <= 30) { + $limit = (int)$this->getInput('limit'); + } else { + $limit = 30; + } - $this->collectExpandableDatas('https://www.economist.com/' . $category . '/rss.xml', $limit); - } + $this->collectExpandableDatas('https://www.economist.com/' . $category . '/rss.xml', $limit); + } - protected function parseItem($feedItem){ - $item = parent::parseItem($feedItem); - $article = getSimpleHTMLDOM($item['uri']); - // before the article can be added, it needs to be cleaned up, thus, the extra function - // We also need to distinguish between old style and new style articles - if ($article->find('article', 0)->getAttribute('data-test-id') == 'Article') { - $contentNode = 'div.layout-article-body'; - $imgNode = 'div.article__lead-image'; - $categoryNode = 'span.article__subheadline'; - } elseif ($article->find('article', 0)->getAttribute('data-test-id') === 'NewArticle') { - $contentNode = 'section'; - $imgNode = 'figure.css-12eysrk.e3y6nua0'; - $categoryNode = 'span.ern1uyf0'; - } else { - return; - } + protected function parseItem($feedItem) + { + $item = parent::parseItem($feedItem); + $article = getSimpleHTMLDOM($item['uri']); + // before the article can be added, it needs to be cleaned up, thus, the extra function + // We also need to distinguish between old style and new style articles + if ($article->find('article', 0)->getAttribute('data-test-id') == 'Article') { + $contentNode = 'div.layout-article-body'; + $imgNode = 'div.article__lead-image'; + $categoryNode = 'span.article__subheadline'; + } elseif ($article->find('article', 0)->getAttribute('data-test-id') === 'NewArticle') { + $contentNode = 'section'; + $imgNode = 'figure.css-12eysrk.e3y6nua0'; + $categoryNode = 'span.ern1uyf0'; + } else { + return; + } - $item['content'] = $this->cleanContent($article, $contentNode); - // only the article lead image is retained if it's there - if (!is_null($article->find($imgNode, 0))) { - $item['enclosures'][] = $article->find($imgNode, 0)->find('img', 0)->getAttribute('src'); - } else { - $item['enclosures'][] = ''; - } - // add the subheadline as category. This will create a link in new articles - // and a text in old articles - $item['categories'][] = $article->find($categoryNode, 0)->innertext; + $item['content'] = $this->cleanContent($article, $contentNode); + // only the article lead image is retained if it's there + if (!is_null($article->find($imgNode, 0))) { + $item['enclosures'][] = $article->find($imgNode, 0)->find('img', 0)->getAttribute('src'); + } else { + $item['enclosures'][] = ''; + } + // add the subheadline as category. This will create a link in new articles + // and a text in old articles + $item['categories'][] = $article->find($categoryNode, 0)->innertext; - return $item; - } + return $item; + } - private function cleanContent($article, $contentNode){ - // the actual article is in this div - $content = $article->find($contentNode, 0)->innertext; - // clean the article content. Remove all div's since the text is in paragraph elements - foreach (array( - '<div ' - ) as $tag_start) { - $content = stripRecursiveHTMLSection($content, 'div', $tag_start); - } - // now remove embedded iframes. The podcast postings contain these for example - $content = preg_replace('/<iframe.*?\/iframe>/i', '', $content); - // fix the relative links - $content = defaultLinkTo($content, $this->getURI()); + private function cleanContent($article, $contentNode) + { + // the actual article is in this div + $content = $article->find($contentNode, 0)->innertext; + // clean the article content. Remove all div's since the text is in paragraph elements + foreach ( + [ + '<div ' + ] as $tag_start + ) { + $content = stripRecursiveHTMLSection($content, 'div', $tag_start); + } + // now remove embedded iframes. The podcast postings contain these for example + $content = preg_replace('/<iframe.*?\/iframe>/i', '', $content); + // fix the relative links + $content = defaultLinkTo($content, $this->getURI()); - return $content; - } + return $content; + } } diff --git a/bridges/EconomistWorldInBriefBridge.php b/bridges/EconomistWorldInBriefBridge.php index 33240f6e..47782a51 100644 --- a/bridges/EconomistWorldInBriefBridge.php +++ b/bridges/EconomistWorldInBriefBridge.php @@ -1,141 +1,143 @@ <?php + class EconomistWorldInBriefBridge extends BridgeAbstract { - const MAINTAINER = 'sqrtminusone'; - const NAME = 'Economist the World in Brief Bridge'; - const URI = 'https://www.economist.com/the-world-in-brief'; + const MAINTAINER = 'sqrtminusone'; + const NAME = 'Economist the World in Brief Bridge'; + const URI = 'https://www.economist.com/the-world-in-brief'; - const CACHE_TIMEOUT = 3600; // 1 hour - const DESCRIPTION = 'Returns stories from the World in Brief section'; + const CACHE_TIMEOUT = 3600; // 1 hour + const DESCRIPTION = 'Returns stories from the World in Brief section'; - const PARAMETERS = array( - '' => array( - 'splitGobbets' => array( - 'name' => 'Split the short stories', - 'type' => 'checkbox', - 'defaultValue' => false, - 'title' => 'Whether to split the short stories into separate entries' - ), - 'limit' => array( - 'name' => 'Truncate headers for the short stories', - 'type' => 'number', - 'defaultValue' => 100 - ), - 'agenda' => array( - 'name' => 'Add agenda for the day', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'agendaPictures' => array( - 'name' => 'Include pictures to the agenda', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'quote' => array( - 'name' => 'Include the quote of the day', - 'type' => 'checkbox' - ) - ) - ); + const PARAMETERS = [ + '' => [ + 'splitGobbets' => [ + 'name' => 'Split the short stories', + 'type' => 'checkbox', + 'defaultValue' => false, + 'title' => 'Whether to split the short stories into separate entries' + ], + 'limit' => [ + 'name' => 'Truncate headers for the short stories', + 'type' => 'number', + 'defaultValue' => 100 + ], + 'agenda' => [ + 'name' => 'Add agenda for the day', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'agendaPictures' => [ + 'name' => 'Include pictures to the agenda', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'quote' => [ + 'name' => 'Include the quote of the day', + 'type' => 'checkbox' + ] + ] + ]; - public function collectData() - { - $html = getSimpleHTMLDOM(self::URI); - $gobbets = $html->find('._gobbets', 0); - if ($this->getInput('splitGobbets') == 1) { - $this->splitGobbets($gobbets); - } else { - $this->mergeGobbets($gobbets); - }; - if ($this->getInput('agenda') == 1) { - $articles = $html->find('._articles', 0); - $this->collectArticles($articles); - } - if ($this->getInput('quote') == 1) { - $quote = $html->find('._quote-container', 0); - $this->addQuote($quote); - } - } + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); + $gobbets = $html->find('._gobbets', 0); + if ($this->getInput('splitGobbets') == 1) { + $this->splitGobbets($gobbets); + } else { + $this->mergeGobbets($gobbets); + }; + if ($this->getInput('agenda') == 1) { + $articles = $html->find('._articles', 0); + $this->collectArticles($articles); + } + if ($this->getInput('quote') == 1) { + $quote = $html->find('._quote-container', 0); + $this->addQuote($quote); + } + } - private function splitGobbets($gobbets) - { - $today = new Datetime(); - $today->setTime(0, 0, 0, 0); - $limit = $this->getInput('limit'); - foreach ($gobbets->find('._gobbet') as $gobbet) { - $title = $gobbet->plaintext; - $match = preg_match('/[\.,]/', $title, $matches, PREG_OFFSET_CAPTURE); - if ($match > 0) { - $point = $matches[0][1]; - $title = mb_substr($title, 0, $point); - } - if ($limit && mb_strlen($title) > $limit) { - $title = mb_substr($title, 0, $limit) . '...'; - } - $item = array( - 'uri' => self::URI, - 'title' => $title, - 'content' => $gobbet->innertext, - 'timestamp' => $today->format('U'), - 'uid' => md5($gobbet->plaintext) - ); - $this->items[] = $item; - } - } + private function splitGobbets($gobbets) + { + $today = new Datetime(); + $today->setTime(0, 0, 0, 0); + $limit = $this->getInput('limit'); + foreach ($gobbets->find('._gobbet') as $gobbet) { + $title = $gobbet->plaintext; + $match = preg_match('/[\.,]/', $title, $matches, PREG_OFFSET_CAPTURE); + if ($match > 0) { + $point = $matches[0][1]; + $title = mb_substr($title, 0, $point); + } + if ($limit && mb_strlen($title) > $limit) { + $title = mb_substr($title, 0, $limit) . '...'; + } + $item = [ + 'uri' => self::URI, + 'title' => $title, + 'content' => $gobbet->innertext, + 'timestamp' => $today->format('U'), + 'uid' => md5($gobbet->plaintext) + ]; + $this->items[] = $item; + } + } - private function mergeGobbets($gobbets) - { - $today = new Datetime(); - $today->setTime(0, 0, 0, 0); - $contents = ''; - foreach ($gobbets->find('._gobbet') as $gobbet) { - $contents .= "<p>{$gobbet->innertext}"; - } - $this->items[] = array( - 'uri' => self::URI, - 'title' => 'World in brief at ' . $today->format('Y.m.d'), - 'content' => $contents, - 'timestamp' => $today->format('U'), - 'uid' => 'world-in-brief-' . $today->format('U') - ); - } + private function mergeGobbets($gobbets) + { + $today = new Datetime(); + $today->setTime(0, 0, 0, 0); + $contents = ''; + foreach ($gobbets->find('._gobbet') as $gobbet) { + $contents .= "<p>{$gobbet->innertext}"; + } + $this->items[] = [ + 'uri' => self::URI, + 'title' => 'World in brief at ' . $today->format('Y.m.d'), + 'content' => $contents, + 'timestamp' => $today->format('U'), + 'uid' => 'world-in-brief-' . $today->format('U') + ]; + } - private function collectArticles($articles) - { - $i = 0; - $today = new Datetime(); - $today->setTime(0, 0, 0, 0); - foreach ($articles->find('._article') as $article) { - $title = $article->find('._headline', 0)->plaintext; - $image = $article->find('._main-image', 0); - $content = $article->find('._content', 0); + private function collectArticles($articles) + { + $i = 0; + $today = new Datetime(); + $today->setTime(0, 0, 0, 0); + foreach ($articles->find('._article') as $article) { + $title = $article->find('._headline', 0)->plaintext; + $image = $article->find('._main-image', 0); + $content = $article->find('._content', 0); - $res_content = ''; - if ($image != null && $this->getInput('agendaPictures') == 1) { - $img = $image->find('img', 0); - $res_content .= '<img src="' . $img->src . '" />'; - } - $res_content .= $content->innertext; - $this->items[] = array( - 'uri' => self::URI, - 'title' => $title, - 'content' => $res_content, - 'timestamp' => $today->format('U'), - 'uid' => 'story-' . $today->format('U') . "{$i}", - ); - $i++; - } - } + $res_content = ''; + if ($image != null && $this->getInput('agendaPictures') == 1) { + $img = $image->find('img', 0); + $res_content .= '<img src="' . $img->src . '" />'; + } + $res_content .= $content->innertext; + $this->items[] = [ + 'uri' => self::URI, + 'title' => $title, + 'content' => $res_content, + 'timestamp' => $today->format('U'), + 'uid' => 'story-' . $today->format('U') . "{$i}", + ]; + $i++; + } + } - private function addQuote($quote) { - $today = new Datetime(); - $today->setTime(0, 0, 0, 0); - $this->items[] = array( - 'uri' => self::URI, - 'title' => 'Quote of the day ' . $today->format('Y.m.d'), - 'content' => $quote->innertext, - 'timestamp' => $today->format('U'), - 'uid' => 'quote-' . $today->format('U') - ); - } + private function addQuote($quote) + { + $today = new Datetime(); + $today->setTime(0, 0, 0, 0); + $this->items[] = [ + 'uri' => self::URI, + 'title' => 'Quote of the day ' . $today->format('Y.m.d'), + 'content' => $quote->innertext, + 'timestamp' => $today->format('U'), + 'uid' => 'quote-' . $today->format('U') + ]; + } } diff --git a/bridges/EliteDangerousGalnetBridge.php b/bridges/EliteDangerousGalnetBridge.php index 510866c9..555be1cd 100644 --- a/bridges/EliteDangerousGalnetBridge.php +++ b/bridges/EliteDangerousGalnetBridge.php @@ -1,53 +1,55 @@ <?php -class EliteDangerousGalnetBridge extends BridgeAbstract { - - const MAINTAINER = 'corenting'; - const NAME = 'Elite: Dangerous Galnet'; - const URI = 'https://community.elitedangerous.com/galnet/'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Returns the latest page of news from Galnet'; - const PARAMETERS = array( - array( - 'language' => array( - 'name' => 'Language', - 'type' => 'list', - 'values' => array( - 'English' => 'en', - 'French' => 'fr', - 'German' => 'de' - ), - 'defaultValue' => 'en' - ) - ) - ); - - public function collectData(){ - $language = $this->getInput('language'); - $url = 'https://community.elitedangerous.com/'; - $url = $url . $language . '/galnet'; - $html = getSimpleHTMLDOM($url); - - foreach($html->find('div.article') as $element) { - $item = array(); - - $uri = $element->find('h3 a', 0)->href; - $uri = 'https://community.elitedangerous.com/' . $language . $uri; - $item['uri'] = $uri; - - $item['title'] = $element->find('h3 a', 0)->plaintext; - - $content = $element->find('p', -1)->innertext; - $item['content'] = $content; - - $date = $element->find('p.small', 0)->innertext; - $article_year = substr($date, -4) - 1286; //Convert E:D date to actual date - $date = substr($date, 0, -4) . $article_year; - $item['timestamp'] = strtotime($date); - - $this->items[] = $item; - } - - //Remove duplicates that sometimes show up on the website - $this->items = array_unique($this->items, SORT_REGULAR); - } + +class EliteDangerousGalnetBridge extends BridgeAbstract +{ + const MAINTAINER = 'corenting'; + const NAME = 'Elite: Dangerous Galnet'; + const URI = 'https://community.elitedangerous.com/galnet/'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Returns the latest page of news from Galnet'; + const PARAMETERS = [ + [ + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'values' => [ + 'English' => 'en', + 'French' => 'fr', + 'German' => 'de' + ], + 'defaultValue' => 'en' + ] + ] + ]; + + public function collectData() + { + $language = $this->getInput('language'); + $url = 'https://community.elitedangerous.com/'; + $url = $url . $language . '/galnet'; + $html = getSimpleHTMLDOM($url); + + foreach ($html->find('div.article') as $element) { + $item = []; + + $uri = $element->find('h3 a', 0)->href; + $uri = 'https://community.elitedangerous.com/' . $language . $uri; + $item['uri'] = $uri; + + $item['title'] = $element->find('h3 a', 0)->plaintext; + + $content = $element->find('p', -1)->innertext; + $item['content'] = $content; + + $date = $element->find('p.small', 0)->innertext; + $article_year = substr($date, -4) - 1286; //Convert E:D date to actual date + $date = substr($date, 0, -4) . $article_year; + $item['timestamp'] = strtotime($date); + + $this->items[] = $item; + } + + //Remove duplicates that sometimes show up on the website + $this->items = array_unique($this->items, SORT_REGULAR); + } } diff --git a/bridges/ElloBridge.php b/bridges/ElloBridge.php index e9b2980e..b0c7b09f 100644 --- a/bridges/ElloBridge.php +++ b/bridges/ElloBridge.php @@ -1,150 +1,141 @@ <?php -class ElloBridge extends BridgeAbstract { - - const MAINTAINER = 'teromene'; - const NAME = 'Ello Bridge'; - const URI = 'https://ello.co/'; - const CACHE_TIMEOUT = 4800; //2hours - const DESCRIPTION = 'Returns the newest posts for Ello'; - - const PARAMETERS = array( - 'By User' => array( - 'u' => array( - 'name' => 'Username', - 'required' => true, - 'exampleValue' => 'zteph', - 'title' => 'Username' - ) - ), - 'Search' => array( - 's' => array( - 'name' => 'Search', - 'required' => true, - 'exampleValue' => 'bird', - 'title' => 'Search' - ) - ) - ); - public function collectData() { - - $header = array( - 'Authorization: Bearer ' . $this->getAPIKey() - ); - - if(!empty($this->getInput('u'))) { - $postData = getContents(self::URI . 'api/v2/users/~' . urlencode($this->getInput('u')) . '/posts', $header) or - returnServerError('Unable to query Ello API.'); - } else { - $postData = getContents(self::URI . 'api/v2/posts?terms=' . urlencode($this->getInput('s')), $header) or - returnServerError('Unable to query Ello API.'); - } - - $postData = json_decode($postData); - $count = 0; - foreach($postData->posts as $post) { - - $item = array(); - $item['author'] = $this->getUsername($post, $postData); - $item['timestamp'] = strtotime($post->created_at); - $item['title'] = strip_tags($this->findText($post->summary)); - $item['content'] = $this->getPostContent($post->body); - $item['enclosures'] = $this->getEnclosures($post, $postData); - $item['uri'] = self::URI . $item['author'] . '/post/' . $post->token; - $content = $post->body; - - $this->items[] = $item; - $count += 1; - - } - - } - - private function findText($path) { - - foreach($path as $summaryElement) { - - if($summaryElement->kind == 'text') { - return $summaryElement->data; - } - - } - - return ''; - - } - - private function getPostContent($path) { - - $content = ''; - foreach($path as $summaryElement) { - - if($summaryElement->kind == 'text') { - $content .= $summaryElement->data; - } elseif ($summaryElement->kind == 'image') { - $alt = ''; - if(property_exists($summaryElement->data, 'alt')) { - $alt = $summaryElement->data->alt; - } - $content .= '<img src="' . $summaryElement->data->url . '" alt="' . $alt . '" />'; - } - - } - - return $content; - - } - - private function getEnclosures($post, $postData) { - - $assets = array(); - foreach($post->links->assets as $asset) { - foreach($postData->linked->assets as $assetLink) { - if($asset == $assetLink->id) { - $assets[] = $assetLink->attachment->original->url; - break; - } - } - } - - return $assets; - - } - - private function getUsername($post, $postData) { - - foreach($postData->linked->users as $user) { - if($user->id == $post->links->author->id) { - return $user->username; - } - } - - } - - private function getAPIKey() { - $cacheFac = new CacheFactory(); - - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope(get_called_class()); - $cache->setKey(array('key')); - $key = $cache->loadData(); - - if($key == null) { - $keyInfo = getContents(self::URI . 'api/webapp-token') or - returnServerError('Unable to get token.'); - $key = json_decode($keyInfo)->token->access_token; - $cache->saveData($key); - } - - return $key; - - } - - public function getName(){ - if(!is_null($this->getInput('u'))) { - return $this->getInput('u') . ' - Ello Bridge'; - } - - return parent::getName(); - } +class ElloBridge extends BridgeAbstract +{ + const MAINTAINER = 'teromene'; + const NAME = 'Ello Bridge'; + const URI = 'https://ello.co/'; + const CACHE_TIMEOUT = 4800; //2hours + const DESCRIPTION = 'Returns the newest posts for Ello'; + + const PARAMETERS = [ + 'By User' => [ + 'u' => [ + 'name' => 'Username', + 'required' => true, + 'exampleValue' => 'zteph', + 'title' => 'Username' + ] + ], + 'Search' => [ + 's' => [ + 'name' => 'Search', + 'required' => true, + 'exampleValue' => 'bird', + 'title' => 'Search' + ] + ] + ]; + + public function collectData() + { + $header = [ + 'Authorization: Bearer ' . $this->getAPIKey() + ]; + + if (!empty($this->getInput('u'))) { + $postData = getContents(self::URI . 'api/v2/users/~' . urlencode($this->getInput('u')) . '/posts', $header) or + returnServerError('Unable to query Ello API.'); + } else { + $postData = getContents(self::URI . 'api/v2/posts?terms=' . urlencode($this->getInput('s')), $header) or + returnServerError('Unable to query Ello API.'); + } + + $postData = json_decode($postData); + $count = 0; + foreach ($postData->posts as $post) { + $item = []; + $item['author'] = $this->getUsername($post, $postData); + $item['timestamp'] = strtotime($post->created_at); + $item['title'] = strip_tags($this->findText($post->summary)); + $item['content'] = $this->getPostContent($post->body); + $item['enclosures'] = $this->getEnclosures($post, $postData); + $item['uri'] = self::URI . $item['author'] . '/post/' . $post->token; + $content = $post->body; + + $this->items[] = $item; + $count += 1; + } + } + + private function findText($path) + { + foreach ($path as $summaryElement) { + if ($summaryElement->kind == 'text') { + return $summaryElement->data; + } + } + + return ''; + } + + private function getPostContent($path) + { + $content = ''; + foreach ($path as $summaryElement) { + if ($summaryElement->kind == 'text') { + $content .= $summaryElement->data; + } elseif ($summaryElement->kind == 'image') { + $alt = ''; + if (property_exists($summaryElement->data, 'alt')) { + $alt = $summaryElement->data->alt; + } + $content .= '<img src="' . $summaryElement->data->url . '" alt="' . $alt . '" />'; + } + } + + return $content; + } + + private function getEnclosures($post, $postData) + { + $assets = []; + foreach ($post->links->assets as $asset) { + foreach ($postData->linked->assets as $assetLink) { + if ($asset == $assetLink->id) { + $assets[] = $assetLink->attachment->original->url; + break; + } + } + } + + return $assets; + } + + private function getUsername($post, $postData) + { + foreach ($postData->linked->users as $user) { + if ($user->id == $post->links->author->id) { + return $user->username; + } + } + } + + private function getAPIKey() + { + $cacheFac = new CacheFactory(); + + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope(get_called_class()); + $cache->setKey(['key']); + $key = $cache->loadData(); + + if ($key == null) { + $keyInfo = getContents(self::URI . 'api/webapp-token') or + returnServerError('Unable to get token.'); + $key = json_decode($keyInfo)->token->access_token; + $cache->saveData($key); + } + + return $key; + } + + public function getName() + { + if (!is_null($this->getInput('u'))) { + return $this->getInput('u') . ' - Ello Bridge'; + } + + return parent::getName(); + } } diff --git a/bridges/ElsevierBridge.php b/bridges/ElsevierBridge.php index 5ab19a1a..8e246fc4 100644 --- a/bridges/ElsevierBridge.php +++ b/bridges/ElsevierBridge.php @@ -1,41 +1,44 @@ <?php -class ElsevierBridge extends BridgeAbstract { - const MAINTAINER = 'dvikan'; - const NAME = 'Elsevier journals recent articles'; - const URI = 'https://www.journals.elsevier.com/'; - const CACHE_TIMEOUT = 43200; //12h - const DESCRIPTION = 'Returns the recent articles published in Elsevier journals'; +class ElsevierBridge extends BridgeAbstract +{ + const MAINTAINER = 'dvikan'; + const NAME = 'Elsevier journals recent articles'; + const URI = 'https://www.journals.elsevier.com/'; + const CACHE_TIMEOUT = 43200; //12h + const DESCRIPTION = 'Returns the recent articles published in Elsevier journals'; - const PARAMETERS = array( array( - 'j' => array( - 'name' => 'Journal name', - 'required' => true, - 'exampleValue' => 'academic-pediatrics', - 'title' => 'Insert html-part of your journal' - ) - )); + const PARAMETERS = [ [ + 'j' => [ + 'name' => 'Journal name', + 'required' => true, + 'exampleValue' => 'academic-pediatrics', + 'title' => 'Insert html-part of your journal' + ] + ]]; - public function collectData(){ - // Not all journals have the /recent-articles page - $url = sprintf('https://www.journals.elsevier.com/%s/recent-articles/', $this->getInput('j')); - $html = getSimpleHTMLDOM($url); + public function collectData() + { + // Not all journals have the /recent-articles page + $url = sprintf('https://www.journals.elsevier.com/%s/recent-articles/', $this->getInput('j')); + $html = getSimpleHTMLDOM($url); - foreach($html->find('article') as $recentArticle) { - $item = []; - $item['uri'] = $recentArticle->find('a', 0)->getAttribute('href'); - $item['title'] = $recentArticle->find('h2', 0)->plaintext; - $item['author'] = $recentArticle->find('p > span', 0)->plaintext; - $publicationDateString = trim($recentArticle->find('p > span', 1)->plaintext); - $publicationDate = DateTimeImmutable::createFromFormat('F d, Y', $publicationDateString); - if ($publicationDate) { - $item['timestamp'] = $publicationDate->getTimestamp(); - } - $this->items[] = $item; - } - } + foreach ($html->find('article') as $recentArticle) { + $item = []; + $item['uri'] = $recentArticle->find('a', 0)->getAttribute('href'); + $item['title'] = $recentArticle->find('h2', 0)->plaintext; + $item['author'] = $recentArticle->find('p > span', 0)->plaintext; + $publicationDateString = trim($recentArticle->find('p > span', 1)->plaintext); + $publicationDate = DateTimeImmutable::createFromFormat('F d, Y', $publicationDateString); + if ($publicationDate) { + $item['timestamp'] = $publicationDate->getTimestamp(); + } + $this->items[] = $item; + } + } - public function getIcon(): string { - return 'https://cdn.elsevier.io/verona/includes/favicons/favicon-32x32.png'; - } + public function getIcon(): string + { + return 'https://cdn.elsevier.io/verona/includes/favicons/favicon-32x32.png'; + } } diff --git a/bridges/EngadgetBridge.php b/bridges/EngadgetBridge.php index cf200fa4..2bed4a4a 100644 --- a/bridges/EngadgetBridge.php +++ b/bridges/EngadgetBridge.php @@ -1,26 +1,30 @@ <?php -class EngadgetBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'Engadget Bridge'; - const URI = 'https://www.engadget.com/'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'Article content for Engadget.'; +class EngadgetBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'Engadget Bridge'; + const URI = 'https://www.engadget.com/'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'Article content for Engadget.'; - public function collectData(){ - $this->collectExpandableDatas(static::URI . 'rss.xml', 15); - } + public function collectData() + { + $this->collectExpandableDatas(static::URI . 'rss.xml', 15); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); - // figure contain's the main article image - $article = $articlePage->find('figure', 0); - // .article-text has the actual article - foreach($articlePage->find('.article-text') as $element) - $article = $article . $element; - $item['content'] = $article; - return $item; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + // figure contain's the main article image + $article = $articlePage->find('figure', 0); + // .article-text has the actual article + foreach ($articlePage->find('.article-text') as $element) { + $article = $article . $element; + } + $item['content'] = $article; + return $item; + } } diff --git a/bridges/EpicgamesBridge.php b/bridges/EpicgamesBridge.php index d7dd6afe..dfb30b7f 100644 --- a/bridges/EpicgamesBridge.php +++ b/bridges/EpicgamesBridge.php @@ -1,92 +1,94 @@ <?php -class EpicgamesBridge extends BridgeAbstract { - const NAME = 'Epic Games Store News'; - const MAINTAINER = 'otakuf'; - const URI = 'https://www.epicgames.com'; - const DESCRIPTION = 'Returns the latest posts from epicgames.com'; - const CACHE_TIMEOUT = 3600; // 60min +class EpicgamesBridge extends BridgeAbstract +{ + const NAME = 'Epic Games Store News'; + const MAINTAINER = 'otakuf'; + const URI = 'https://www.epicgames.com'; + const DESCRIPTION = 'Returns the latest posts from epicgames.com'; + const CACHE_TIMEOUT = 3600; // 60min - const PARAMETERS = array( array( - 'postcount' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'title' => 'Maximum number of items to return', - 'defaultValue' => 10, - ), - 'language' => array( - 'name' => 'Language', - 'type' => 'list', - 'values' => array( - 'English' => 'en', - 'العربية' => 'ar', - 'Deutsch' => 'de', - 'Español (Spain)' => 'es-ES', - 'Español (LA)' => 'es-MX', - 'Français' => 'fr', - 'Italiano' => 'it', - '日本語' => 'ja', - '한국어' => 'ko', - 'Polski' => 'pl', - 'Português (Brasil)' => 'pt-BR', - 'Русский' => 'ru', - 'ไทย' => 'th', - 'Türkçe' => 'tr', - '简体中文' => 'zh-CN', - '繁體中文' => 'zh-Hant', - ), - 'title' => 'Language of blog posts', - 'defaultValue' => 'en', - ), - )); + const PARAMETERS = [ [ + 'postcount' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'title' => 'Maximum number of items to return', + 'defaultValue' => 10, + ], + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'values' => [ + 'English' => 'en', + 'العربية' => 'ar', + 'Deutsch' => 'de', + 'Español (Spain)' => 'es-ES', + 'Español (LA)' => 'es-MX', + 'Français' => 'fr', + 'Italiano' => 'it', + '日本語' => 'ja', + '한국어' => 'ko', + 'Polski' => 'pl', + 'Português (Brasil)' => 'pt-BR', + 'Русский' => 'ru', + 'ไทย' => 'th', + 'Türkçe' => 'tr', + '简体中文' => 'zh-CN', + '繁體中文' => 'zh-Hant', + ], + 'title' => 'Language of blog posts', + 'defaultValue' => 'en', + ], + ]]; - public function collectData() { - $api = 'https://store-content.ak.epicgames.com/api/'; + public function collectData() + { + $api = 'https://store-content.ak.epicgames.com/api/'; - // Get sticky posts first - // Example: https://store-content.ak.epicgames.com/api/ru/content/blog/sticky?locale=ru - $urlSticky = $api . $this->getInput('language') . '/content/blog/sticky'; - // Then get posts - // Example: https://store-content.ak.epicgames.com/api/ru/content/blog?limit=25 - $urlBlog = $api . $this->getInput('language') . '/content/blog?limit=' . $this->getInput('postcount'); + // Get sticky posts first + // Example: https://store-content.ak.epicgames.com/api/ru/content/blog/sticky?locale=ru + $urlSticky = $api . $this->getInput('language') . '/content/blog/sticky'; + // Then get posts + // Example: https://store-content.ak.epicgames.com/api/ru/content/blog?limit=25 + $urlBlog = $api . $this->getInput('language') . '/content/blog?limit=' . $this->getInput('postcount'); - $dataSticky = getContents($urlSticky); - $dataBlog = getContents($urlBlog); + $dataSticky = getContents($urlSticky); + $dataBlog = getContents($urlBlog); - // Merge data - $decodedData = array_merge(json_decode($dataSticky), json_decode($dataBlog)); + // Merge data + $decodedData = array_merge(json_decode($dataSticky), json_decode($dataBlog)); - foreach($decodedData as $key => $value) { - $item = array(); - $item['uri'] = self::URI . $value->url; - $item['title'] = $value->title; - $item['timestamp'] = $value->date; - $item['author'] = 'Epic Games Store'; - if(!empty($value->author)) { - $item['author'] = $value->author; - } - if(!empty($value->content)) { - $item['content'] = defaultLinkTo($value->content, self::URI); - } - if(!empty($value->image)) { - $item['enclosures'][] = $value->image; - } - $item['uid'] = $value->_id; - $item['id'] = $value->_id; + foreach ($decodedData as $key => $value) { + $item = []; + $item['uri'] = self::URI . $value->url; + $item['title'] = $value->title; + $item['timestamp'] = $value->date; + $item['author'] = 'Epic Games Store'; + if (!empty($value->author)) { + $item['author'] = $value->author; + } + if (!empty($value->content)) { + $item['content'] = defaultLinkTo($value->content, self::URI); + } + if (!empty($value->image)) { + $item['enclosures'][] = $value->image; + } + $item['uid'] = $value->_id; + $item['id'] = $value->_id; - $this->items[] = $item; - } + $this->items[] = $item; + } - // Sort data - usort($this->items, function ($item1, $item2) { - if ($item2['timestamp'] == $item1['timestamp']) { - return 0; - } - return ($item2['timestamp'] < $item1['timestamp']) ? -1 : 1; - }); + // Sort data + usort($this->items, function ($item1, $item2) { + if ($item2['timestamp'] == $item1['timestamp']) { + return 0; + } + return ($item2['timestamp'] < $item1['timestamp']) ? -1 : 1; + }); - // Limit data - $this->items = array_slice($this->items, 0, $this->getInput('postcount')); - } + // Limit data + $this->items = array_slice($this->items, 0, $this->getInput('postcount')); + } } diff --git a/bridges/EsquerdaNetBridge.php b/bridges/EsquerdaNetBridge.php index 5c56e95b..ffb4fd4e 100644 --- a/bridges/EsquerdaNetBridge.php +++ b/bridges/EsquerdaNetBridge.php @@ -1,69 +1,75 @@ <?php -class EsquerdaNetBridge extends FeedExpander { - const MAINTAINER = 'somini'; - const NAME = 'Esquerda.net'; - const URI = 'https://www.esquerda.net'; - const DESCRIPTION = 'Esquerda.net'; - const PARAMETERS = array( - array( - 'feed' => array( - 'name' => 'Feed', - 'type' => 'list', - 'defaultValue' => 'Geral', - 'values' => array( - 'Geral' => 'geral', - 'Dossier' => 'artigos-dossier', - 'Vídeo' => 'video', - 'Opinião' => 'opinioes', - 'Rádio' => 'radio', - ) - ) - ) - ); - public function getURI() { - $type = $this->getInput('feed'); - return self::URI . '/rss/' . $type; - } +class EsquerdaNetBridge extends FeedExpander +{ + const MAINTAINER = 'somini'; + const NAME = 'Esquerda.net'; + const URI = 'https://www.esquerda.net'; + const DESCRIPTION = 'Esquerda.net'; + const PARAMETERS = [ + [ + 'feed' => [ + 'name' => 'Feed', + 'type' => 'list', + 'defaultValue' => 'Geral', + 'values' => [ + 'Geral' => 'geral', + 'Dossier' => 'artigos-dossier', + 'Vídeo' => 'video', + 'Opinião' => 'opinioes', + 'Rádio' => 'radio', + ] + ] + ] + ]; - public function getIcon() { - return 'https://www.esquerda.net/sites/default/files/favicon_0.ico'; - } + public function getURI() + { + $type = $this->getInput('feed'); + return self::URI . '/rss/' . $type; + } - public function collectData(){ - parent::collectExpandableDatas($this->getURI()); - } + public function getIcon() + { + return 'https://www.esquerda.net/sites/default/files/favicon_0.ico'; + } - protected function parseItem($newsItem){ - # Fix Publish date - $badDate = $newsItem->pubDate; - preg_match('|(?P<day>\d\d)/(?P<month>\d\d)/(?P<year>\d\d\d\d) - (?P<hour>\d\d):(?P<minute>\d\d)|', $badDate, $d); - $newsItem->pubDate = sprintf('%s-%s-%sT%s:%s', $d['year'], $d['month'], $d['day'], $d['hour'], $d['minute']); - $item = parent::parseItem($newsItem); - # Include all the content - $uri = $item['uri']; - $html = getSimpleHTMLDOMCached($uri); - $content = $html->find('div#content div.content', 0); - ## Fix author - $authorHTML = $html->find('.field-name-field-op-author a', 0); - if ($authorHTML) { - $item['author'] = $authorHTML->innertext; - $authorHTML->remove(); - } - ## Remove crap - $content->find('.field-name-addtoany', 0)->remove(); - ## Fix links - $content = defaultLinkTo($content, self::URI); - ## Fix Images - foreach($content->find('img') as $img) { - $altSrc = $img->getAttribute('data-src'); - if ($altSrc) { - $img->setAttribute('src', $altSrc); - } - $img->width = null; - $img->height = null; - } - $item['content'] = $content; - return $item; - } + public function collectData() + { + parent::collectExpandableDatas($this->getURI()); + } + + protected function parseItem($newsItem) + { + # Fix Publish date + $badDate = $newsItem->pubDate; + preg_match('|(?P<day>\d\d)/(?P<month>\d\d)/(?P<year>\d\d\d\d) - (?P<hour>\d\d):(?P<minute>\d\d)|', $badDate, $d); + $newsItem->pubDate = sprintf('%s-%s-%sT%s:%s', $d['year'], $d['month'], $d['day'], $d['hour'], $d['minute']); + $item = parent::parseItem($newsItem); + # Include all the content + $uri = $item['uri']; + $html = getSimpleHTMLDOMCached($uri); + $content = $html->find('div#content div.content', 0); + ## Fix author + $authorHTML = $html->find('.field-name-field-op-author a', 0); + if ($authorHTML) { + $item['author'] = $authorHTML->innertext; + $authorHTML->remove(); + } + ## Remove crap + $content->find('.field-name-addtoany', 0)->remove(); + ## Fix links + $content = defaultLinkTo($content, self::URI); + ## Fix Images + foreach ($content->find('img') as $img) { + $altSrc = $img->getAttribute('data-src'); + if ($altSrc) { + $img->setAttribute('src', $altSrc); + } + $img->width = null; + $img->height = null; + } + $item['content'] = $content; + return $item; + } } diff --git a/bridges/EstCeQuonMetEnProdBridge.php b/bridges/EstCeQuonMetEnProdBridge.php index 67e69f55..862567d2 100644 --- a/bridges/EstCeQuonMetEnProdBridge.php +++ b/bridges/EstCeQuonMetEnProdBridge.php @@ -1,26 +1,28 @@ <?php -class EstCeQuonMetEnProdBridge extends BridgeAbstract { - const MAINTAINER = 'ORelio'; - const NAME = 'Est-ce qu\'on met en prod aujourd\'hui ?'; - const URI = 'https://www.estcequonmetenprodaujourdhui.info/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Should we put a website in production today? (French)'; +class EstCeQuonMetEnProdBridge extends BridgeAbstract +{ + const MAINTAINER = 'ORelio'; + const NAME = 'Est-ce qu\'on met en prod aujourd\'hui ?'; + const URI = 'https://www.estcequonmetenprodaujourdhui.info/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Should we put a website in production today? (French)'; - public function collectData() { - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $item = array(); - $item['uri'] = $this->getURI() . '#' . date('Y-m-d'); - $item['title'] = $this->getName(); - $item['author'] = 'Nicolas Hoffmann'; - $item['timestamp'] = strtotime('today midnight'); - $item['content'] = str_replace( - 'src="/', - 'src="' . self::URI, - trim(extractFromDelimiters($html->outertext, '<body role="document">', '<div id="share')) - ); + $item = []; + $item['uri'] = $this->getURI() . '#' . date('Y-m-d'); + $item['title'] = $this->getName(); + $item['author'] = 'Nicolas Hoffmann'; + $item['timestamp'] = strtotime('today midnight'); + $item['content'] = str_replace( + 'src="/', + 'src="' . self::URI, + trim(extractFromDelimiters($html->outertext, '<body role="document">', '<div id="share')) + ); - $this->items[] = $item; - } + $this->items[] = $item; + } } diff --git a/bridges/EtsyBridge.php b/bridges/EtsyBridge.php index 7d79b82e..05bf7d26 100644 --- a/bridges/EtsyBridge.php +++ b/bridges/EtsyBridge.php @@ -1,81 +1,85 @@ <?php -class EtsyBridge extends BridgeAbstract { - const NAME = 'Etsy search'; - const URI = 'https://www.etsy.com'; - const DESCRIPTION = 'Returns feeds for search results'; - const MAINTAINER = 'logmanoriginal'; - const PARAMETERS = array( - array( - 'query' => array( - 'name' => 'Search query', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert your search term here', - 'exampleValue' => 'lamp' - ), - 'queryextension' => array( - 'name' => 'Query extension', - 'type' => 'text', - 'required' => false, - 'title' => 'Insert additional query parts here +class EtsyBridge extends BridgeAbstract +{ + const NAME = 'Etsy search'; + const URI = 'https://www.etsy.com'; + const DESCRIPTION = 'Returns feeds for search results'; + const MAINTAINER = 'logmanoriginal'; + const PARAMETERS = [ + [ + 'query' => [ + 'name' => 'Search query', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert your search term here', + 'exampleValue' => 'lamp' + ], + 'queryextension' => [ + 'name' => 'Query extension', + 'type' => 'text', + 'required' => false, + 'title' => 'Insert additional query parts here (anything after ?search=<your search query>)', - 'exampleValue' => '&explicit=1&locationQuery=2921044' - ), - 'hideimage' => array( - 'name' => 'Hide image in content', - 'type' => 'checkbox', - 'title' => 'Activate to hide the image in the content', - ) - ) - ); + 'exampleValue' => '&explicit=1&locationQuery=2921044' + ], + 'hideimage' => [ + 'name' => 'Hide image in content', + 'type' => 'checkbox', + 'title' => 'Activate to hide the image in the content', + ] + ] + ]; - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); - $results = $html->find('li.wt-list-unstyled'); + $results = $html->find('li.wt-list-unstyled'); - foreach($results as $result) { - // Remove Lazy loading - if($result->find('.wt-skeleton-ui', 0)) - continue; + foreach ($results as $result) { + // Remove Lazy loading + if ($result->find('.wt-skeleton-ui', 0)) { + continue; + } - $item = array(); + $item = []; - $item['title'] = $result->find('a', 0)->title; - $item['uri'] = $result->find('a', 0)->href; - $item['author'] = $result->find('p.wt-text-gray > span', 2)->plaintext; + $item['title'] = $result->find('a', 0)->title; + $item['uri'] = $result->find('a', 0)->href; + $item['author'] = $result->find('p.wt-text-gray > span', 2)->plaintext; - $item['content'] = '<p>' - . $result->find('span.currency-symbol', 0)->plaintext - . $result->find('span.currency-value', 0)->plaintext - . '</p><p>' - . $result->find('a', 0)->title - . '</p>'; + $item['content'] = '<p>' + . $result->find('span.currency-symbol', 0)->plaintext + . $result->find('span.currency-value', 0)->plaintext + . '</p><p>' + . $result->find('a', 0)->title + . '</p>'; - $image = $result->find('img.wt-display-block', 0)->src; + $image = $result->find('img.wt-display-block', 0)->src; - if(!$this->getInput('hideimage')) { - $item['content'] .= '<img src="' . $image . '">'; - } + if (!$this->getInput('hideimage')) { + $item['content'] .= '<img src="' . $image . '">'; + } - $item['enclosures'] = array($image); + $item['enclosures'] = [$image]; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getURI(){ - if(!is_null($this->getInput('query'))) { - $uri = self::URI . '/search?q=' . urlencode($this->getInput('query')); + public function getURI() + { + if (!is_null($this->getInput('query'))) { + $uri = self::URI . '/search?q=' . urlencode($this->getInput('query')); - if(!is_null($this->getInput('queryextension'))) { - $uri .= $this->getInput('queryextension'); - } + if (!is_null($this->getInput('queryextension'))) { + $uri .= $this->getInput('queryextension'); + } - return $uri; - } + return $uri; + } - return parent::getURI(); - } + return parent::getURI(); + } } diff --git a/bridges/EuronewsBridge.php b/bridges/EuronewsBridge.php index df11014c..2508a274 100644 --- a/bridges/EuronewsBridge.php +++ b/bridges/EuronewsBridge.php @@ -1,209 +1,211 @@ <?php + class EuronewsBridge extends BridgeAbstract { - const MAINTAINER = 'sqrtminusone'; - const NAME = 'Euronews Bridge'; - const URI = 'https://www.euronews.com/'; - const CACHE_TIMEOUT = 600; // 10 minutes - const DESCRIPTION = 'Return articles from the "Just In" feed of Euronews.'; + const MAINTAINER = 'sqrtminusone'; + const NAME = 'Euronews Bridge'; + const URI = 'https://www.euronews.com/'; + const CACHE_TIMEOUT = 600; // 10 minutes + const DESCRIPTION = 'Return articles from the "Just In" feed of Euronews.'; - const PARAMETERS = array( - '' => array( - 'lang' => array( - 'name' => 'Language', - 'type' => 'list', - 'defaultValue' => 'euronews.com', - 'values' => array( - 'English' => 'euronews.com', - 'French' => 'fr.euronews.com', - 'German' => 'de.euronews.com', - 'Italian' => 'it.euronews.com', - 'Spanish' => 'es.euronews.com', - 'Portuguese' => 'pt.euronews.com', - 'Russian' => 'ru.euronews.com', - 'Turkish' => 'tr.euronews.com', - 'Greek' => 'gr.euronews.com', - 'Hungarian' => 'hu.euronews.com', - 'Persian' => 'per.euronews.com', - 'Arabic' => 'arabic.euronews.com', - /* These versions don't have timeline.json */ - // 'Albanian' => 'euronews.al', - // 'Romanian' => 'euronews.ro', - // 'Georigian' => 'euronewsgeorgia.com', - // 'Bulgarian' => 'euronewsbulgaria.com' - // 'Serbian' => 'euronews.rs' - ) - ), - 'limit' => array( - 'name' => 'Limit of items per feed', - 'required' => true, - 'type' => 'number', - 'defaultValue' => 10, - 'title' => 'Maximum number of returned feed items. Maximum 50, default 10' - ), - ) - ); + const PARAMETERS = [ + '' => [ + 'lang' => [ + 'name' => 'Language', + 'type' => 'list', + 'defaultValue' => 'euronews.com', + 'values' => [ + 'English' => 'euronews.com', + 'French' => 'fr.euronews.com', + 'German' => 'de.euronews.com', + 'Italian' => 'it.euronews.com', + 'Spanish' => 'es.euronews.com', + 'Portuguese' => 'pt.euronews.com', + 'Russian' => 'ru.euronews.com', + 'Turkish' => 'tr.euronews.com', + 'Greek' => 'gr.euronews.com', + 'Hungarian' => 'hu.euronews.com', + 'Persian' => 'per.euronews.com', + 'Arabic' => 'arabic.euronews.com', + /* These versions don't have timeline.json */ + // 'Albanian' => 'euronews.al', + // 'Romanian' => 'euronews.ro', + // 'Georigian' => 'euronewsgeorgia.com', + // 'Bulgarian' => 'euronewsbulgaria.com' + // 'Serbian' => 'euronews.rs' + ] + ], + 'limit' => [ + 'name' => 'Limit of items per feed', + 'required' => true, + 'type' => 'number', + 'defaultValue' => 10, + 'title' => 'Maximum number of returned feed items. Maximum 50, default 10' + ], + ] + ]; - public function collectData() - { - $limit = $this->getInput('limit'); - $root_url = 'https://' . $this->getInput('lang'); - $url = $root_url . '/api/timeline.json?limit=' . $limit; - $json = getContents($url); - $data = json_decode($json, true); + public function collectData() + { + $limit = $this->getInput('limit'); + $root_url = 'https://' . $this->getInput('lang'); + $url = $root_url . '/api/timeline.json?limit=' . $limit; + $json = getContents($url); + $data = json_decode($json, true); - foreach ($data as $datum) { - $datum_uri = $root_url . $datum['fullUrl']; - $url_datum = $this->getItemContent($datum_uri); - $categories = array(); - if (array_key_exists('program', $datum)) { - if (array_key_exists('title', $datum['program'])) { - $categories[] = $datum['program']['title']; - } - } - if (array_key_exists('themes', $datum)) { - foreach ($datum['themes'] as $theme) { - $categories[] = $theme['title']; - } - } - $item = array( - 'uri' => $datum_uri, - 'title' => $datum['title'], - 'uid' => strval($datum['id']), - 'timestamp' => $datum['publishedAt'], - 'content' => $url_datum['content'], - 'author' => $url_datum['author'], - 'enclosures' => $url_datum['enclosures'], - 'categories' => array_unique($categories) - ); - $this->items[] = $item; - } - } + foreach ($data as $datum) { + $datum_uri = $root_url . $datum['fullUrl']; + $url_datum = $this->getItemContent($datum_uri); + $categories = []; + if (array_key_exists('program', $datum)) { + if (array_key_exists('title', $datum['program'])) { + $categories[] = $datum['program']['title']; + } + } + if (array_key_exists('themes', $datum)) { + foreach ($datum['themes'] as $theme) { + $categories[] = $theme['title']; + } + } + $item = [ + 'uri' => $datum_uri, + 'title' => $datum['title'], + 'uid' => strval($datum['id']), + 'timestamp' => $datum['publishedAt'], + 'content' => $url_datum['content'], + 'author' => $url_datum['author'], + 'enclosures' => $url_datum['enclosures'], + 'categories' => array_unique($categories) + ]; + $this->items[] = $item; + } + } - private function getItemContent($url) - { - try { - $html = getSimpleHTMLDOMCached($url); - } catch (Exception $e) { - // Every once in a while it fails with too many redirects - return array('author' => null, 'content' => null, 'enclosures' => null); - } - $data = $html->find('script[type="application/ld+json"]', 0)->innertext; - $json = json_decode($data, true); - $author = 'Euronews'; - $content = ''; - $enclosures = array(); - if (array_key_exists('@graph', $json)) { - foreach ($json['@graph'] as $item) { - if ($item['@type'] == 'NewsArticle') { - if (array_key_exists('author', $item)) { - $author = $item['author']['name']; - } - if (array_key_exists('image', $item)) { - $content .= '<figure>'; - $content .= '<img src="' . $item['image']['url'] . '">'; - $content .= '<figcaption>' . $item['image']['caption'] . '</figcaption>'; - $content .= '</figure><br>'; - } - if (array_key_exists('video', $item)) { - $enclosures[] = $item['video']['contentUrl']; - } - } - } - } + private function getItemContent($url) + { + try { + $html = getSimpleHTMLDOMCached($url); + } catch (Exception $e) { + // Every once in a while it fails with too many redirects + return ['author' => null, 'content' => null, 'enclosures' => null]; + } + $data = $html->find('script[type="application/ld+json"]', 0)->innertext; + $json = json_decode($data, true); + $author = 'Euronews'; + $content = ''; + $enclosures = []; + if (array_key_exists('@graph', $json)) { + foreach ($json['@graph'] as $item) { + if ($item['@type'] == 'NewsArticle') { + if (array_key_exists('author', $item)) { + $author = $item['author']['name']; + } + if (array_key_exists('image', $item)) { + $content .= '<figure>'; + $content .= '<img src="' . $item['image']['url'] . '">'; + $content .= '<figcaption>' . $item['image']['caption'] . '</figcaption>'; + $content .= '</figure><br>'; + } + if (array_key_exists('video', $item)) { + $enclosures[] = $item['video']['contentUrl']; + } + } + } + } - // Normal article - $article_content = $html->find('.c-article-content', 0); - if ($article_content) { - // Usually the .c-article-content is the root of the - // content, but once in a blue moon the root is the second - // div - if ((count($article_content->children()) == 2) - && ($article_content->children(1)->tag == 'div') - ) { - $article_content = $article_content->children(1); - } - // The content is interspersed with links and stuff, so we - // iterate over the children - foreach ($article_content->children() as $element) { - if ($element->tag == 'p') { - $scribble_live = $element->find('#scribblelive-items', 0); - if (is_null($scribble_live)) { - // A normal paragraph - $content .= '<p>' . $element->innertext . '</p>'; - } else { - // LIVE mode - foreach ($scribble_live->children() as $child) { - if ($child->tag == 'div') { - $content .= '<div>' . $child->innertext . '</div>'; - } - } - } - } elseif (preg_match('/h[1-6]/', $element->tag)) { - // Header - $content .= '<h' . $element->tag[1] . '>' . $element->innertext . '</h' . $element->tag[1] . '>'; - } elseif ($element->tag == 'div') { - if (preg_match('/.*widget--type-image.*/', $element->class)) { - // Image - $content .= '<figure>'; - $content .= '<img src="' . $element->find('img', 0)->src . '">'; - $caption = $element->find('figcaption', 0); - if ($caption) { - $content .= '<figcaption>' . $element->plaintext . '</figcaption>'; - } - $content .= '</figure><br>'; - } elseif (preg_match('/.*widget--type-quotation.*/', $element->class)) { - // Quotation - $quote = $element->find('.widget__quoteText', 0); - $author = $element->find('.widget__author', 0); - $content .= '<figure>'; - $content .= '<blockquote>' . $quote->plaintext . '</blockquote>'; - if ($author) { - $content .= '<figcaption>' . $author->plaintext . '</figcaption>'; - } - $content .= '</figure><br>'; - } - } - } - } + // Normal article + $article_content = $html->find('.c-article-content', 0); + if ($article_content) { + // Usually the .c-article-content is the root of the + // content, but once in a blue moon the root is the second + // div + if ( + (count($article_content->children()) == 2) + && ($article_content->children(1)->tag == 'div') + ) { + $article_content = $article_content->children(1); + } + // The content is interspersed with links and stuff, so we + // iterate over the children + foreach ($article_content->children() as $element) { + if ($element->tag == 'p') { + $scribble_live = $element->find('#scribblelive-items', 0); + if (is_null($scribble_live)) { + // A normal paragraph + $content .= '<p>' . $element->innertext . '</p>'; + } else { + // LIVE mode + foreach ($scribble_live->children() as $child) { + if ($child->tag == 'div') { + $content .= '<div>' . $child->innertext . '</div>'; + } + } + } + } elseif (preg_match('/h[1-6]/', $element->tag)) { + // Header + $content .= '<h' . $element->tag[1] . '>' . $element->innertext . '</h' . $element->tag[1] . '>'; + } elseif ($element->tag == 'div') { + if (preg_match('/.*widget--type-image.*/', $element->class)) { + // Image + $content .= '<figure>'; + $content .= '<img src="' . $element->find('img', 0)->src . '">'; + $caption = $element->find('figcaption', 0); + if ($caption) { + $content .= '<figcaption>' . $element->plaintext . '</figcaption>'; + } + $content .= '</figure><br>'; + } elseif (preg_match('/.*widget--type-quotation.*/', $element->class)) { + // Quotation + $quote = $element->find('.widget__quoteText', 0); + $author = $element->find('.widget__author', 0); + $content .= '<figure>'; + $content .= '<blockquote>' . $quote->plaintext . '</blockquote>'; + if ($author) { + $content .= '<figcaption>' . $author->plaintext . '</figcaption>'; + } + $content .= '</figure><br>'; + } + } + } + } - // Video article - if (is_null($article_content)) { - $image = $html->find('.c-article-media__img', 0); - if ($image) { - $content .= '<figure>'; - $content .= '<img src="' . $image->src . '">'; - $content .= '</figure><br>'; - } + // Video article + if (is_null($article_content)) { + $image = $html->find('.c-article-media__img', 0); + if ($image) { + $content .= '<figure>'; + $content .= '<img src="' . $image->src . '">'; + $content .= '</figure><br>'; + } - $description = $html->find('.m-object__description', 0); - if ($description) { - // In some editions the description is a link to the - // current page - $content .= '<div>' . $description->plaintext . '</div>'; - } + $description = $html->find('.m-object__description', 0); + if ($description) { + // In some editions the description is a link to the + // current page + $content .= '<div>' . $description->plaintext . '</div>'; + } - // Euronews usually hosts videos on dailymotion... - $player_div = $html->find('.dmPlayer', 0); - if ($player_div) { - $video_id = $player_div->getAttribute('data-video-id'); - $video_url = 'https://www.dailymotion.com/video/' . $video_id; - $content .= '<a href="' . $video_url . '">' . $video_url . '</a>'; - } + // Euronews usually hosts videos on dailymotion... + $player_div = $html->find('.dmPlayer', 0); + if ($player_div) { + $video_id = $player_div->getAttribute('data-video-id'); + $video_url = 'https://www.dailymotion.com/video/' . $video_id; + $content .= '<a href="' . $video_url . '">' . $video_url . '</a>'; + } - // ...or on YouTube - $player_div = $html->find('.js-player-pfp', 0); - if ($player_div) { - $video_id = $player_div->getAttribute('data-video-id'); - $video_url = 'https://www.youtube.com/watch?v=' . $video_id; - $content .= '<a href="' . $video_url . '">' . $video_url . '</a>'; - } - } + // ...or on YouTube + $player_div = $html->find('.js-player-pfp', 0); + if ($player_div) { + $video_id = $player_div->getAttribute('data-video-id'); + $video_url = 'https://www.youtube.com/watch?v=' . $video_id; + $content .= '<a href="' . $video_url . '">' . $video_url . '</a>'; + } + } - return array( - 'author' => $author, - 'content' => $content, - 'enclosures' => $enclosures - ); - } + return [ + 'author' => $author, + 'content' => $content, + 'enclosures' => $enclosures + ]; + } } diff --git a/bridges/ExecuteProgramBridge.php b/bridges/ExecuteProgramBridge.php index 24342d1f..a2da864e 100644 --- a/bridges/ExecuteProgramBridge.php +++ b/bridges/ExecuteProgramBridge.php @@ -2,37 +2,37 @@ class ExecuteProgramBridge extends BridgeAbstract { - const NAME = 'Execute Program Blog'; - const URI = 'https://www.executeprogram.com/blog'; - const DESCRIPTION = 'Unofficial feed for the www.executeprogram.com blog'; - const MAINTAINER = 'dvikan'; + const NAME = 'Execute Program Blog'; + const URI = 'https://www.executeprogram.com/blog'; + const DESCRIPTION = 'Unofficial feed for the www.executeprogram.com blog'; + const MAINTAINER = 'dvikan'; - public function collectData() - { - $data = json_decode(getContents('https://www.executeprogram.com/api/pages/blog')); + public function collectData() + { + $data = json_decode(getContents('https://www.executeprogram.com/api/pages/blog')); - foreach ($data->posts as $post) { - $year = $post->date->year; - $month = $post->date->month; - $day = $post->date->day; + foreach ($data->posts as $post) { + $year = $post->date->year; + $month = $post->date->month; + $day = $post->date->day; - $item = array(); - $item['uri'] = sprintf('https://www.executeprogram.com/blog/%s', $post->slug); - $item['title'] = $post->title; - $dateTime = \DateTime::createFromFormat('Y-m-d', $year . '-' . $month . '-' . $day); - $item['timestamp'] = $dateTime->format('U'); - $item['content'] = $post->body; + $item = []; + $item['uri'] = sprintf('https://www.executeprogram.com/blog/%s', $post->slug); + $item['title'] = $post->title; + $dateTime = \DateTime::createFromFormat('Y-m-d', $year . '-' . $month . '-' . $day); + $item['timestamp'] = $dateTime->format('U'); + $item['content'] = $post->body; - $this->items[] = $item; - } + $this->items[] = $item; + } - usort($this->items, function ($a, $b) { - return $a['timestamp'] < $b['timestamp']; - }); - } + usort($this->items, function ($a, $b) { + return $a['timestamp'] < $b['timestamp']; + }); + } - public function getIcon() - { - return 'https://www.executeprogram.com/favicon.ico'; - } + public function getIcon() + { + return 'https://www.executeprogram.com/favicon.ico'; + } } diff --git a/bridges/ExplosmBridge.php b/bridges/ExplosmBridge.php index cfe42195..8874c6cb 100644 --- a/bridges/ExplosmBridge.php +++ b/bridges/ExplosmBridge.php @@ -1,59 +1,61 @@ <?php -class ExplosmBridge extends BridgeAbstract { - const MAINTAINER = 'bockiii'; - const NAME = 'Explosm Bridge'; - const URI = 'https://www.explosm.net/'; - const CACHE_TIMEOUT = 4800; //2hours - const DESCRIPTION = 'Returns the last 5 comics'; - const PARAMETERS = array( - 'Get latest posts' => array( - 'limit' => array( - 'name' => 'Posts limit', - 'type' => 'number', - 'title' => 'Maximum number of items to return', - 'defaultValue' => 5 - ) - ) - ); +class ExplosmBridge extends BridgeAbstract +{ + const MAINTAINER = 'bockiii'; + const NAME = 'Explosm Bridge'; + const URI = 'https://www.explosm.net/'; + const CACHE_TIMEOUT = 4800; //2hours + const DESCRIPTION = 'Returns the last 5 comics'; + const PARAMETERS = [ + 'Get latest posts' => [ + 'limit' => [ + 'name' => 'Posts limit', + 'type' => 'number', + 'title' => 'Maximum number of items to return', + 'defaultValue' => 5 + ] + ] + ]; - public function collectData(){ - $limit = $this->getInput('limit'); - $latest = getSimpleHTMLDOM('https://explosm.net/comics/latest'); - $image = $latest->find('div[id=comic]', 0)->find('img', 0)->getAttribute('src'); - $date_string = $latest->find('p[class*=Author__P]', 0)->innertext; - $next_data_string = $latest->find('script[id=__NEXT_DATA__]', 0)->innertext; - $exp = '/{\\\"latest\\\":\[{\\\"slug\\\":\\\"(.*?)\\ /'; - $reg_array = array(); - preg_match($exp, $next_data_string, $reg_array); - $comic_id = $reg_array[1]; - $comic_id = substr($comic_id, 0, strpos($comic_id, '\\')); - $item = array(); - $item['uri'] = $this::URI . 'comics/' . $comic_id; - $item['uid'] = $this::URI . 'comics/' . $comic_id; - $item['title'] = 'Comic for ' . $date_string; - $item['timestamp'] = strtotime($date_string); - $item['author'] = $latest->find('p[class*=Author__P]', 2)->innertext; - $item['content'] = '<img src="' . $image . '" />'; - $this->items[] = $item; + public function collectData() + { + $limit = $this->getInput('limit'); + $latest = getSimpleHTMLDOM('https://explosm.net/comics/latest'); + $image = $latest->find('div[id=comic]', 0)->find('img', 0)->getAttribute('src'); + $date_string = $latest->find('p[class*=Author__P]', 0)->innertext; + $next_data_string = $latest->find('script[id=__NEXT_DATA__]', 0)->innertext; + $exp = '/{\\\"latest\\\":\[{\\\"slug\\\":\\\"(.*?)\\ /'; + $reg_array = []; + preg_match($exp, $next_data_string, $reg_array); + $comic_id = $reg_array[1]; + $comic_id = substr($comic_id, 0, strpos($comic_id, '\\')); + $item = []; + $item['uri'] = $this::URI . 'comics/' . $comic_id; + $item['uid'] = $this::URI . 'comics/' . $comic_id; + $item['title'] = 'Comic for ' . $date_string; + $item['timestamp'] = strtotime($date_string); + $item['author'] = $latest->find('p[class*=Author__P]', 2)->innertext; + $item['content'] = '<img src="' . $image . '" />'; + $this->items[] = $item; - $next_comic = substr($this::URI, 0, -1) - . $latest->find('div[class*=MainComic__Selector]', 0)->find('a', 0)->getAttribute('href'); - // use index 1 as the latest comic was already found - for ($i = 1; $i <= $limit; $i++) { - $this_comic = getSimpleHTMLDOM($next_comic); - $image = $this_comic->find('div[id=comic]', 0)->find('img', 0)->getAttribute('src'); - $date_string = $this_comic->find('p[class*=Author__P]', 0)->innertext; - $item = array(); - $item['uri'] = $next_comic; - $item['uid'] = $next_comic; - $item['title'] = 'Comic for ' . $date_string; - $item['timestamp'] = strtotime($date_string); - $item['author'] = $this_comic->find('p[class*=Author__P]', 2)->innertext; - $item['content'] = '<img src="' . $image . '" />'; - $this->items[] = $item; - $next_comic = substr($this::URI, 0, -1) - . $this_comic->find('div[class*=MainComic__Selector]', 0)->find('a', 0)->getAttribute('href'); // get next comic link - } - } + $next_comic = substr($this::URI, 0, -1) + . $latest->find('div[class*=MainComic__Selector]', 0)->find('a', 0)->getAttribute('href'); + // use index 1 as the latest comic was already found + for ($i = 1; $i <= $limit; $i++) { + $this_comic = getSimpleHTMLDOM($next_comic); + $image = $this_comic->find('div[id=comic]', 0)->find('img', 0)->getAttribute('src'); + $date_string = $this_comic->find('p[class*=Author__P]', 0)->innertext; + $item = []; + $item['uri'] = $next_comic; + $item['uid'] = $next_comic; + $item['title'] = 'Comic for ' . $date_string; + $item['timestamp'] = strtotime($date_string); + $item['author'] = $this_comic->find('p[class*=Author__P]', 2)->innertext; + $item['content'] = '<img src="' . $image . '" />'; + $this->items[] = $item; + $next_comic = substr($this::URI, 0, -1) + . $this_comic->find('div[class*=MainComic__Selector]', 0)->find('a', 0)->getAttribute('href'); // get next comic link + } + } } diff --git a/bridges/ExtremeDownloadBridge.php b/bridges/ExtremeDownloadBridge.php index 60301fe7..074045df 100644 --- a/bridges/ExtremeDownloadBridge.php +++ b/bridges/ExtremeDownloadBridge.php @@ -1,111 +1,116 @@ <?php -class ExtremeDownloadBridge extends BridgeAbstract { - const NAME = 'Extreme Download'; - const URI = 'https://www.extreme-down.plus/'; - const DESCRIPTION = 'Suivi de série sur Extreme Download'; - const MAINTAINER = 'sysadminstory'; - const PARAMETERS = array( - 'Suivre la publication des épisodes d\'une série en cours de diffusion' => array( - 'url' => array( - 'name' => 'URL de la série', - 'type' => 'text', - 'required' => true, - 'title' => 'URL d\'une série sans le https://www.extreme-down.plus/', - 'exampleValue' => 'series-hd/hd-series-vostfr/46631-halt-and-catch-fire-saison-04-vostfr-hdtv-720p.html'), - 'filter' => array( - 'name' => 'Type de contenu', - 'type' => 'list', - 'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux', - 'values' => array( - 'Streaming et Téléchargement' => 'both', - 'Téléchargement' => 'download', - 'Streaming' => 'streaming' - ) - ) - ) - ); - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI . $this->getInput('url')); +class ExtremeDownloadBridge extends BridgeAbstract +{ + const NAME = 'Extreme Download'; + const URI = 'https://www.extreme-down.plus/'; + const DESCRIPTION = 'Suivi de série sur Extreme Download'; + const MAINTAINER = 'sysadminstory'; + const PARAMETERS = [ + 'Suivre la publication des épisodes d\'une série en cours de diffusion' => [ + 'url' => [ + 'name' => 'URL de la série', + 'type' => 'text', + 'required' => true, + 'title' => 'URL d\'une série sans le https://www.extreme-down.plus/', + 'exampleValue' => 'series-hd/hd-series-vostfr/46631-halt-and-catch-fire-saison-04-vostfr-hdtv-720p.html'], + 'filter' => [ + 'name' => 'Type de contenu', + 'type' => 'list', + 'title' => 'Type de contenu à suivre : Téléchargement, Streaming ou les deux', + 'values' => [ + 'Streaming et Téléchargement' => 'both', + 'Téléchargement' => 'download', + 'Streaming' => 'streaming' + ] + ] + ] + ]; - $filter = $this->getInput('filter'); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI . $this->getInput('url')); - $typesText = array( - 'download' => 'Téléchargement', - 'streaming' => 'Streaming' - ); + $filter = $this->getInput('filter'); - // Get the TV show title - $this->showTitle = trim($html->find('span[id=news-title]', 0)->plaintext); + $typesText = [ + 'download' => 'Téléchargement', + 'streaming' => 'Streaming' + ]; - $list = $html->find('div[class=prez_7]'); - foreach($list as $element) { - $add = false; - // Link type is needed is needed to generate an unique link - $type = $this->findLinkType($element); - if($filter == 'both') { - $add = true; - } else { - if($type == $filter) { - $add = true; - } - } - if($add == true) { - $item = array(); + // Get the TV show title + $this->showTitle = trim($html->find('span[id=news-title]', 0)->plaintext); - // Get the element name - $title = $element->plaintext; + $list = $html->find('div[class=prez_7]'); + foreach ($list as $element) { + $add = false; + // Link type is needed is needed to generate an unique link + $type = $this->findLinkType($element); + if ($filter == 'both') { + $add = true; + } else { + if ($type == $filter) { + $add = true; + } + } + if ($add == true) { + $item = []; - // Get thee element links - $links = $element->next_sibling()->innertext; + // Get the element name + $title = $element->plaintext; - $item['content'] = $links; - $item['title'] = $this->showTitle . ' ' . $title . ' - ' . $typesText[$type]; - // As RSS Bridge use the URI as GUID they need to be unique : adding a md5 hash of the title element - // should geneerate unique URI to prevent confusion for RSS readers - $item['uri'] = self::URI . $this->getInput('url') . '#' . hash('md5', $item['title']); + // Get thee element links + $links = $element->next_sibling()->innertext; - $this->items[] = $item; - } - } - } + $item['content'] = $links; + $item['title'] = $this->showTitle . ' ' . $title . ' - ' . $typesText[$type]; + // As RSS Bridge use the URI as GUID they need to be unique : adding a md5 hash of the title element + // should geneerate unique URI to prevent confusion for RSS readers + $item['uri'] = self::URI . $this->getInput('url') . '#' . hash('md5', $item['title']); - public function getName(){ - switch($this->queriedContext) { - case 'Suivre la publication des épisodes d\'une série en cours de diffusion': - return $this->showTitle . ' - ' . self::NAME; - break; - default: - return self::NAME; - } - } + $this->items[] = $item; + } + } + } - public function getURI() { - switch($this->queriedContext) { - case 'Suivre la publication des épisodes d\'une série en cours de diffusion': - return self::URI . $this->getInput('url'); - break; - default: - return self::URI; - } - } + public function getName() + { + switch ($this->queriedContext) { + case 'Suivre la publication des épisodes d\'une série en cours de diffusion': + return $this->showTitle . ' - ' . self::NAME; + break; + default: + return self::NAME; + } + } - private function findLinkType($element) - { - $return = ''; - // Walk through all elements in the reverse order until finding one with class 'presz_2' - while($element->class != 'prez_2') { - $element = $element->prev_sibling(); - } - $text = html_entity_decode($element->plaintext); + public function getURI() + { + switch ($this->queriedContext) { + case 'Suivre la publication des épisodes d\'une série en cours de diffusion': + return self::URI . $this->getInput('url'); + break; + default: + return self::URI; + } + } - // Regarding the text of the element, return the according link type - if(stristr($text, 'téléchargement') != false) { - $return = 'download'; - } else if(stristr($text, 'streaming') != false) { - $return = 'streaming'; - } + private function findLinkType($element) + { + $return = ''; + // Walk through all elements in the reverse order until finding one with class 'presz_2' + while ($element->class != 'prez_2') { + $element = $element->prev_sibling(); + } + $text = html_entity_decode($element->plaintext); - return $return; - } + // Regarding the text of the element, return the according link type + if (stristr($text, 'téléchargement') != false) { + $return = 'download'; + } elseif (stristr($text, 'streaming') != false) { + $return = 'streaming'; + } + + return $return; + } } diff --git a/bridges/FB2Bridge.php b/bridges/FB2Bridge.php index 46a92c56..efebd48b 100644 --- a/bridges/FB2Bridge.php +++ b/bridges/FB2Bridge.php @@ -1,311 +1,327 @@ <?php -class FB2Bridge extends BridgeAbstract { - const MAINTAINER = 'teromene'; - const NAME = 'Facebook Bridge | Touch Site'; - const URI = 'https://www.facebook.com/'; - const CACHE_TIMEOUT = 1000; - const DESCRIPTION = 'Input a page title or a profile log. For a profile log, +class FB2Bridge extends BridgeAbstract +{ + const MAINTAINER = 'teromene'; + const NAME = 'Facebook Bridge | Touch Site'; + const URI = 'https://www.facebook.com/'; + const CACHE_TIMEOUT = 1000; + const DESCRIPTION = 'Input a page title or a profile log. For a profile log, please insert the parameter as follow : myExamplePage/132621766841117'; - const PARAMETERS = array( array( - 'u' => array( - 'name' => 'Username', - 'required' => true - ), - 'abbrev_name' => array( - 'name' => 'Abbreviate author name in title', - 'type' => 'checkbox', - 'defaultValue' => true, - ), - )); - - public function getIcon() { - return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico'; - } - - public function collectData(){ - - //Utility function for cleaning a Facebook link - $unescape_fb_link = function($matches){ - if(is_array($matches) && count($matches) > 1) { - $link = $matches[1]; - if(strpos($link, '/') === 0) - $link = self::URI . substr($link, 1); - if(strpos($link, 'facebook.com/l.php?u=') !== false) - $link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&')); - return ' href="' . $link . '"'; - } - }; - - //Utility function for converting facebook emoticons - $unescape_fb_emote = function($matches){ - static $facebook_emoticons = array( - 'smile' => ':)', - 'frown' => ':(', - 'tongue' => ':P', - 'grin' => ':D', - 'gasp' => ':O', - 'wink' => ';)', - 'pacman' => ':<', - 'grumpy' => '>_<', - 'unsure' => ':/', - 'cry' => ':\'(', - 'kiki' => '^_^', - 'glasses' => '8-)', - 'sunglasses' => 'B-)', - 'heart' => '<3', - 'devil' => ']:D', - 'angel' => '0:)', - 'squint' => '-_-', - 'confused' => 'o_O', - 'upset' => 'xD', - 'colonthree' => ':3', - 'like' => '👍'); - $len = count($matches); - if ($len > 1) - for ($i = 1; $i < $len; $i++) - foreach ($facebook_emoticons as $name => $emote) - if ($matches[$i] === $name) - return $emote; - return $matches[0]; - }; - - if($this->getInput('u') !== null) { - $page = 'https://touch.facebook.com/' . $this->getInput('u'); - $cookies = $this->getCookies($page); - $pageInfo = $this->getPageInfos($page, $cookies); - - if($pageInfo['userId'] === null) { - returnClientError(<<<EOD + const PARAMETERS = [ [ + 'u' => [ + 'name' => 'Username', + 'required' => true + ], + 'abbrev_name' => [ + 'name' => 'Abbreviate author name in title', + 'type' => 'checkbox', + 'defaultValue' => true, + ], + ]]; + + public function getIcon() + { + return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico'; + } + + public function collectData() + { + //Utility function for cleaning a Facebook link + $unescape_fb_link = function ($matches) { + if (is_array($matches) && count($matches) > 1) { + $link = $matches[1]; + if (strpos($link, '/') === 0) { + $link = self::URI . substr($link, 1); + } + if (strpos($link, 'facebook.com/l.php?u=') !== false) { + $link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&')); + } + return ' href="' . $link . '"'; + } + }; + + //Utility function for converting facebook emoticons + $unescape_fb_emote = function ($matches) { + static $facebook_emoticons = [ + 'smile' => ':)', + 'frown' => ':(', + 'tongue' => ':P', + 'grin' => ':D', + 'gasp' => ':O', + 'wink' => ';)', + 'pacman' => ':<', + 'grumpy' => '>_<', + 'unsure' => ':/', + 'cry' => ':\'(', + 'kiki' => '^_^', + 'glasses' => '8-)', + 'sunglasses' => 'B-)', + 'heart' => '<3', + 'devil' => ']:D', + 'angel' => '0:)', + 'squint' => '-_-', + 'confused' => 'o_O', + 'upset' => 'xD', + 'colonthree' => ':3', + 'like' => '👍']; + $len = count($matches); + if ($len > 1) { + for ($i = 1; $i < $len; $i++) { + foreach ($facebook_emoticons as $name => $emote) { + if ($matches[$i] === $name) { + return $emote; + } + } + } + } + return $matches[0]; + }; + + if ($this->getInput('u') !== null) { + $page = 'https://touch.facebook.com/' . $this->getInput('u'); + $cookies = $this->getCookies($page); + $pageInfo = $this->getPageInfos($page, $cookies); + + if ($pageInfo['userId'] === null) { + returnClientError(<<<EOD Unable to get the page id. You should consider getting the ID by hand, then importing it into FB2Bridge EOD - ); - } elseif($pageInfo['userId'] == -1) { - returnClientError(<<<EOD + ); + } elseif ($pageInfo['userId'] == -1) { + returnClientError(<<<EOD This page is not accessible without being logged in. EOD - ); - } - } - - //Build the string for the first request - $requestString = 'https://touch.facebook.com/page_content_list_view/more/?page_id=' - . $pageInfo['userId'] - . '&start_cursor=1&num_to_fetch=105&surface_type=timeline'; - $fileContent = getContents($requestString); - $html = $this->buildContent($fileContent); - $author = $pageInfo['username']; - - foreach($html->find('article') as $content) { - - $item = array(); - - preg_match('/publish_time\\\":([0-9]+),/', $content->getAttribute('data-store', 0), $match); - if(isset($match[1])) - $timestamp = $match[1]; - else - $timestamp = 0; - - $item['uri'] = html_entity_decode('https://touch.facebook.com' - . $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES); - - //Decode images - $imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) { - return "<img src='" . str_replace(array('\\3a ', '\\3d ', '\\26 '), array(':', '=', '&'), $matches[1]) . "' />"; - }, $content); - $content = str_get_html($imagecleaned); - - if($content->find('header', 0) !== null) { - $content->find('header', 0)->innertext = ''; - } - - if($content->find('footer', 0) !== null) { - $content->find('footer', 0)->innertext = ''; - } - - // Replace emoticon images by their textual representation (part of the span) - foreach($content->find('span[title*="emoticon"]') as $emoticon) { - $emoticon->innertext = $emoticon->find('span[aria-hidden="true"]', 0)->innertext; - } - - //Remove html nodes, keep only img, links, basic formatting - $content = strip_tags($content, '<a><img><i><u><br><p><h3><h4><section>'); - - //Adapt link hrefs: convert relative links into absolute links and bypass external link redirection - $content = preg_replace_callback('/ href=\"([^"]+)\"/i', $unescape_fb_link, $content); - - //Clean useless html tag properties and fix link closing tags - foreach (array( - 'onmouseover', - 'onclick', - 'target', - 'ajaxify', - 'tabindex', - 'class', - 'data-[^=]*', - 'aria-[^=]*', - 'role', - 'rel', - 'id') as $property_name) - $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content); - $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content); - - //Convert textual representation of emoticons eg - // "<i><u>smile emoticon</u></i>" back to ASCII emoticons eg ":)" - $content = preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', $unescape_fb_emote, $content); - - //Remove the "...Plus" tag - $content = preg_replace( - '/… (<span>|)<a href="https:\/\/www\.facebook\.com\/story\.php\?story_fbid=.*?<\/a>/m', - '', $content, 1); - - //Remove tracking images - $content = preg_replace('/<img src=\'.*?safe_image\.php.*?\' \/>/m', '', $content); - - //Remove the double section tags - $content = str_replace( - array('<section><section>', '</section></section>'), - array('<section>', '</section>'), - $content - ); - - //Move the section tag link upper, if it is down - $content = str_get_html($content); - $sectionContent = $content->find('section', 0); - if($sectionContent != null) { - $sectionLink = $sectionContent->nextSibling(); - if($sectionLink != null) { - $fullLink = '<a href="' . $sectionLink->getAttribute('href') . '">' . $sectionContent->innertext . '</a>'; - $sectionContent->innertext = $fullLink; - } - } - - //Move the href tag upper if it is inside the section - foreach($content->find('section > a') as $sectionToFix) { - $sectionLink = $sectionToFix->getAttribute('href'); - $section = $sectionToFix->parent(); - $section->outertext = '<a href="' . $sectionLink . '">' . $section . '</a>'; - } - - $item['content'] = html_entity_decode($content, ENT_QUOTES); - - $title = $author; - if ($this->getInput('abbrev_name') === true) { - if (strlen($title) > 24) - $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...'; - } - $title = $title . ' | ' . strip_tags($content); - if (strlen($title) > 64) - $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...'; - - $item['title'] = html_entity_decode($title, ENT_QUOTES); - $item['author'] = html_entity_decode($author, ENT_QUOTES); - $item['timestamp'] = html_entity_decode($timestamp, ENT_QUOTES); - - if($item['timestamp'] != 0) - array_push($this->items, $item); - } - - } - - //Builds the HTML from the encoded JS that Facebook provides. - private function buildContent($pageContent){ - // The html ends with: - // /div>","replaceifexists - $regex = '/\\"html\\":(\".+\/div>"),"replace/'; - preg_match($regex, $pageContent, $result); - - $htmlContent = json_decode($result[1]); - $htmlContent = preg_replace('/(?<!style)="(.*?)"/', '=\'$1\'', $htmlContent); - $htmlContent = html_entity_decode($htmlContent, ENT_QUOTES, 'UTF-8'); - - return str_get_html($htmlContent); - } - - //Builds the cookie from the page, as Facebook sometimes refuses to give - //the page if no cookie is provided. - private function getCookies($pageURL){ - - $ctx = stream_context_create(array( - 'http' => array( - 'user_agent' => Configuration::getConfig('http', 'useragent'), - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' - ) - ) - ); - $a = file_get_contents($pageURL, 0, $ctx); - - //First request to get the cookie - $cookies = ''; - foreach($http_response_header as $hdr) { - if(strpos($hdr, 'Set-Cookie') !== false) { - $cLine = explode(':', $hdr)[1]; - $cLine = explode(';', $cLine)[0]; - $cookies .= ';' . $cLine; - } - } - - return substr($cookies, 1); - } - - //Get the page ID and username from the Facebook page. - private function getPageInfos($page, $cookies){ - - $context = stream_context_create(array( - 'http' => array( - 'user_agent' => Configuration::getConfig('http', 'useragent'), - 'header' => 'Cookie: ' . $cookies - ) - ) - ); - - $pageContent = file_get_contents($page, 0, $context); - - if(strpos($pageContent, 'signup-button') != false) { - return -1; - } - - //Get the username - $usernameRegex = '/data-nt=\"FB:TEXT4\">(.*?)<\/div>/m'; - preg_match($usernameRegex, $pageContent, $usernameMatches); - if(count($usernameMatches) > 0) { - $username = strip_tags($usernameMatches[1]); - } else { - $username = $this->getInput('u'); - } - - //Get the page ID if we don't have a captcha - $regex = '/page_id=([0-9]*)&/'; - preg_match($regex, $pageContent, $matches); - - if(count($matches) > 0) { - return array('userId' => $matches[1], 'username' => $username); - } - - //Get the page ID if we do have a captcha - $regex = '/"pageID":"([0-9]*)"/'; - preg_match($regex, $pageContent, $matches); - - return array('userId' => $matches[1], 'username' => $username); - - } - - public function getName(){ - $username = $this->getInput('u'); - if (isset($username)) { - return $this->getInput('u') . ' | Facebook'; - } else { - return self::NAME; - } - } - - public function getURI(){ - $username = $this->getInput('u'); - if (isset($username)) { - return 'https://facebook.com/' . $this->getInput('u') . '/posts'; - } else { - return self::URI; - } - } + ); + } + } + + //Build the string for the first request + $requestString = 'https://touch.facebook.com/page_content_list_view/more/?page_id=' + . $pageInfo['userId'] + . '&start_cursor=1&num_to_fetch=105&surface_type=timeline'; + $fileContent = getContents($requestString); + $html = $this->buildContent($fileContent); + $author = $pageInfo['username']; + + foreach ($html->find('article') as $content) { + $item = []; + + preg_match('/publish_time\\\":([0-9]+),/', $content->getAttribute('data-store', 0), $match); + if (isset($match[1])) { + $timestamp = $match[1]; + } else { + $timestamp = 0; + } + + $item['uri'] = html_entity_decode('https://touch.facebook.com' + . $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES); + + //Decode images + $imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) { + return "<img src='" . str_replace(['\\3a ', '\\3d ', '\\26 '], [':', '=', '&'], $matches[1]) . "' />"; + }, $content); + $content = str_get_html($imagecleaned); + + if ($content->find('header', 0) !== null) { + $content->find('header', 0)->innertext = ''; + } + + if ($content->find('footer', 0) !== null) { + $content->find('footer', 0)->innertext = ''; + } + + // Replace emoticon images by their textual representation (part of the span) + foreach ($content->find('span[title*="emoticon"]') as $emoticon) { + $emoticon->innertext = $emoticon->find('span[aria-hidden="true"]', 0)->innertext; + } + + //Remove html nodes, keep only img, links, basic formatting + $content = strip_tags($content, '<a><img><i><u><br><p><h3><h4><section>'); + + //Adapt link hrefs: convert relative links into absolute links and bypass external link redirection + $content = preg_replace_callback('/ href=\"([^"]+)\"/i', $unescape_fb_link, $content); + + //Clean useless html tag properties and fix link closing tags + foreach ( + [ + 'onmouseover', + 'onclick', + 'target', + 'ajaxify', + 'tabindex', + 'class', + 'data-[^=]*', + 'aria-[^=]*', + 'role', + 'rel', + 'id'] as $property_name + ) { + $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content); + } + $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content); + + //Convert textual representation of emoticons eg + // "<i><u>smile emoticon</u></i>" back to ASCII emoticons eg ":)" + $content = preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', $unescape_fb_emote, $content); + + //Remove the "...Plus" tag + $content = preg_replace( + '/… (<span>|)<a href="https:\/\/www\.facebook\.com\/story\.php\?story_fbid=.*?<\/a>/m', + '', + $content, + 1 + ); + + //Remove tracking images + $content = preg_replace('/<img src=\'.*?safe_image\.php.*?\' \/>/m', '', $content); + + //Remove the double section tags + $content = str_replace( + ['<section><section>', '</section></section>'], + ['<section>', '</section>'], + $content + ); + + //Move the section tag link upper, if it is down + $content = str_get_html($content); + $sectionContent = $content->find('section', 0); + if ($sectionContent != null) { + $sectionLink = $sectionContent->nextSibling(); + if ($sectionLink != null) { + $fullLink = '<a href="' . $sectionLink->getAttribute('href') . '">' . $sectionContent->innertext . '</a>'; + $sectionContent->innertext = $fullLink; + } + } + + //Move the href tag upper if it is inside the section + foreach ($content->find('section > a') as $sectionToFix) { + $sectionLink = $sectionToFix->getAttribute('href'); + $section = $sectionToFix->parent(); + $section->outertext = '<a href="' . $sectionLink . '">' . $section . '</a>'; + } + + $item['content'] = html_entity_decode($content, ENT_QUOTES); + + $title = $author; + if ($this->getInput('abbrev_name') === true) { + if (strlen($title) > 24) { + $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...'; + } + } + $title = $title . ' | ' . strip_tags($content); + if (strlen($title) > 64) { + $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...'; + } + + $item['title'] = html_entity_decode($title, ENT_QUOTES); + $item['author'] = html_entity_decode($author, ENT_QUOTES); + $item['timestamp'] = html_entity_decode($timestamp, ENT_QUOTES); + + if ($item['timestamp'] != 0) { + array_push($this->items, $item); + } + } + } + + //Builds the HTML from the encoded JS that Facebook provides. + private function buildContent($pageContent) + { + // The html ends with: + // /div>","replaceifexists + $regex = '/\\"html\\":(\".+\/div>"),"replace/'; + preg_match($regex, $pageContent, $result); + + $htmlContent = json_decode($result[1]); + $htmlContent = preg_replace('/(?<!style)="(.*?)"/', '=\'$1\'', $htmlContent); + $htmlContent = html_entity_decode($htmlContent, ENT_QUOTES, 'UTF-8'); + + return str_get_html($htmlContent); + } + + //Builds the cookie from the page, as Facebook sometimes refuses to give + //the page if no cookie is provided. + private function getCookies($pageURL) + { + $ctx = stream_context_create([ + 'http' => [ + 'user_agent' => Configuration::getConfig('http', 'useragent'), + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' + ] + ]); + $a = file_get_contents($pageURL, 0, $ctx); + + //First request to get the cookie + $cookies = ''; + foreach ($http_response_header as $hdr) { + if (strpos($hdr, 'Set-Cookie') !== false) { + $cLine = explode(':', $hdr)[1]; + $cLine = explode(';', $cLine)[0]; + $cookies .= ';' . $cLine; + } + } + + return substr($cookies, 1); + } + + //Get the page ID and username from the Facebook page. + private function getPageInfos($page, $cookies) + { + $context = stream_context_create([ + 'http' => [ + 'user_agent' => Configuration::getConfig('http', 'useragent'), + 'header' => 'Cookie: ' . $cookies + ] + ]); + + $pageContent = file_get_contents($page, 0, $context); + + if (strpos($pageContent, 'signup-button') != false) { + return -1; + } + + //Get the username + $usernameRegex = '/data-nt=\"FB:TEXT4\">(.*?)<\/div>/m'; + preg_match($usernameRegex, $pageContent, $usernameMatches); + if (count($usernameMatches) > 0) { + $username = strip_tags($usernameMatches[1]); + } else { + $username = $this->getInput('u'); + } + + //Get the page ID if we don't have a captcha + $regex = '/page_id=([0-9]*)&/'; + preg_match($regex, $pageContent, $matches); + + if (count($matches) > 0) { + return ['userId' => $matches[1], 'username' => $username]; + } + + //Get the page ID if we do have a captcha + $regex = '/"pageID":"([0-9]*)"/'; + preg_match($regex, $pageContent, $matches); + + return ['userId' => $matches[1], 'username' => $username]; + } + + public function getName() + { + $username = $this->getInput('u'); + if (isset($username)) { + return $this->getInput('u') . ' | Facebook'; + } else { + return self::NAME; + } + } + + public function getURI() + { + $username = $this->getInput('u'); + if (isset($username)) { + return 'https://facebook.com/' . $this->getInput('u') . '/posts'; + } else { + return self::URI; + } + } } diff --git a/bridges/FDroidBridge.php b/bridges/FDroidBridge.php index ca494b8c..d5663903 100644 --- a/bridges/FDroidBridge.php +++ b/bridges/FDroidBridge.php @@ -1,83 +1,88 @@ <?php -class FDroidBridge extends BridgeAbstract { - const MAINTAINER = 'Mitsukarenai'; - const NAME = 'F-Droid Bridge'; - const URI = 'https://f-droid.org/'; - const CACHE_TIMEOUT = 60 * 60 * 4; // 4 hours - const DESCRIPTION = 'Returns latest added/updated apps on the open-source Android apps repository F-Droid'; +class FDroidBridge extends BridgeAbstract +{ + const MAINTAINER = 'Mitsukarenai'; + const NAME = 'F-Droid Bridge'; + const URI = 'https://f-droid.org/'; + const CACHE_TIMEOUT = 60 * 60 * 4; // 4 hours + const DESCRIPTION = 'Returns latest added/updated apps on the open-source Android apps repository F-Droid'; - const PARAMETERS = array( array( - 'u' => array( - 'name' => 'Widget selection', - 'type' => 'list', - 'values' => array( - 'Latest added apps' => 'added', - 'Latest updated apps' => 'updated' - ) - ) - )); + const PARAMETERS = [ [ + 'u' => [ + 'name' => 'Widget selection', + 'type' => 'list', + 'values' => [ + 'Latest added apps' => 'added', + 'Latest updated apps' => 'updated' + ] + ] + ]]; - public function getIcon() { - return self::URI . 'assets/favicon.ico?v=8j6PKzW9Mk'; - } + public function getIcon() + { + return self::URI . 'assets/favicon.ico?v=8j6PKzW9Mk'; + } - private function getTimestamp($url) { - $curlOptions = array( - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_NOBODY => true, - CURLOPT_CONNECTTIMEOUT => 19, - CURLOPT_TIMEOUT => 19, - ); - $ch = curl_init($url); - curl_setopt_array($ch, $curlOptions); - $curlHeaders = curl_exec($ch); - $curlError = curl_error($ch); - curl_close($ch); - if(!empty($curlError)) - return false; - $curlHeaders = explode("\n", $curlHeaders); - $timestamp = false; - foreach($curlHeaders as $header) { - if(strpos($header, 'Last-Modified') !== false) { - $timestamp = str_replace('Last-Modified: ', '', $header); - $timestamp = strtotime($timestamp); - } - } - return $timestamp; - } + private function getTimestamp($url) + { + $curlOptions = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_NOBODY => true, + CURLOPT_CONNECTTIMEOUT => 19, + CURLOPT_TIMEOUT => 19, + ]; + $ch = curl_init($url); + curl_setopt_array($ch, $curlOptions); + $curlHeaders = curl_exec($ch); + $curlError = curl_error($ch); + curl_close($ch); + if (!empty($curlError)) { + return false; + } + $curlHeaders = explode("\n", $curlHeaders); + $timestamp = false; + foreach ($curlHeaders as $header) { + if (strpos($header, 'Last-Modified') !== false) { + $timestamp = str_replace('Last-Modified: ', '', $header); + $timestamp = strtotime($timestamp); + } + } + return $timestamp; + } - public function collectData(){ - $url = self::URI; - $html = getSimpleHTMLDOM($url); + public function collectData() + { + $url = self::URI; + $html = getSimpleHTMLDOM($url); - // targetting the corresponding widget based on user selection - // "updated" is the 5th widget on the page, "added" is the 6th + // targetting the corresponding widget based on user selection + // "updated" is the 5th widget on the page, "added" is the 6th - switch($this->getInput('u')) { - case 'updated': - $html_widget = $html->find('div.sidebar-widget', 5); - break; - default: - $html_widget = $html->find('div.sidebar-widget', 6); - break; - } + switch ($this->getInput('u')) { + case 'updated': + $html_widget = $html->find('div.sidebar-widget', 5); + break; + default: + $html_widget = $html->find('div.sidebar-widget', 6); + break; + } - // and now extracting app info from the selected widget (and yeah turns out icons are of heterogeneous sizes) + // and now extracting app info from the selected widget (and yeah turns out icons are of heterogeneous sizes) - foreach($html_widget->find('a') as $element) { - $item = array(); - $item['uri'] = self::URI . $element->href; - $item['title'] = $element->find('h4', 0)->plaintext; - $item['icon'] = $element->find('img', 0)->src; - $item['timestamp'] = $this->getTimestamp($item['icon']); - $item['summary'] = $element->find('span.package-summary', 0)->plaintext; - $item['content'] = ' + foreach ($html_widget->find('a') as $element) { + $item = []; + $item['uri'] = self::URI . $element->href; + $item['title'] = $element->find('h4', 0)->plaintext; + $item['icon'] = $element->find('img', 0)->src; + $item['timestamp'] = $this->getTimestamp($item['icon']); + $item['summary'] = $element->find('span.package-summary', 0)->plaintext; + $item['content'] = ' <a href="' . $item['uri'] . '"> <img alt="" style="max-height:128px" src="' . $item['icon'] . '"> </a><br>' . $item['summary']; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/FDroidRepoBridge.php b/bridges/FDroidRepoBridge.php index c26bbacf..74147310 100644 --- a/bridges/FDroidRepoBridge.php +++ b/bridges/FDroidRepoBridge.php @@ -1,150 +1,162 @@ <?php -class FDroidRepoBridge extends BridgeAbstract { - const NAME = 'F-Droid Repository Bridge'; - const URI = 'https://f-droid.org/'; - const DESCRIPTION = 'Query any F-Droid Repository for its latest updates.'; - - const ITEM_LIMIT = 50; - - const PARAMETERS = array( - 'global' => array( - 'url' => array( - 'name' => 'Repository URL', - 'title' => 'Usually ends with /repo/', - 'required' => true, - 'exampleValue' => 'https://srv.tt-rss.org/fdroid/repo' - ) - ), - 'Latest Updates' => array( - 'sorting' => array( - 'name' => 'Sort By', - 'type' => 'list', - 'values' => array( - 'Latest added apps' => 'added', - 'Latest updated apps' => 'lastUpdated' - ) - ), - 'locale' => array( - 'name' => 'Locale', - 'defaultValue' => 'en-US' - ) - ), - 'Follow Package' => array( - 'package' => array( - 'name' => 'Package Identifier', - 'required' => true, - 'exampleValue' => 'org.fox.ttrss' - ) - ) - ); - - // Stores repo information - private $repo; - - public function getURI() { - if (empty($this->queriedContext)) - return parent::getURI(); - - $url = rtrim($this->GetInput('url'), '/'); - return strstr($url, '?', true) ?: $url; - } - - public function getName() { - if (empty($this->queriedContext)) - return parent::getName(); - - $name = $this->repo['repo']['name']; - switch($this->queriedContext) { - case 'Latest Updates': - return $name; - case 'Follow Package': - return $this->getInput('package') . ' - ' . $name; - default: - returnServerError('Unimplemented Context (getName)'); - } - } - - public function collectData() { - $this->repo = $this->getRepo(); - switch($this->queriedContext) { - case 'Latest Updates': - $this->getAllUpdates(); - break; - case 'Follow Package': - $this->getPackage($this->getInput('package')); - break; - default: - returnServerError('Unimplemented Context (collectData)'); - } - } - - private function getRepo() { - $url = $this->getURI(); - - // Get repo information (only available as JAR) - $jar = getContents($url . '/index-v1.jar'); - $jar_loc = tempnam(sys_get_temp_dir(), ''); - file_put_contents($jar_loc, $jar); - - // JAR files are specially formatted ZIP files - $jar = new ZipArchive; - if ($jar->open($jar_loc) !== true) { - returnServerError('Failed to extract archive'); - } - - // Get file pointer to the relevant JSON inside - $fp = $jar->getStream('index-v1.json'); - if (!$fp) { - returnServerError('Failed to get file pointer'); - } - - $data = json_decode(stream_get_contents($fp), true); - fclose($fp); - $jar->close(); - return $data; - } - - private function getAllUpdates() { - $apps = $this->repo['apps']; - usort($apps, function($a, $b) { - return $b[$this->getInput('sorting')] <=> $a[$this->getInput('sorting')]; - }); - $apps = array_slice($apps, 0, self::ITEM_LIMIT); - foreach($apps as $app) { - $latest = reset($this->repo['packages'][$app['packageName']]); - - if (isset($app['localized'])) { - // Try provided locale, then en-US, then any - $lang = $app['localized']; - $lang = $lang[$this->getInput('locale')] ?? $lang['en-US'] ?? reset($lang); - } else - $lang = array(); - - $item = array(); - $item['uri'] = $this->getURI() . '/' . $latest['apkName']; - $item['title'] = $lang['name'] ?? $app['packageName']; - $item['title'] .= ' ' . $latest['versionName']; - $item['timestamp'] = date(DateTime::ISO8601, (int) ($app['lastUpdated'] / 1000)); - if (isset($app['authorName'])) - $item['author'] = $app['authorName']; - if (isset($app['categories'])) - $item['categories'] = $app['categories']; - - // Adding Content - $icon = $app['icon'] ?? ''; - if (!empty($icon)) { - $icon = $this->getURI() . '/icons-320/' . $icon; - $item['enclosures'] = array($icon); - $icon = '<img src="' . $icon . '">'; - } - $summary = $lang['summary'] ?? $app['summary'] ?? ''; - $description = markdownToHtml(trim($lang['description'] ?? $app['description'] ?? 'None')); - $whatsNew = markdownToHtml(trim($lang['whatsNew'] ?? 'None')); - $website = $this->link($lang['webSite'] ?? $app['webSite'] ?? $app['authorWebSite'] ?? null); - $source = $this->link($app['sourceCode'] ?? null); - $issueTracker = $this->link($app['issueTracker'] ?? null); - $license = $app['license'] ?? 'None'; - $item['content'] = <<<EOD + +class FDroidRepoBridge extends BridgeAbstract +{ + const NAME = 'F-Droid Repository Bridge'; + const URI = 'https://f-droid.org/'; + const DESCRIPTION = 'Query any F-Droid Repository for its latest updates.'; + + const ITEM_LIMIT = 50; + + const PARAMETERS = [ + 'global' => [ + 'url' => [ + 'name' => 'Repository URL', + 'title' => 'Usually ends with /repo/', + 'required' => true, + 'exampleValue' => 'https://srv.tt-rss.org/fdroid/repo' + ] + ], + 'Latest Updates' => [ + 'sorting' => [ + 'name' => 'Sort By', + 'type' => 'list', + 'values' => [ + 'Latest added apps' => 'added', + 'Latest updated apps' => 'lastUpdated' + ] + ], + 'locale' => [ + 'name' => 'Locale', + 'defaultValue' => 'en-US' + ] + ], + 'Follow Package' => [ + 'package' => [ + 'name' => 'Package Identifier', + 'required' => true, + 'exampleValue' => 'org.fox.ttrss' + ] + ] + ]; + + // Stores repo information + private $repo; + + public function getURI() + { + if (empty($this->queriedContext)) { + return parent::getURI(); + } + + $url = rtrim($this->GetInput('url'), '/'); + return strstr($url, '?', true) ?: $url; + } + + public function getName() + { + if (empty($this->queriedContext)) { + return parent::getName(); + } + + $name = $this->repo['repo']['name']; + switch ($this->queriedContext) { + case 'Latest Updates': + return $name; + case 'Follow Package': + return $this->getInput('package') . ' - ' . $name; + default: + returnServerError('Unimplemented Context (getName)'); + } + } + + public function collectData() + { + $this->repo = $this->getRepo(); + switch ($this->queriedContext) { + case 'Latest Updates': + $this->getAllUpdates(); + break; + case 'Follow Package': + $this->getPackage($this->getInput('package')); + break; + default: + returnServerError('Unimplemented Context (collectData)'); + } + } + + private function getRepo() + { + $url = $this->getURI(); + + // Get repo information (only available as JAR) + $jar = getContents($url . '/index-v1.jar'); + $jar_loc = tempnam(sys_get_temp_dir(), ''); + file_put_contents($jar_loc, $jar); + + // JAR files are specially formatted ZIP files + $jar = new ZipArchive(); + if ($jar->open($jar_loc) !== true) { + returnServerError('Failed to extract archive'); + } + + // Get file pointer to the relevant JSON inside + $fp = $jar->getStream('index-v1.json'); + if (!$fp) { + returnServerError('Failed to get file pointer'); + } + + $data = json_decode(stream_get_contents($fp), true); + fclose($fp); + $jar->close(); + return $data; + } + + private function getAllUpdates() + { + $apps = $this->repo['apps']; + usort($apps, function ($a, $b) { + return $b[$this->getInput('sorting')] <=> $a[$this->getInput('sorting')]; + }); + $apps = array_slice($apps, 0, self::ITEM_LIMIT); + foreach ($apps as $app) { + $latest = reset($this->repo['packages'][$app['packageName']]); + + if (isset($app['localized'])) { + // Try provided locale, then en-US, then any + $lang = $app['localized']; + $lang = $lang[$this->getInput('locale')] ?? $lang['en-US'] ?? reset($lang); + } else { + $lang = []; + } + + $item = []; + $item['uri'] = $this->getURI() . '/' . $latest['apkName']; + $item['title'] = $lang['name'] ?? $app['packageName']; + $item['title'] .= ' ' . $latest['versionName']; + $item['timestamp'] = date(DateTime::ISO8601, (int) ($app['lastUpdated'] / 1000)); + if (isset($app['authorName'])) { + $item['author'] = $app['authorName']; + } + if (isset($app['categories'])) { + $item['categories'] = $app['categories']; + } + + // Adding Content + $icon = $app['icon'] ?? ''; + if (!empty($icon)) { + $icon = $this->getURI() . '/icons-320/' . $icon; + $item['enclosures'] = [$icon]; + $icon = '<img src="' . $icon . '">'; + } + $summary = $lang['summary'] ?? $app['summary'] ?? ''; + $description = markdownToHtml(trim($lang['description'] ?? $app['description'] ?? 'None')); + $whatsNew = markdownToHtml(trim($lang['whatsNew'] ?? 'None')); + $website = $this->link($lang['webSite'] ?? $app['webSite'] ?? $app['authorWebSite'] ?? null); + $source = $this->link($app['sourceCode'] ?? null); + $issueTracker = $this->link($app['issueTracker'] ?? null); + $license = $app['license'] ?? 'None'; + $item['content'] = <<<EOD {$icon} <p>{$summary}</p> <h1>Description</h1> @@ -157,40 +169,44 @@ class FDroidRepoBridge extends BridgeAbstract { <p>Issue Tracker: {$issueTracker}</p> <p>license: {$app['license']}</p> EOD; - $this->items[] = $item; - } - } - - private function getPackage($package) { - if (!isset($this->repo['packages'][$package])) { - returnClientError('Invalid Package Name'); - } - $package = $this->repo['packages'][$package]; - - $count = self::ITEM_LIMIT; - foreach($package as $version) { - $item = array(); - $item['uri'] = $this->getURI() . '/' . $version['apkName']; - $item['title'] = $version['versionName']; - $item['timestamp'] = date(DateTime::ISO8601, (int) ($version['added'] / 1000)); - $item['uid'] = $version['versionCode']; - $size = round($version['size'] / 1048576, 1); // Bytes -> MB - $sdk_link = 'https://developer.android.com/studio/releases/platforms'; - $item['content'] = <<<EOD + $this->items[] = $item; + } + } + + private function getPackage($package) + { + if (!isset($this->repo['packages'][$package])) { + returnClientError('Invalid Package Name'); + } + $package = $this->repo['packages'][$package]; + + $count = self::ITEM_LIMIT; + foreach ($package as $version) { + $item = []; + $item['uri'] = $this->getURI() . '/' . $version['apkName']; + $item['title'] = $version['versionName']; + $item['timestamp'] = date(DateTime::ISO8601, (int) ($version['added'] / 1000)); + $item['uid'] = $version['versionCode']; + $size = round($version['size'] / 1048576, 1); // Bytes -> MB + $sdk_link = 'https://developer.android.com/studio/releases/platforms'; + $item['content'] = <<<EOD <p>size: {$size}MB</p> <p>Minimum SDK: {$version['minSdkVersion']} (<a href="{$sdk_link}">SDK to Android Version List</a>)</p> <p>hash ({$version['hashType']}): {$version['hash']}</p> EOD; - $this->items[] = $item; - if (--$count <= 0) - break; - } - } - - private function link($url) { - if (empty($url)) - return null; - return '<a href="' . $url . '">' . $url . '</a>'; - } + $this->items[] = $item; + if (--$count <= 0) { + break; + } + } + } + + private function link($url) + { + if (empty($url)) { + return null; + } + return '<a href="' . $url . '">' . $url . '</a>'; + } } diff --git a/bridges/FM4Bridge.php b/bridges/FM4Bridge.php index b477f4cc..45bccd52 100644 --- a/bridges/FM4Bridge.php +++ b/bridges/FM4Bridge.php @@ -2,64 +2,67 @@ class FM4Bridge extends BridgeAbstract { - const MAINTAINER = 'joni1993'; - const NAME = 'FM4 Bridge'; - const URI = 'https://fm4.orf.at'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Feed for FM4 articles by tags (authors)'; - const PARAMETERS = array( - array( - 'tag' => array( - 'name' => 'Tag (author, category, ...)', - 'title' => 'Tag to retrieve', - 'exampleValue' => 'musik' - ), - 'loadcontent' => array( - 'name' => 'Load Full Article Content', - 'title' => 'Retrieve full content of articles (may take longer)', - 'type' => 'checkbox' - ), - 'pages' => array( - 'name' => 'Pages', - 'title' => 'Amount of pages to load', - 'type' => 'number', - 'defaultValue' => 1 - ) - ) - ); + const MAINTAINER = 'joni1993'; + const NAME = 'FM4 Bridge'; + const URI = 'https://fm4.orf.at'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Feed for FM4 articles by tags (authors)'; + const PARAMETERS = [ + [ + 'tag' => [ + 'name' => 'Tag (author, category, ...)', + 'title' => 'Tag to retrieve', + 'exampleValue' => 'musik' + ], + 'loadcontent' => [ + 'name' => 'Load Full Article Content', + 'title' => 'Retrieve full content of articles (may take longer)', + 'type' => 'checkbox' + ], + 'pages' => [ + 'name' => 'Pages', + 'title' => 'Amount of pages to load', + 'type' => 'number', + 'defaultValue' => 1 + ] + ] + ]; - private function getPageData($tag, $page) { - if($tag) - $uri = self::URI . '/tags/' . $tag; - else - $uri = self::URI; + private function getPageData($tag, $page) + { + if ($tag) { + $uri = self::URI . '/tags/' . $tag; + } else { + $uri = self::URI; + } - $uri = $uri . '?page=' . $page; + $uri = $uri . '?page=' . $page; - $html = getSimpleHTMLDOM($uri); + $html = getSimpleHTMLDOM($uri); - $page_items = array(); + $page_items = []; - foreach ($html->find('div[class*=listItem]') as $article) { - $item = array(); + foreach ($html->find('div[class*=listItem]') as $article) { + $item = []; - $item['uri'] = $article->find('a', 0)->href; - $item['title'] = $article->find('h2', 0)->plaintext; - $item['author'] = $article->find('p[class*=keyword]', 0)->plaintext; - $item['timestamp'] = strtotime($article->find('p[class*=time]', 0)->plaintext); + $item['uri'] = $article->find('a', 0)->href; + $item['title'] = $article->find('h2', 0)->plaintext; + $item['author'] = $article->find('p[class*=keyword]', 0)->plaintext; + $item['timestamp'] = strtotime($article->find('p[class*=time]', 0)->plaintext); - if ($this->getInput('loadcontent')) { - $item['content'] = getSimpleHTMLDOM($item['uri'])->find('div[class=storyText]', 0); - } + if ($this->getInput('loadcontent')) { + $item['content'] = getSimpleHTMLDOM($item['uri'])->find('div[class=storyText]', 0); + } - $page_items[] = $item; - } - return $page_items; - } + $page_items[] = $item; + } + return $page_items; + } - public function collectData() { - for ($cur_page = 1; $cur_page <= $this->getInput('pages'); $cur_page++) { - $this->items = array_merge($this->items, $this->getPageData($this->getInput('tag'), $cur_page)); - } - } + public function collectData() + { + for ($cur_page = 1; $cur_page <= $this->getInput('pages'); $cur_page++) { + $this->items = array_merge($this->items, $this->getPageData($this->getInput('tag'), $cur_page)); + } + } } diff --git a/bridges/FSecureBlogBridge.php b/bridges/FSecureBlogBridge.php index 46ad8ac0..cc1c0683 100644 --- a/bridges/FSecureBlogBridge.php +++ b/bridges/FSecureBlogBridge.php @@ -1,120 +1,126 @@ <?php -class FSecureBlogBridge extends BridgeAbstract { - const NAME = 'F-Secure Blog'; - const URI = 'https://blog.f-secure.com'; - const DESCRIPTION = 'F-Secure Blog'; - const MAINTAINER = 'simon816'; - const PARAMETERS = array( - '' => array( - 'categories' => array( - 'name' => 'Blog categories', - 'exampleValue' => 'home-security', - ), - 'language' => array( - 'name' => 'Language', - 'required' => true, - 'defaultValue' => 'en', - ), - 'oldest_date' => array( - 'name' => 'Oldest article date', - 'exampleValue' => '-6 months', - ), - ) - ); +class FSecureBlogBridge extends BridgeAbstract +{ + const NAME = 'F-Secure Blog'; + const URI = 'https://blog.f-secure.com'; + const DESCRIPTION = 'F-Secure Blog'; + const MAINTAINER = 'simon816'; + const PARAMETERS = [ + '' => [ + 'categories' => [ + 'name' => 'Blog categories', + 'exampleValue' => 'home-security', + ], + 'language' => [ + 'name' => 'Language', + 'required' => true, + 'defaultValue' => 'en', + ], + 'oldest_date' => [ + 'name' => 'Oldest article date', + 'exampleValue' => '-6 months', + ], + ] + ]; - public function getURI() { - $lang = $this->getInput('language') or 'en'; - if ($lang === 'en') { - return self::URI; - } - return self::URI . "/$lang"; - } + public function getURI() + { + $lang = $this->getInput('language') or 'en'; + if ($lang === 'en') { + return self::URI; + } + return self::URI . "/$lang"; + } - public function collectData() { - $this->items = array(); - $this->seen = array(); + public function collectData() + { + $this->items = []; + $this->seen = []; - $this->oldest = strtotime($this->getInput('oldest_date')) ?: 0; + $this->oldest = strtotime($this->getInput('oldest_date')) ?: 0; - $categories = $this->getInput('categories'); - if (!empty($categories)) { - foreach (explode(',', $categories) as $cat) { - if (!empty($cat)) { - $this->collectCategory($cat); - } - } - return; - } + $categories = $this->getInput('categories'); + if (!empty($categories)) { + foreach (explode(',', $categories) as $cat) { + if (!empty($cat)) { + $this->collectCategory($cat); + } + } + return; + } - $html = getSimpleHTMLDOMCached($this->getURI() . '/'); + $html = getSimpleHTMLDOMCached($this->getURI() . '/'); - foreach ($html->find('ul.c-header-menu-desktop__list li a') as $link) { - $url = parse_url($link->href); - if (($pos = strpos($url['path'], '/category/')) !== false) { - $cat = substr($url['path'], $pos + strlen('/category/'), -1); - $this->collectCategory($cat); - } - } - } + foreach ($html->find('ul.c-header-menu-desktop__list li a') as $link) { + $url = parse_url($link->href); + if (($pos = strpos($url['path'], '/category/')) !== false) { + $cat = substr($url['path'], $pos + strlen('/category/'), -1); + $this->collectCategory($cat); + } + } + } - private function collectCategory($category) { - $url = $this->getURI() . "/category/$category/"; - while ($url) { - //Limit total amount of requests - if(count($this->items) >= 20) { - break; - } - $url = $this->collectListing($url); - } - } + private function collectCategory($category) + { + $url = $this->getURI() . "/category/$category/"; + while ($url) { + //Limit total amount of requests + if (count($this->items) >= 20) { + break; + } + $url = $this->collectListing($url); + } + } - // n.b. this relies on articles to be ordered by date so the cutoff works - private function collectListing($url) { - $html = getSimpleHTMLDOMCached($url, 60 * 60); - $items = $html->find('section.b-blog .l-blog__content__listing div.c-listing-item'); + // n.b. this relies on articles to be ordered by date so the cutoff works + private function collectListing($url) + { + $html = getSimpleHTMLDOMCached($url, 60 * 60); + $items = $html->find('section.b-blog .l-blog__content__listing div.c-listing-item'); - $catName = trim($html->find('section.b-blog .c-blog-header__title', 0)->plaintext); + $catName = trim($html->find('section.b-blog .c-blog-header__title', 0)->plaintext); - foreach ($items as $item) { - $url = $item->getAttribute('data-url'); - if (!$this->collectArticle($url)) { - return null; // Too old, stop collecting - } - } + foreach ($items as $item) { + $url = $item->getAttribute('data-url'); + if (!$this->collectArticle($url)) { + return null; // Too old, stop collecting + } + } - // Point's to 404 for non-english blog - // $next = $html->find('link[rel=next]', 0); - $next = $html->find('ul.page-numbers a.next', 0); - return $next ? $next->href : null; - } + // Point's to 404 for non-english blog + // $next = $html->find('link[rel=next]', 0); + $next = $html->find('ul.page-numbers a.next', 0); + return $next ? $next->href : null; + } - // Returns a boolean whether to continue collecting articles - // i.e. date is after oldest cutoff - private function collectArticle($url) { - if (array_key_exists($url, $this->seen)) { - return true; - } - $html = getSimpleHTMLDOMCached($url); + // Returns a boolean whether to continue collecting articles + // i.e. date is after oldest cutoff + private function collectArticle($url) + { + if (array_key_exists($url, $this->seen)) { + return true; + } + $html = getSimpleHTMLDOMCached($url); - $rssItem = array( 'uri' => $url, 'uid' => $url ); - $rssItem['title'] = $html->find('meta[property=og:title]', 0)->content; - $dt = $html->find('meta[property=article:published_time]', 0)->content; - // Exit if too old - if (strtotime($dt) < $this->oldest) { - return false; - } - $rssItem['timestamp'] = $dt; - $img = $html->find('meta[property=og:image]', 0); - $rssItem['enclosures'] = $img ? array($img->content) : array(); - $rssItem['author'] = trim($html->find('.c-blog-author__text a', 0)->plaintext); - $rssItem['categories'] = array_map(function ($link) { - return trim($link->plaintext); - }, $html->find('.b-single-header__categories .c-category-list a')); - $rssItem['content'] = trim($html->find('article', 0)->innertext); + $rssItem = [ 'uri' => $url, 'uid' => $url ]; + $rssItem['title'] = $html->find('meta[property=og:title]', 0)->content; + $dt = $html->find('meta[property=article:published_time]', 0)->content; + // Exit if too old + if (strtotime($dt) < $this->oldest) { + return false; + } + $rssItem['timestamp'] = $dt; + $img = $html->find('meta[property=og:image]', 0); + $rssItem['enclosures'] = $img ? [$img->content] : []; + $rssItem['author'] = trim($html->find('.c-blog-author__text a', 0)->plaintext); + $rssItem['categories'] = array_map(function ($link) { + return trim($link->plaintext); + }, $html->find('.b-single-header__categories .c-category-list a')); + $rssItem['content'] = trim($html->find('article', 0)->innertext); - $this->items[] = $rssItem; - $this->seen[$url] = 1; - return true; - } + $this->items[] = $rssItem; + $this->seen[$url] = 1; + return true; + } } diff --git a/bridges/FabriceBellardBridge.php b/bridges/FabriceBellardBridge.php index 0085e924..5e012665 100644 --- a/bridges/FabriceBellardBridge.php +++ b/bridges/FabriceBellardBridge.php @@ -1,35 +1,38 @@ <?php -class FabriceBellardBridge extends BridgeAbstract { - const NAME = 'Fabrice Bellard'; - const URI = 'https://bellard.org/'; - const DESCRIPTION = "Fabrice Bellard's Home Page"; - const MAINTAINER = 'somini'; - - public function collectData() { - $html = getSimpleHTMLDOM(self::URI); - - foreach ($html->find('p') as $obj) { - $item = array(); - - $html = defaultLinkTo($html, $this->getURI()); - - $links = $obj->find('a'); - if (count($links) > 0) { - $link_uri = $links[0]->href; - } else { - $link_uri = $this->getURI(); - } - - /* try to make sure the link is valid */ - if ($link_uri[-1] !== '/' && strpos($link_uri, '/') === false) { - $link_uri = $link_uri . '/'; - } - - $item['title'] = strip_tags($obj->innertext); - $item['uri'] = $link_uri; - $item['content'] = $obj->innertext; - - $this->items[] = $item; - } - } + +class FabriceBellardBridge extends BridgeAbstract +{ + const NAME = 'Fabrice Bellard'; + const URI = 'https://bellard.org/'; + const DESCRIPTION = "Fabrice Bellard's Home Page"; + const MAINTAINER = 'somini'; + + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); + + foreach ($html->find('p') as $obj) { + $item = []; + + $html = defaultLinkTo($html, $this->getURI()); + + $links = $obj->find('a'); + if (count($links) > 0) { + $link_uri = $links[0]->href; + } else { + $link_uri = $this->getURI(); + } + + /* try to make sure the link is valid */ + if ($link_uri[-1] !== '/' && strpos($link_uri, '/') === false) { + $link_uri = $link_uri . '/'; + } + + $item['title'] = strip_tags($obj->innertext); + $item['uri'] = $link_uri; + $item['content'] = $obj->innertext; + + $this->items[] = $item; + } + } } diff --git a/bridges/FacebookBridge.php b/bridges/FacebookBridge.php index e5cc6c34..99fa346f 100644 --- a/bridges/FacebookBridge.php +++ b/bridges/FacebookBridge.php @@ -1,500 +1,494 @@ <?php -class FacebookBridge extends BridgeAbstract { - // const MAINTAINER = 'teromene, logmanoriginal'; - const NAME = 'Facebook Bridge | Main Site'; - const URI = 'https://www.facebook.com/'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Input a page title or a profile log. For a profile log, +class FacebookBridge extends BridgeAbstract +{ + // const MAINTAINER = 'teromene, logmanoriginal'; + const NAME = 'Facebook Bridge | Main Site'; + const URI = 'https://www.facebook.com/'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Input a page title or a profile log. For a profile log, please insert the parameter as follow : myExamplePage/132621766841117'; - const PARAMETERS = array( - 'User' => array( - 'u' => array( - 'name' => 'Username', - 'required' => true - ), - 'media_type' => array( - 'name' => 'Media type', - 'type' => 'list', - 'required' => false, - 'values' => array( - 'All' => 'all', - 'Video' => 'video', - 'No Video' => 'novideo' - ), - 'defaultValue' => 'all' - ), - 'skip_reviews' => array( - 'name' => 'Skip reviews', - 'type' => 'checkbox', - 'required' => false, - 'defaultValue' => false, - 'title' => 'Feed includes reviews when unchecked' - ) - ), - 'Group' => array( - 'g' => array( - 'name' => 'Group', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'https://www.facebook.com/groups/743149642484225', - 'title' => 'Insert group name or facebook group URL' - ) - ), - 'global' => array( - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Specify the number of items to return (default: -1)', - 'defaultValue' => -1 - ) - ) - ); - - private $authorName = ''; - private $groupName = ''; - - public function getIcon() { - return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico'; - } - - public function getName(){ - - switch($this->queriedContext) { - - case 'User': - if(!empty($this->authorName)) { - return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName; - } - break; - - case 'Group': - if(!empty($this->groupName)) { - return $this->groupName; - } - break; - - } - - return parent::getName(); - } - - public function detectParameters($url){ - $params = array(); - - // By profile - $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/profile\.php\?id\=([^\/?&\n]+)?(.*)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['u'] = urldecode($matches[3]); - return $params; - } - - // By group - $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/groups\/([^\/?\n]+)?(.*)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['g'] = urldecode($matches[3]); - return $params; - } - - // By username - $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/([^\/?\n]+)/'; - - if(preg_match($regex, $url, $matches) > 0) { - $params['u'] = urldecode($matches[3]); - return $params; - } - - return null; - } - - public function getURI() { - $uri = self::URI; - - switch($this->queriedContext) { - - case 'Group': - // Discover groups via https://www.facebook.com/groups/ - // Example group: https://www.facebook.com/groups/sailors.worldwide - $uri .= 'groups/' . $this->sanitizeGroup(filter_var($this->getInput('g'), FILTER_SANITIZE_URL)); - break; - - case 'User': - // Example user 1: https://www.facebook.com/artetv/ - // Example user 2: artetv - $user = $this->sanitizeUser($this->getInput('u')); - - if(!strpos($user, '/')) { - $uri .= urlencode($user) . '/posts'; - } else { - $uri .= 'pages/' . $user; - } - - break; - - } - - // Request the mobile version to reduce page size (no javascript) - // More information: https://stackoverflow.com/a/11103592 - return $uri .= '?_fb_noscript=1'; - } - - public function collectData() { - - switch($this->queriedContext) { - - case 'Group': - $this->collectGroupData(); - break; - - case 'User': - $this->collectUserData(); - break; - - default: - returnClientError('Unknown context: "' . $this->queriedContext . '"!'); - - } - - $limit = $this->getInput('limit') ?: -1; - - if($limit > 0 && count($this->items) > $limit) { - $this->items = array_slice($this->items, 0, $limit); - } - - } - - #region Group - - private function collectGroupData() { - - if(getEnv('HTTP_ACCEPT_LANGUAGE')) { - $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')); - } else { - $header = array(); - } - - $touchURI = str_replace( - 'https://www.facebook', - 'https://touch.facebook', - $this->getURI() - ); - - $html = getSimpleHTMLDOM($touchURI, $header); - - if(!$this->isPublicGroup($html)) { - returnClientError('This group is not public! RSS-Bridge only supports public groups!'); - } - - defaultLinkTo($html, substr(self::URI, 0, strlen(self::URI) - 1)); - - $this->groupName = $this->extractGroupName($html); - - $posts = $html->find('div.story_body_container') - or returnServerError('Failed finding posts!'); - - foreach($posts as $post) { - - $item = array(); - - $item['uri'] = $this->extractGroupPostURI($post); - $item['title'] = $this->extractGroupPostTitle($post); - $item['author'] = $this->extractGroupPostAuthor($post); - $item['content'] = $this->extractGroupPostContent($post); - $item['enclosures'] = $this->extractGroupPostEnclosures($post); - - $this->items[] = $item; - - } - - } - - private function sanitizeGroup($group) { - - if(filter_var( - $group, - FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) { - // User provided a URL - - $urlparts = parse_url($group); - - $this->validateHost($urlparts['host']); - - return explode('/', $urlparts['path'])[2]; - - } elseif(strpos($group, '/') !== false) { - returnClientError('The group you provided is invalid: ' . $group); - } else { - return $group; - } - - } - - private function validateHost($provided_host) { - // Handle mobile links - if (strpos($provided_host, 'm.') === 0) { - $provided_host = substr($provided_host, strlen('m.')); - } - if (strpos($provided_host, 'touch.') === 0) { - $provided_host = substr($provided_host, strlen('touch.')); - } - - $facebook_host = parse_url(self::URI)['host']; - - if ($provided_host !== $facebook_host - && 'www.' . $provided_host !== $facebook_host) { - returnClientError('The host you provided is invalid! Received "' - . $provided_host - . '", expected "' - . $facebook_host - . '"!'); - } - } - - /** - * @param $html simple_html_dom - * @return bool - */ - private function isPublicGroup($html) { - - // Facebook touch just presents a login page for non-public groups - $title = $html->find('title', 0); - return $title->plaintext !== 'Log in to Facebook | Facebook'; - } - - private function extractGroupName($html) { - - $ogtitle = $html->find('._de1', 0) - or returnServerError('Unable to find group title!'); - - return html_entity_decode($ogtitle->plaintext, ENT_QUOTES); - } - - private function extractGroupPostURI($post) { - - $elements = $post->find('a') - or returnServerError('Unable to find URI!'); - - foreach($elements as $anchor) { - - // Find the one that is a permalink - if(strpos($anchor->href, 'permalink') !== false) { - $arr = explode('?', $anchor->href, 2); - return $arr[0]; - } - - } - - return null; - - } - - private function extractGroupPostContent($post) { - - $content = $post->find('div._5rgt', 0) - or returnServerError('Unable to find user content!'); - - $context_text = $content->innertext; - if ($content->next_sibling() !== null) { - $context_text .= $content->next_sibling()->innertext; - } - return $context_text; - - } - - private function extractGroupPostAuthor($post) { - - $element = $post->find('h3 a', 0) - or returnServerError('Unable to find author information!'); - - return $element->plaintext; - - } - - private function extractGroupPostEnclosures($post) { - - $elements = $post->find('span._6qdm'); - if ($post->find('div._5rgt', 0)->next_sibling() !== null) { - array_push($elements, ...$post->find('div._5rgt', 0)->next_sibling()->find('i.img')); - } - - $enclosures = array(); - - $background_img_regex = '/background-image: ?url\\((.+?)\\);/'; - - foreach($elements as $enclosure) { - if(preg_match($background_img_regex, $enclosure, $matches) > 0) { - $bg_img_value = trim(html_entity_decode($matches[1], ENT_QUOTES), "'\""); - $bg_img_url = urldecode(preg_replace('/\\\([0-9a-z]{2}) /', '%$1', $bg_img_value)); - $enclosures[] = urldecode($bg_img_url); - } - } - - return empty($enclosures) ? null : $enclosures; - - } - - private function extractGroupPostTitle($post) { - - $element = $post->find('h3', 0) - or returnServerError('Unable to find title!'); - - if(strpos($element->plaintext, 'shared') === false) { - - $content = strip_tags($this->extractGroupPostContent($post)); - - return $this->extractGroupPostAuthor($post) - . ' posted: ' - . substr( - $content, - 0, - strpos(wordwrap($content, 64), "\n") - ) - . '...'; - - } - - return $element->plaintext; - - } - - #endregion (Group) - - #region User - - /** - * Checks if $user is a valid username or URI and returns the username - */ - private function sanitizeUser($user) { - if (filter_var($user, FILTER_VALIDATE_URL)) { - - $urlparts = parse_url($user); - - $this->validateHost($urlparts['host']); - - if(!array_key_exists('path', $urlparts) - || $urlparts['path'] === '/') { - returnClientError('The URL you provided doesn\'t contain the user name!'); - } - - return explode('/', $urlparts['path'])[1]; - - } else { - - // First character cannot be a forward slash - if(strpos($user, '/') === 0) { - returnClientError('Remove leading slash "/" from the username!'); - } - - return $user; - - } - } - - /** - * Bypass external link redirection - */ - private function unescapeFacebookLink($content){ - return preg_replace_callback('/ href=\"([^"]+)\"/i', function($matches){ - if(is_array($matches) && count($matches) > 1) { - - $link = $matches[1]; - - if(strpos($link, 'facebook.com/l.php?u=') !== false) - $link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&')); - - return ' href="' . $link . '"'; - - } - }, $content); - } - - /** - * Remove Facebook's tracking code - */ - private function removeTrackingCodes($content){ - return preg_replace_callback('/ href=\"([^"]+)\"/i', function($matches){ - if(is_array($matches) && count($matches) > 1) { - - $link = $matches[1]; - - if(strpos($link, 'facebook.com') !== false) { - if(strpos($link, '?') !== false) { - $link = substr($link, 0, strpos($link, '?')); - } - } - return ' href="' . $link . '"'; - - } - }, $content); - } - - /** - * Convert textual representation of emoticons back to ASCII emoticons. - * i.e. "<i><u>smile emoticon</u></i>" => ":)" - */ - private function unescapeFacebookEmote($content){ - return preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', function($matches){ - static $facebook_emoticons = array( - 'smile' => ':)', - 'frown' => ':(', - 'tongue' => ':P', - 'grin' => ':D', - 'gasp' => ':O', - 'wink' => ';)', - 'pacman' => ':<', - 'grumpy' => '>_<', - 'unsure' => ':/', - 'cry' => ':\'(', - 'kiki' => '^_^', - 'glasses' => '8-)', - 'sunglasses' => 'B-)', - 'heart' => '<3', - 'devil' => ']:D', - 'angel' => '0:)', - 'squint' => '-_-', - 'confused' => 'o_O', - 'upset' => 'xD', - 'colonthree' => ':3', - 'like' => '👍'); - - $len = count($matches); - - if ($len > 1) - for ($i = 1; $i < $len; $i++) - foreach ($facebook_emoticons as $name => $emote) - if ($matches[$i] === $name) - return $emote; - - return $matches[0]; - }, $content); - } - - /** - * Returns the captcha message for the given captcha - */ - private function returnCaptchaMessage($captcha) { - // Save form for submitting after getting captcha response - if (session_status() == PHP_SESSION_NONE) { - session_start(); - } - - $captcha_fields = array(); - - foreach ($captcha->find('input, button') as $input) { - $captcha_fields[$input->name] = $input->value; - } - - $_SESSION['captcha_fields'] = $captcha_fields; - $_SESSION['captcha_action'] = $captcha->find('form', 0)->action; - - // Show captcha filling form to the viewer, proxying the captcha image - $img = base64_encode(getContents($captcha->find('img', 0)->src)); - - header('Content-Type: text/html', true, 500); - - $message = <<<EOD + const PARAMETERS = [ + 'User' => [ + 'u' => [ + 'name' => 'Username', + 'required' => true + ], + 'media_type' => [ + 'name' => 'Media type', + 'type' => 'list', + 'required' => false, + 'values' => [ + 'All' => 'all', + 'Video' => 'video', + 'No Video' => 'novideo' + ], + 'defaultValue' => 'all' + ], + 'skip_reviews' => [ + 'name' => 'Skip reviews', + 'type' => 'checkbox', + 'required' => false, + 'defaultValue' => false, + 'title' => 'Feed includes reviews when unchecked' + ] + ], + 'Group' => [ + 'g' => [ + 'name' => 'Group', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'https://www.facebook.com/groups/743149642484225', + 'title' => 'Insert group name or facebook group URL' + ] + ], + 'global' => [ + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Specify the number of items to return (default: -1)', + 'defaultValue' => -1 + ] + ] + ]; + + private $authorName = ''; + private $groupName = ''; + + public function getIcon() + { + return 'https://static.xx.fbcdn.net/rsrc.php/yo/r/iRmz9lCMBD2.ico'; + } + + public function getName() + { + switch ($this->queriedContext) { + case 'User': + if (!empty($this->authorName)) { + return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName; + } + break; + + case 'Group': + if (!empty($this->groupName)) { + return $this->groupName; + } + break; + } + + return parent::getName(); + } + + public function detectParameters($url) + { + $params = []; + + // By profile + $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/profile\.php\?id\=([^\/?&\n]+)?(.*)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['u'] = urldecode($matches[3]); + return $params; + } + + // By group + $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/groups\/([^\/?\n]+)?(.*)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['g'] = urldecode($matches[3]); + return $params; + } + + // By username + $regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/([^\/?\n]+)/'; + + if (preg_match($regex, $url, $matches) > 0) { + $params['u'] = urldecode($matches[3]); + return $params; + } + + return null; + } + + public function getURI() + { + $uri = self::URI; + + switch ($this->queriedContext) { + case 'Group': + // Discover groups via https://www.facebook.com/groups/ + // Example group: https://www.facebook.com/groups/sailors.worldwide + $uri .= 'groups/' . $this->sanitizeGroup(filter_var($this->getInput('g'), FILTER_SANITIZE_URL)); + break; + + case 'User': + // Example user 1: https://www.facebook.com/artetv/ + // Example user 2: artetv + $user = $this->sanitizeUser($this->getInput('u')); + + if (!strpos($user, '/')) { + $uri .= urlencode($user) . '/posts'; + } else { + $uri .= 'pages/' . $user; + } + + break; + } + + // Request the mobile version to reduce page size (no javascript) + // More information: https://stackoverflow.com/a/11103592 + return $uri .= '?_fb_noscript=1'; + } + + public function collectData() + { + switch ($this->queriedContext) { + case 'Group': + $this->collectGroupData(); + break; + + case 'User': + $this->collectUserData(); + break; + + default: + returnClientError('Unknown context: "' . $this->queriedContext . '"!'); + } + + $limit = $this->getInput('limit') ?: -1; + + if ($limit > 0 && count($this->items) > $limit) { + $this->items = array_slice($this->items, 0, $limit); + } + } + + #region Group + + private function collectGroupData() + { + if (getEnv('HTTP_ACCEPT_LANGUAGE')) { + $header = ['Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')]; + } else { + $header = []; + } + + $touchURI = str_replace( + 'https://www.facebook', + 'https://touch.facebook', + $this->getURI() + ); + + $html = getSimpleHTMLDOM($touchURI, $header); + + if (!$this->isPublicGroup($html)) { + returnClientError('This group is not public! RSS-Bridge only supports public groups!'); + } + + defaultLinkTo($html, substr(self::URI, 0, strlen(self::URI) - 1)); + + $this->groupName = $this->extractGroupName($html); + + $posts = $html->find('div.story_body_container') + or returnServerError('Failed finding posts!'); + + foreach ($posts as $post) { + $item = []; + + $item['uri'] = $this->extractGroupPostURI($post); + $item['title'] = $this->extractGroupPostTitle($post); + $item['author'] = $this->extractGroupPostAuthor($post); + $item['content'] = $this->extractGroupPostContent($post); + $item['enclosures'] = $this->extractGroupPostEnclosures($post); + + $this->items[] = $item; + } + } + + private function sanitizeGroup($group) + { + if ( + filter_var( + $group, + FILTER_VALIDATE_URL, + FILTER_FLAG_PATH_REQUIRED + ) + ) { + // User provided a URL + + $urlparts = parse_url($group); + + $this->validateHost($urlparts['host']); + + return explode('/', $urlparts['path'])[2]; + } elseif (strpos($group, '/') !== false) { + returnClientError('The group you provided is invalid: ' . $group); + } else { + return $group; + } + } + + private function validateHost($provided_host) + { + // Handle mobile links + if (strpos($provided_host, 'm.') === 0) { + $provided_host = substr($provided_host, strlen('m.')); + } + if (strpos($provided_host, 'touch.') === 0) { + $provided_host = substr($provided_host, strlen('touch.')); + } + + $facebook_host = parse_url(self::URI)['host']; + + if ( + $provided_host !== $facebook_host + && 'www.' . $provided_host !== $facebook_host + ) { + returnClientError('The host you provided is invalid! Received "' + . $provided_host + . '", expected "' + . $facebook_host + . '"!'); + } + } + + /** + * @param $html simple_html_dom + * @return bool + */ + private function isPublicGroup($html) + { + // Facebook touch just presents a login page for non-public groups + $title = $html->find('title', 0); + return $title->plaintext !== 'Log in to Facebook | Facebook'; + } + + private function extractGroupName($html) + { + $ogtitle = $html->find('._de1', 0) + or returnServerError('Unable to find group title!'); + + return html_entity_decode($ogtitle->plaintext, ENT_QUOTES); + } + + private function extractGroupPostURI($post) + { + $elements = $post->find('a') + or returnServerError('Unable to find URI!'); + + foreach ($elements as $anchor) { + // Find the one that is a permalink + if (strpos($anchor->href, 'permalink') !== false) { + $arr = explode('?', $anchor->href, 2); + return $arr[0]; + } + } + + return null; + } + + private function extractGroupPostContent($post) + { + $content = $post->find('div._5rgt', 0) + or returnServerError('Unable to find user content!'); + + $context_text = $content->innertext; + if ($content->next_sibling() !== null) { + $context_text .= $content->next_sibling()->innertext; + } + return $context_text; + } + + private function extractGroupPostAuthor($post) + { + $element = $post->find('h3 a', 0) + or returnServerError('Unable to find author information!'); + + return $element->plaintext; + } + + private function extractGroupPostEnclosures($post) + { + $elements = $post->find('span._6qdm'); + if ($post->find('div._5rgt', 0)->next_sibling() !== null) { + array_push($elements, ...$post->find('div._5rgt', 0)->next_sibling()->find('i.img')); + } + + $enclosures = []; + + $background_img_regex = '/background-image: ?url\\((.+?)\\);/'; + + foreach ($elements as $enclosure) { + if (preg_match($background_img_regex, $enclosure, $matches) > 0) { + $bg_img_value = trim(html_entity_decode($matches[1], ENT_QUOTES), "'\""); + $bg_img_url = urldecode(preg_replace('/\\\([0-9a-z]{2}) /', '%$1', $bg_img_value)); + $enclosures[] = urldecode($bg_img_url); + } + } + + return empty($enclosures) ? null : $enclosures; + } + + private function extractGroupPostTitle($post) + { + $element = $post->find('h3', 0) + or returnServerError('Unable to find title!'); + + if (strpos($element->plaintext, 'shared') === false) { + $content = strip_tags($this->extractGroupPostContent($post)); + + return $this->extractGroupPostAuthor($post) + . ' posted: ' + . substr( + $content, + 0, + strpos(wordwrap($content, 64), "\n") + ) + . '...'; + } + + return $element->plaintext; + } + + #endregion (Group) + + #region User + + /** + * Checks if $user is a valid username or URI and returns the username + */ + private function sanitizeUser($user) + { + if (filter_var($user, FILTER_VALIDATE_URL)) { + $urlparts = parse_url($user); + + $this->validateHost($urlparts['host']); + + if ( + !array_key_exists('path', $urlparts) + || $urlparts['path'] === '/' + ) { + returnClientError('The URL you provided doesn\'t contain the user name!'); + } + + return explode('/', $urlparts['path'])[1]; + } else { + // First character cannot be a forward slash + if (strpos($user, '/') === 0) { + returnClientError('Remove leading slash "/" from the username!'); + } + + return $user; + } + } + + /** + * Bypass external link redirection + */ + private function unescapeFacebookLink($content) + { + return preg_replace_callback('/ href=\"([^"]+)\"/i', function ($matches) { + if (is_array($matches) && count($matches) > 1) { + $link = $matches[1]; + + if (strpos($link, 'facebook.com/l.php?u=') !== false) { + $link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&')); + } + + return ' href="' . $link . '"'; + } + }, $content); + } + + /** + * Remove Facebook's tracking code + */ + private function removeTrackingCodes($content) + { + return preg_replace_callback('/ href=\"([^"]+)\"/i', function ($matches) { + if (is_array($matches) && count($matches) > 1) { + $link = $matches[1]; + + if (strpos($link, 'facebook.com') !== false) { + if (strpos($link, '?') !== false) { + $link = substr($link, 0, strpos($link, '?')); + } + } + return ' href="' . $link . '"'; + } + }, $content); + } + + /** + * Convert textual representation of emoticons back to ASCII emoticons. + * i.e. "<i><u>smile emoticon</u></i>" => ":)" + */ + private function unescapeFacebookEmote($content) + { + return preg_replace_callback('/<i><u>([^ <>]+) ([^<>]+)<\/u><\/i>/i', function ($matches) { + static $facebook_emoticons = [ + 'smile' => ':)', + 'frown' => ':(', + 'tongue' => ':P', + 'grin' => ':D', + 'gasp' => ':O', + 'wink' => ';)', + 'pacman' => ':<', + 'grumpy' => '>_<', + 'unsure' => ':/', + 'cry' => ':\'(', + 'kiki' => '^_^', + 'glasses' => '8-)', + 'sunglasses' => 'B-)', + 'heart' => '<3', + 'devil' => ']:D', + 'angel' => '0:)', + 'squint' => '-_-', + 'confused' => 'o_O', + 'upset' => 'xD', + 'colonthree' => ':3', + 'like' => '👍']; + + $len = count($matches); + + if ($len > 1) { + for ($i = 1; $i < $len; $i++) { + foreach ($facebook_emoticons as $name => $emote) { + if ($matches[$i] === $name) { + return $emote; + } + } + } + } + + return $matches[0]; + }, $content); + } + + /** + * Returns the captcha message for the given captcha + */ + private function returnCaptchaMessage($captcha) + { + // Save form for submitting after getting captcha response + if (session_status() == PHP_SESSION_NONE) { + session_start(); + } + + $captcha_fields = []; + + foreach ($captcha->find('input, button') as $input) { + $captcha_fields[$input->name] = $input->value; + } + + $_SESSION['captcha_fields'] = $captcha_fields; + $_SESSION['captcha_action'] = $captcha->find('form', 0)->action; + + // Show captcha filling form to the viewer, proxying the captcha image + $img = base64_encode(getContents($captcha->find('img', 0)->src)); + + header('Content-Type: text/html', true, 500); + + $message = <<<EOD <form method="post" action="?{$_SERVER['QUERY_STRING']}"> <h2>Facebook captcha challenge</h2> <p>Unfortunately, rss-bridge cannot fetch the requested page.<br /> @@ -505,246 +499,257 @@ Facebook wants rss-bridge to resolve the following captcha:</p> </form> EOD; - die($message); - } - - /** - * Checks if a capture response was received and tries to load the contents - * @return mixed null if no capture response was received, simplhtmldom document otherwise - */ - private function handleCaptchaResponse() { - if (isset($_POST['captcha_response'])) { - if (session_status() == PHP_SESSION_NONE) - session_start(); - - if (isset($_SESSION['captcha_fields'], $_SESSION['captcha_action'])) { - $captcha_action = $_SESSION['captcha_action']; - $captcha_fields = $_SESSION['captcha_fields']; - $captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']); - - $header = array( - 'Content-type: application/x-www-form-urlencoded', - 'Referer: ' . $captcha_action, - 'Cookie: noscript=1' - ); - - $opts = array( - CURLOPT_POST => 1, - CURLOPT_POSTFIELDS => http_build_query($captcha_fields) - ); - - $html = getSimpleHTMLDOM($captcha_action, $header, $opts); - - return $html; - } - - unset($_SESSION['captcha_fields']); - unset($_SESSION['captcha_action']); - } - - return null; - } - - private function collectUserData(){ - - $html = $this->handleCaptchaResponse(); - - // Retrieve page contents - if(is_null($html)) { - - if(getEnv('HTTP_ACCEPT_LANGUAGE')) { - $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')); - } else { - $header = array(); - } - - $html = getSimpleHTMLDOM($this->getURI(), $header); - - } - - // Handle captcha form? - $captcha = $html->find('div.captcha_interstitial', 0); - - if (!is_null($captcha)) { - $this->returnCaptchaMessage($captcha); - } - - // No captcha? We can carry on retrieving page contents :) - // First, we check whether the page is public or not - $loginForm = $html->find('._585r', 0); - - if($loginForm != null) { - returnServerError('You must be logged in to view this page. This is not supported by RSS-Bridge.'); - } - - $element = $html - ->find('#pagelet_timeline_main_column')[0] - ->children(0) - ->children(0) - ->next_sibling() - ->children(0); - - if(isset($element)) { - - $author = str_replace(' - Posts | Facebook', '', $html->find('title#pageTitle', 0)->innertext); - - $profilePic = $html->find('meta[property="og:image"]', 0)->content; - - $this->authorName = $author; - - foreach($element->children() as $cell) { - // Manage summary posts - if(strpos($cell->class, '_3xaf') !== false) { - $posts = $cell->children(); - } else { - $posts = array($cell); - } - - // Optionally skip reviews - if($this->getInput('skip_reviews') - && !is_null($cell->find('#review_composer_container', 0))) { - continue; - } - - foreach($posts as $post) { - // Check media type - switch($this->getInput('media_type')) { - case 'all': break; - case 'video': - if(empty($post->find('[aria-label=Video]'))) continue 2; - break; - case 'novideo': - if(!empty($post->find('[aria-label=Video]'))) continue 2; - break; - default: break; - } - - $item = array(); - - if(count($post->find('abbr')) > 0) { - - $content = $post->find('.userContentWrapper', 0); - - // This array specifies filters applied to all posts in order of appearance - $content_filters = array( - '._5mly', // Remove embedded videos (the preview image remains) - '._2ezg', // Remove "Views ..." - '.hidden_elem', // Remove hidden elements (they are hidden anyway) - '.timestampContent', // Remove relative timestamp - '._6spk', // Remove redundant separator - ); - - foreach($content_filters as $filter) { - foreach($content->find($filter) as $subject) { - $subject->outertext = ''; - } - } - - // Change origin tag for embedded media from div to paragraph - foreach($content->find('._59tj') as $subject) { - $subject->outertext = '<p>' . $subject->innertext . '</p>'; - } - - // Change title tag for embedded media from anchor to paragraph - foreach($content->find('._3n1k a') as $anchor) { - $anchor->outertext = '<p>' . $anchor->innertext . '</p>'; - } - - $content = preg_replace( - '/(?i)><div class=\"_3dp([^>]+)>(.+?)div\ class=\"[^u]+userContent\"/i', - '', - $content); - - $content = preg_replace( - '/(?i)><div class=\"_4l5([^>]+)>(.+?)<\/div>/i', - '', - $content); - - // Remove "SpSonsSoriSsés" - $content = preg_replace( - '/(?iU)<a [^>]+ href="#" role="link" [^>}]+>.+<\/a>/iU', - '', - $content); - - // Remove html nodes, keep only img, links, basic formatting - $content = strip_tags($content, '<a><img><i><u><br><p>'); - - $content = $this->unescapeFacebookLink($content); - - // Clean useless html tag properties and fix link closing tags - foreach (array( - 'onmouseover', - 'onclick', - 'target', - 'ajaxify', - 'tabindex', - 'class', - 'style', - 'data-[^=]*', - 'aria-[^=]*', - 'role', - 'rel', - 'id') as $property_name) { - $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content); - } - - $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content); - - $this->unescapeFacebookEmote($content); - - // Restore links in the post before further parsing - $post = defaultLinkTo($post, self::URI); - - // Restore links in the content before adding to the item - $content = defaultLinkTo($content, self::URI); - - $content = $this->removeTrackingCodes($content); - - // Retrieve date of the post - $date = $post->find('abbr')[0]; - - if(isset($date) && $date->hasAttribute('data-utime')) { - $date = $date->getAttribute('data-utime'); - } else { - $date = 0; - } - - // Build title from content - $title = strip_tags($post->find('.userContent', 0)->innertext); - if(strlen($title) > 64) - $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...'; - - $uri = $post->find('abbr')[0]->parent()->getAttribute('href'); - - // Extract fbid and patch link - if (strpos($uri, '?') !== false) { - $query = substr($uri, strpos($uri, '?') + 1); - parse_str($query, $query_params); - if (isset($query_params['story_fbid'])) { - $uri = self::URI . $query_params['story_fbid']; - } else { - $uri = substr($uri, 0, strpos($uri, '?')); - } - } - - //Build and add final item - $item['uri'] = htmlspecialchars_decode($uri, ENT_QUOTES); - $item['content'] = htmlspecialchars_decode($content, ENT_QUOTES); - $item['title'] = htmlspecialchars_decode($title, ENT_QUOTES); - $item['author'] = htmlspecialchars_decode($author, ENT_QUOTES); - $item['timestamp'] = $date; - - if(strpos($item['content'], '<img') === false) { - $item['enclosures'] = array($profilePic); - } - - $this->items[] = $item; - } - } - } - } - } - - #endregion (User) - + die($message); + } + + /** + * Checks if a capture response was received and tries to load the contents + * @return mixed null if no capture response was received, simplhtmldom document otherwise + */ + private function handleCaptchaResponse() + { + if (isset($_POST['captcha_response'])) { + if (session_status() == PHP_SESSION_NONE) { + session_start(); + } + + if (isset($_SESSION['captcha_fields'], $_SESSION['captcha_action'])) { + $captcha_action = $_SESSION['captcha_action']; + $captcha_fields = $_SESSION['captcha_fields']; + $captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']); + + $header = [ + 'Content-type: application/x-www-form-urlencoded', + 'Referer: ' . $captcha_action, + 'Cookie: noscript=1' + ]; + + $opts = [ + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => http_build_query($captcha_fields) + ]; + + $html = getSimpleHTMLDOM($captcha_action, $header, $opts); + + return $html; + } + + unset($_SESSION['captcha_fields']); + unset($_SESSION['captcha_action']); + } + + return null; + } + + private function collectUserData() + { + $html = $this->handleCaptchaResponse(); + + // Retrieve page contents + if (is_null($html)) { + if (getEnv('HTTP_ACCEPT_LANGUAGE')) { + $header = ['Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE')]; + } else { + $header = []; + } + + $html = getSimpleHTMLDOM($this->getURI(), $header); + } + + // Handle captcha form? + $captcha = $html->find('div.captcha_interstitial', 0); + + if (!is_null($captcha)) { + $this->returnCaptchaMessage($captcha); + } + + // No captcha? We can carry on retrieving page contents :) + // First, we check whether the page is public or not + $loginForm = $html->find('._585r', 0); + + if ($loginForm != null) { + returnServerError('You must be logged in to view this page. This is not supported by RSS-Bridge.'); + } + + $element = $html + ->find('#pagelet_timeline_main_column')[0] + ->children(0) + ->children(0) + ->next_sibling() + ->children(0); + + if (isset($element)) { + $author = str_replace(' - Posts | Facebook', '', $html->find('title#pageTitle', 0)->innertext); + + $profilePic = $html->find('meta[property="og:image"]', 0)->content; + + $this->authorName = $author; + + foreach ($element->children() as $cell) { + // Manage summary posts + if (strpos($cell->class, '_3xaf') !== false) { + $posts = $cell->children(); + } else { + $posts = [$cell]; + } + + // Optionally skip reviews + if ( + $this->getInput('skip_reviews') + && !is_null($cell->find('#review_composer_container', 0)) + ) { + continue; + } + + foreach ($posts as $post) { + // Check media type + switch ($this->getInput('media_type')) { + case 'all': + break; + case 'video': + if (empty($post->find('[aria-label=Video]'))) { + continue 2; + } + break; + case 'novideo': + if (!empty($post->find('[aria-label=Video]'))) { + continue 2; + } + break; + default: + break; + } + + $item = []; + + if (count($post->find('abbr')) > 0) { + $content = $post->find('.userContentWrapper', 0); + + // This array specifies filters applied to all posts in order of appearance + $content_filters = [ + '._5mly', // Remove embedded videos (the preview image remains) + '._2ezg', // Remove "Views ..." + '.hidden_elem', // Remove hidden elements (they are hidden anyway) + '.timestampContent', // Remove relative timestamp + '._6spk', // Remove redundant separator + ]; + + foreach ($content_filters as $filter) { + foreach ($content->find($filter) as $subject) { + $subject->outertext = ''; + } + } + + // Change origin tag for embedded media from div to paragraph + foreach ($content->find('._59tj') as $subject) { + $subject->outertext = '<p>' . $subject->innertext . '</p>'; + } + + // Change title tag for embedded media from anchor to paragraph + foreach ($content->find('._3n1k a') as $anchor) { + $anchor->outertext = '<p>' . $anchor->innertext . '</p>'; + } + + $content = preg_replace( + '/(?i)><div class=\"_3dp([^>]+)>(.+?)div\ class=\"[^u]+userContent\"/i', + '', + $content + ); + + $content = preg_replace( + '/(?i)><div class=\"_4l5([^>]+)>(.+?)<\/div>/i', + '', + $content + ); + + // Remove "SpSonsSoriSsés" + $content = preg_replace( + '/(?iU)<a [^>]+ href="#" role="link" [^>}]+>.+<\/a>/iU', + '', + $content + ); + + // Remove html nodes, keep only img, links, basic formatting + $content = strip_tags($content, '<a><img><i><u><br><p>'); + + $content = $this->unescapeFacebookLink($content); + + // Clean useless html tag properties and fix link closing tags + foreach ( + [ + 'onmouseover', + 'onclick', + 'target', + 'ajaxify', + 'tabindex', + 'class', + 'style', + 'data-[^=]*', + 'aria-[^=]*', + 'role', + 'rel', + 'id'] as $property_name + ) { + $content = preg_replace('/ ' . $property_name . '=\"[^"]*\"/i', '', $content); + } + + $content = preg_replace('/<\/a [^>]+>/i', '</a>', $content); + + $this->unescapeFacebookEmote($content); + + // Restore links in the post before further parsing + $post = defaultLinkTo($post, self::URI); + + // Restore links in the content before adding to the item + $content = defaultLinkTo($content, self::URI); + + $content = $this->removeTrackingCodes($content); + + // Retrieve date of the post + $date = $post->find('abbr')[0]; + + if (isset($date) && $date->hasAttribute('data-utime')) { + $date = $date->getAttribute('data-utime'); + } else { + $date = 0; + } + + // Build title from content + $title = strip_tags($post->find('.userContent', 0)->innertext); + if (strlen($title) > 64) { + $title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...'; + } + + $uri = $post->find('abbr')[0]->parent()->getAttribute('href'); + + // Extract fbid and patch link + if (strpos($uri, '?') !== false) { + $query = substr($uri, strpos($uri, '?') + 1); + parse_str($query, $query_params); + if (isset($query_params['story_fbid'])) { + $uri = self::URI . $query_params['story_fbid']; + } else { + $uri = substr($uri, 0, strpos($uri, '?')); + } + } + + //Build and add final item + $item['uri'] = htmlspecialchars_decode($uri, ENT_QUOTES); + $item['content'] = htmlspecialchars_decode($content, ENT_QUOTES); + $item['title'] = htmlspecialchars_decode($title, ENT_QUOTES); + $item['author'] = htmlspecialchars_decode($author, ENT_QUOTES); + $item['timestamp'] = $date; + + if (strpos($item['content'], '<img') === false) { + $item['enclosures'] = [$profilePic]; + } + + $this->items[] = $item; + } + } + } + } + } + + #endregion (User) } diff --git a/bridges/FeedExpanderExampleBridge.php b/bridges/FeedExpanderExampleBridge.php index 708d4c13..a6b37f65 100644 --- a/bridges/FeedExpanderExampleBridge.php +++ b/bridges/FeedExpanderExampleBridge.php @@ -1,61 +1,66 @@ <?php -class FeedExpanderExampleBridge extends FeedExpander { - const MAINTAINER = 'logmanoriginal'; - const NAME = 'FeedExpander Example'; - const URI = 'http://github.com/RSS-Bridge/rss-bridge/'; - const DESCRIPTION = 'Example bridge to test FeedExpander'; +class FeedExpanderExampleBridge extends FeedExpander +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'FeedExpander Example'; + const URI = 'http://github.com/RSS-Bridge/rss-bridge/'; + const DESCRIPTION = 'Example bridge to test FeedExpander'; - const PARAMETERS = array( - 'Feed' => array( - 'version' => array( - 'name' => 'Version', - 'type' => 'list', - 'title' => 'Select your feed format/version', - 'defaultValue' => 'RSS 2.0', - 'values' => array( - 'RSS 0.91' => 'rss_0_9_1', - 'RSS 1.0' => 'rss_1_0', - 'RSS 2.0' => 'rss_2_0', - 'ATOM 1.0' => 'atom_1_0' - ) - ) - ) - ); + const PARAMETERS = [ + 'Feed' => [ + 'version' => [ + 'name' => 'Version', + 'type' => 'list', + 'title' => 'Select your feed format/version', + 'defaultValue' => 'RSS 2.0', + 'values' => [ + 'RSS 0.91' => 'rss_0_9_1', + 'RSS 1.0' => 'rss_1_0', + 'RSS 2.0' => 'rss_2_0', + 'ATOM 1.0' => 'atom_1_0' + ] + ] + ] + ]; - public function collectData(){ - switch($this->getInput('version')) { - case 'rss_0_9_1': - parent::collectExpandableDatas('http://static.userland.com/gems/backend/sampleRss.xml'); - break; - case 'rss_1_0': - parent::collectExpandableDatas('http://feeds.nature.com/nature/rss/current?format=xml'); - break; - case 'rss_2_0': - parent::collectExpandableDatas('http://feeds.rssboard.org/rssboard?format=xml'); - break; - case 'atom_1_0': - parent::collectExpandableDatas('http://segfault.linuxmint.com/feed/atom/'); - break; - default: returnClientError('Unknown version ' . $this->getInput('version') . '!'); - } - } + public function collectData() + { + switch ($this->getInput('version')) { + case 'rss_0_9_1': + parent::collectExpandableDatas('http://static.userland.com/gems/backend/sampleRss.xml'); + break; + case 'rss_1_0': + parent::collectExpandableDatas('http://feeds.nature.com/nature/rss/current?format=xml'); + break; + case 'rss_2_0': + parent::collectExpandableDatas('http://feeds.rssboard.org/rssboard?format=xml'); + break; + case 'atom_1_0': + parent::collectExpandableDatas('http://segfault.linuxmint.com/feed/atom/'); + break; + default: + returnClientError('Unknown version ' . $this->getInput('version') . '!'); + } + } - protected function parseItem($newsItem) { - switch($this->getInput('version')) { - case 'rss_0_9_1': - return $this->parseRss091Item($newsItem); - break; - case 'rss_1_0': - return $this->parseRss1Item($newsItem); - break; - case 'rss_2_0': - return $this->parseRss2Item($newsItem); - break; - case 'atom_1_0': - return $this->parseATOMItem($newsItem); - break; - default: returnClientError('Unknown version ' . $this->getInput('version') . '!'); - } - } + protected function parseItem($newsItem) + { + switch ($this->getInput('version')) { + case 'rss_0_9_1': + return $this->parseRss091Item($newsItem); + break; + case 'rss_1_0': + return $this->parseRss1Item($newsItem); + break; + case 'rss_2_0': + return $this->parseRss2Item($newsItem); + break; + case 'atom_1_0': + return $this->parseATOMItem($newsItem); + break; + default: + returnClientError('Unknown version ' . $this->getInput('version') . '!'); + } + } } diff --git a/bridges/FeedMergeBridge.php b/bridges/FeedMergeBridge.php index b90055e5..df3d39c4 100644 --- a/bridges/FeedMergeBridge.php +++ b/bridges/FeedMergeBridge.php @@ -1,61 +1,65 @@ <?php -class FeedMergeBridge extends FeedExpander { - const MAINTAINER = 'dvikan'; - const NAME = 'FeedMerge'; - const URI = 'https://github.com/RSS-Bridge/rss-bridge'; - const DESCRIPTION = <<<'TEXT' +class FeedMergeBridge extends FeedExpander +{ + const MAINTAINER = 'dvikan'; + const NAME = 'FeedMerge'; + const URI = 'https://github.com/RSS-Bridge/rss-bridge'; + const DESCRIPTION = <<<'TEXT' This bridge merges two or more feeds into a single feed. Max 10 items are fetched from each feed. TEXT; - const PARAMETERS = [ - [ - 'feed_name' => [ - 'name' => 'Feed name', - 'type' => 'text', - 'exampleValue' => 'rss-bridge/FeedMerger', - ], - 'feed_1' => [ - 'name' => 'Feed url', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day' - ], - 'feed_2' => ['name' => 'Feed url', 'type' => 'text'], - 'feed_3' => ['name' => 'Feed url', 'type' => 'text'], - 'feed_4' => ['name' => 'Feed url', 'type' => 'text'], - 'feed_5' => ['name' => 'Feed url', 'type' => 'text'], + const PARAMETERS = [ + [ + 'feed_name' => [ + 'name' => 'Feed name', + 'type' => 'text', + 'exampleValue' => 'rss-bridge/FeedMerger', + ], + 'feed_1' => [ + 'name' => 'Feed url', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day' + ], + 'feed_2' => ['name' => 'Feed url', 'type' => 'text'], + 'feed_3' => ['name' => 'Feed url', 'type' => 'text'], + 'feed_4' => ['name' => 'Feed url', 'type' => 'text'], + 'feed_5' => ['name' => 'Feed url', 'type' => 'text'], - 'limit' => self::LIMIT, - ] - ]; + 'limit' => self::LIMIT, + ] + ]; - public function collectData() { - $limit = (int)($this->getInput('limit') ?: 10); - $feeds = [ - $this->getInput('feed_1'), - $this->getInput('feed_2'), - $this->getInput('feed_3'), - $this->getInput('feed_4'), - $this->getInput('feed_5'), - ]; - // Remove empty values - $feeds = array_filter($feeds); - foreach ($feeds as $feed) { - // Fetch all items from the feed - $this->collectExpandableDatas($feed); - } - // Sort by timestamp descending - usort($this->items, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']); - // Grab the first $limit items - $this->items = array_slice($this->items, 0, $limit); - } + public function collectData() + { + $limit = (int)($this->getInput('limit') ?: 10); + $feeds = [ + $this->getInput('feed_1'), + $this->getInput('feed_2'), + $this->getInput('feed_3'), + $this->getInput('feed_4'), + $this->getInput('feed_5'), + ]; + // Remove empty values + $feeds = array_filter($feeds); + foreach ($feeds as $feed) { + // Fetch all items from the feed + $this->collectExpandableDatas($feed); + } + // Sort by timestamp descending + usort($this->items, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']); + // Grab the first $limit items + $this->items = array_slice($this->items, 0, $limit); + } - public function getIcon() { - return 'https://cdn.jsdelivr.net/npm/famfamfam-silk@1.0.0/dist/png/folder_feed.png'; - } + public function getIcon() + { + return 'https://cdn.jsdelivr.net/npm/famfamfam-silk@1.0.0/dist/png/folder_feed.png'; + } - public function getName() { - return $this->getInput('feed_name') ?: 'rss-bridge/FeedMerger'; - } + public function getName() + { + return $this->getInput('feed_name') ?: 'rss-bridge/FeedMerger'; + } } diff --git a/bridges/FeedReducerBridge.php b/bridges/FeedReducerBridge.php index 613a5539..a37824c9 100644 --- a/bridges/FeedReducerBridge.php +++ b/bridges/FeedReducerBridge.php @@ -1,60 +1,64 @@ <?php -class FeedReducerBridge extends FeedExpander { - - const MAINTAINER = 'mdemoss'; - const NAME = 'Feed Reducer'; - const URI = 'http://github.com/RSS-Bridge/rss-bridge/'; - const DESCRIPTION = 'Choose a percentage of a feed you want to see.'; - const PARAMETERS = array( array( - 'url' => array( - 'name' => 'Feed URI', - 'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?length=42', - 'required' => true - ), - 'percentage' => array( - 'name' => 'percentage', - 'type' => 'number', - 'exampleValue' => 50, - 'required' => true - ) - )); - const CACHE_TIMEOUT = 3600; - - public function collectData(){ - if(preg_match('#^http(s?)://#i', $this->getInput('url'))) { - $this->collectExpandableDatas($this->getInput('url')); - } else { - throw new Exception('URI must begin with http(s)://'); - } - } - - public function getItems(){ - $filteredItems = array(); - $intPercentage = (int)preg_replace('/[^0-9]/', '', $this->getInput('percentage')); - - foreach ($this->items as $thisItem) { - // The URL is included in the hash: - // - so you can change the output by adding a local-part to the URL - // - so items with the same URI in different feeds won't be correlated - - // $pseudoRandomInteger will be a 16 bit unsigned int mod 100. - // This won't be uniformly distributed 1-100, but should be close enough. - - $pseudoRandomInteger = unpack( - 'S', // unsigned 16-bit int - hash( 'sha256', $thisItem['uri'] . '::' . $this->getInput('url'), true ) - )[1] % 100; - - if ($pseudoRandomInteger < $intPercentage) { - $filteredItems[] = $thisItem; - } - } - - return $filteredItems; - } - - public function getName(){ - $trimmedPercentage = preg_replace('/[^0-9]/', '', $this->getInput('percentage') ?? ''); - return parent::getName() . ' [' . $trimmedPercentage . '%]'; - } + +class FeedReducerBridge extends FeedExpander +{ + const MAINTAINER = 'mdemoss'; + const NAME = 'Feed Reducer'; + const URI = 'http://github.com/RSS-Bridge/rss-bridge/'; + const DESCRIPTION = 'Choose a percentage of a feed you want to see.'; + const PARAMETERS = [ [ + 'url' => [ + 'name' => 'Feed URI', + 'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?length=42', + 'required' => true + ], + 'percentage' => [ + 'name' => 'percentage', + 'type' => 'number', + 'exampleValue' => 50, + 'required' => true + ] + ]]; + const CACHE_TIMEOUT = 3600; + + public function collectData() + { + if (preg_match('#^http(s?)://#i', $this->getInput('url'))) { + $this->collectExpandableDatas($this->getInput('url')); + } else { + throw new Exception('URI must begin with http(s)://'); + } + } + + public function getItems() + { + $filteredItems = []; + $intPercentage = (int)preg_replace('/[^0-9]/', '', $this->getInput('percentage')); + + foreach ($this->items as $thisItem) { + // The URL is included in the hash: + // - so you can change the output by adding a local-part to the URL + // - so items with the same URI in different feeds won't be correlated + + // $pseudoRandomInteger will be a 16 bit unsigned int mod 100. + // This won't be uniformly distributed 1-100, but should be close enough. + + $pseudoRandomInteger = unpack( + 'S', // unsigned 16-bit int + hash('sha256', $thisItem['uri'] . '::' . $this->getInput('url'), true) + )[1] % 100; + + if ($pseudoRandomInteger < $intPercentage) { + $filteredItems[] = $thisItem; + } + } + + return $filteredItems; + } + + public function getName() + { + $trimmedPercentage = preg_replace('/[^0-9]/', '', $this->getInput('percentage') ?? ''); + return parent::getName() . ' [' . $trimmedPercentage . '%]'; + } } diff --git a/bridges/FicbookBridge.php b/bridges/FicbookBridge.php index 64cdb32d..2a245d4e 100644 --- a/bridges/FicbookBridge.php +++ b/bridges/FicbookBridge.php @@ -1,184 +1,195 @@ <?php -class FicbookBridge extends BridgeAbstract { - - const NAME = 'Ficbook Bridge'; - const URI = 'https://ficbook.net/'; - const DESCRIPTION = 'No description provided'; - const MAINTAINER = 'logmanoriginal'; - - const PARAMETERS = array( - 'Site News' => array(), - 'Fiction Updates' => array( - 'fiction_id' => array( - 'name' => 'Fanfiction ID', - 'type' => 'text', - 'pattern' => '[0-9]+', - 'required' => true, - 'title' => 'Insert fanfiction ID', - 'exampleValue' => '5783919', - ), - 'include_contents' => array( - 'name' => 'Include contents', - 'type' => 'checkbox', - 'title' => 'Activate to include contents in the feed', - ), - ), - 'Fiction Comments' => array( - 'fiction_id' => array( - 'name' => 'Fanfiction ID', - 'type' => 'text', - 'pattern' => '[0-9]+', - 'required' => true, - 'title' => 'Insert fanfiction ID', - 'exampleValue' => '5783919', - ), - ), - ); - - protected $titleName; - - public function getURI() { - switch($this->queriedContext) { - case 'Site News': - // For some reason this is not HTTPS - return 'http://ficbook.net/sitenews'; - - case 'Fiction Updates': - return self::URI - . 'readfic/' - . urlencode($this->getInput('fiction_id')); - - case 'Fiction Comments': - return self::URI - . 'readfic/' - . urlencode($this->getInput('fiction_id')) - . '/comments#content'; - - default: return parent::getURI(); - } - } - - public function getName() { - switch($this->queriedContext) { - case 'Site News': - return $this->queriedContext . ' | ' . self::NAME; - - case 'Fiction Updates': - return $this->titleName . ' | ' . self::NAME; - - case 'Fiction Comments': - return $this->titleName . ' | Comments | ' . self::NAME; - - default: return self::NAME; - } - } - - public function collectData() { - - $header = array('Accept-Language: en-US'); - - $html = getSimpleHTMLDOM($this->getURI(), $header); - - $html = defaultLinkTo($html, self::URI); - - if ($this->queriedContext == 'Fiction Updates' or $this->queriedContext == 'Fiction Comments') { - $this->titleName = $html->find('.fanfic-main-info > h1', 0)->innertext; - } - - switch($this->queriedContext) { - case 'Site News': return $this->collectSiteNews($html); - case 'Fiction Updates': return $this->collectUpdatesData($html); - case 'Fiction Comments': return $this->collectCommentsData($html); - } - - } - - private function collectSiteNews($html) { - foreach($html->find('.news_view') as $news) { - $this->items[] = array( - 'title' => $news->find('h1.title', 0)->plaintext, - 'timestamp' => strtotime($this->fixDate($news->find('span[title]', 0)->title)), - 'content' => $news->find('.news_text', 0), - ); - } - } - - private function collectCommentsData($html) { - foreach($html->find('article.comment-container') as $article) { - $this->items[] = array( - 'uri' => $article->find('.comment_link_to_fic > a', 0)->href, - 'title' => $article->find('.comment_author', 0)->plaintext, - 'author' => $article->find('.comment_author', 0)->plaintext, - 'timestamp' => strtotime($this->fixDate($article->find('time[datetime]', 0)->datetime)), - 'content' => $article->find('.comment_message', 0), - 'enclosures' => array($article->find('img', 0)->src), - ); - } - } - - private function collectUpdatesData($html) { - foreach($html->find('ul.list-of-fanfic-parts > li') as $chapter) { - $item = array( - 'uri' => $chapter->find('a', 0)->href, - 'title' => $chapter->find('a', 0)->plaintext, - 'timestamp' => strtotime($this->fixDate($chapter->find('span[title]', 0)->title)), - ); - - if($this->getInput('include_contents')) { - $content = getSimpleHTMLDOMCached($item['uri']); - $item['content'] = $content->find('#content', 0); - } - - $this->items[] = $item; - - // Sort by time, descending - usort($this->items, function($a, $b){ return $b['timestamp'] - $a['timestamp']; }); - } - } - - private function fixDate($date) { - - // FIXME: This list was generated using Google tranlator. Someone who - // actually knows russian should check this list! Please keep in mind - // that month names must match exactly the names returned by Ficbook. - $ru_month = array( - 'января', - 'февраля', - 'марта', - 'апреля', - 'мая', - 'июня', - 'июля', - 'августа', - 'сентября', - 'октября', - 'ноября', - 'декабря', - ); - - $en_month = array( - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - ); - - $fixed_date = str_replace($ru_month, $en_month, $date); - - if($fixed_date === $date) { - Debug::log('Unable to fix date: ' . $date); - return null; - } - - return $fixed_date; - - } + +class FicbookBridge extends BridgeAbstract +{ + const NAME = 'Ficbook Bridge'; + const URI = 'https://ficbook.net/'; + const DESCRIPTION = 'No description provided'; + const MAINTAINER = 'logmanoriginal'; + + const PARAMETERS = [ + 'Site News' => [], + 'Fiction Updates' => [ + 'fiction_id' => [ + 'name' => 'Fanfiction ID', + 'type' => 'text', + 'pattern' => '[0-9]+', + 'required' => true, + 'title' => 'Insert fanfiction ID', + 'exampleValue' => '5783919', + ], + 'include_contents' => [ + 'name' => 'Include contents', + 'type' => 'checkbox', + 'title' => 'Activate to include contents in the feed', + ], + ], + 'Fiction Comments' => [ + 'fiction_id' => [ + 'name' => 'Fanfiction ID', + 'type' => 'text', + 'pattern' => '[0-9]+', + 'required' => true, + 'title' => 'Insert fanfiction ID', + 'exampleValue' => '5783919', + ], + ], + ]; + + protected $titleName; + + public function getURI() + { + switch ($this->queriedContext) { + case 'Site News': + // For some reason this is not HTTPS + return 'http://ficbook.net/sitenews'; + + case 'Fiction Updates': + return self::URI + . 'readfic/' + . urlencode($this->getInput('fiction_id')); + + case 'Fiction Comments': + return self::URI + . 'readfic/' + . urlencode($this->getInput('fiction_id')) + . '/comments#content'; + + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Site News': + return $this->queriedContext . ' | ' . self::NAME; + + case 'Fiction Updates': + return $this->titleName . ' | ' . self::NAME; + + case 'Fiction Comments': + return $this->titleName . ' | Comments | ' . self::NAME; + + default: + return self::NAME; + } + } + + public function collectData() + { + $header = ['Accept-Language: en-US']; + + $html = getSimpleHTMLDOM($this->getURI(), $header); + + $html = defaultLinkTo($html, self::URI); + + if ($this->queriedContext == 'Fiction Updates' or $this->queriedContext == 'Fiction Comments') { + $this->titleName = $html->find('.fanfic-main-info > h1', 0)->innertext; + } + + switch ($this->queriedContext) { + case 'Site News': + return $this->collectSiteNews($html); + case 'Fiction Updates': + return $this->collectUpdatesData($html); + case 'Fiction Comments': + return $this->collectCommentsData($html); + } + } + + private function collectSiteNews($html) + { + foreach ($html->find('.news_view') as $news) { + $this->items[] = [ + 'title' => $news->find('h1.title', 0)->plaintext, + 'timestamp' => strtotime($this->fixDate($news->find('span[title]', 0)->title)), + 'content' => $news->find('.news_text', 0), + ]; + } + } + + private function collectCommentsData($html) + { + foreach ($html->find('article.comment-container') as $article) { + $this->items[] = [ + 'uri' => $article->find('.comment_link_to_fic > a', 0)->href, + 'title' => $article->find('.comment_author', 0)->plaintext, + 'author' => $article->find('.comment_author', 0)->plaintext, + 'timestamp' => strtotime($this->fixDate($article->find('time[datetime]', 0)->datetime)), + 'content' => $article->find('.comment_message', 0), + 'enclosures' => [$article->find('img', 0)->src], + ]; + } + } + + private function collectUpdatesData($html) + { + foreach ($html->find('ul.list-of-fanfic-parts > li') as $chapter) { + $item = [ + 'uri' => $chapter->find('a', 0)->href, + 'title' => $chapter->find('a', 0)->plaintext, + 'timestamp' => strtotime($this->fixDate($chapter->find('span[title]', 0)->title)), + ]; + + if ($this->getInput('include_contents')) { + $content = getSimpleHTMLDOMCached($item['uri']); + $item['content'] = $content->find('#content', 0); + } + + $this->items[] = $item; + + // Sort by time, descending + usort($this->items, function ($a, $b) { + return $b['timestamp'] - $a['timestamp']; + }); + } + } + + private function fixDate($date) + { + // FIXME: This list was generated using Google tranlator. Someone who + // actually knows russian should check this list! Please keep in mind + // that month names must match exactly the names returned by Ficbook. + $ru_month = [ + 'января', + 'февраля', + 'марта', + 'апреля', + 'мая', + 'июня', + 'июля', + 'августа', + 'сентября', + 'октября', + 'ноября', + 'декабря', + ]; + + $en_month = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + + $fixed_date = str_replace($ru_month, $en_month, $date); + + if ($fixed_date === $date) { + Debug::log('Unable to fix date: ' . $date); + return null; + } + + return $fixed_date; + } } diff --git a/bridges/FilterBridge.php b/bridges/FilterBridge.php index fdf1fec8..6878804b 100644 --- a/bridges/FilterBridge.php +++ b/bridges/FilterBridge.php @@ -1,141 +1,144 @@ <?php -class FilterBridge extends FeedExpander { +class FilterBridge extends FeedExpander +{ + const MAINTAINER = 'Frenzie, ORelio'; + const NAME = 'Filter'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Filters a feed of your choice'; + const URI = 'https://github.com/RSS-Bridge/rss-bridge'; - const MAINTAINER = 'Frenzie, ORelio'; - const NAME = 'Filter'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Filters a feed of your choice'; - const URI = 'https://github.com/RSS-Bridge/rss-bridge'; + const PARAMETERS = [[ + 'url' => [ + 'name' => 'Feed URL', + 'type' => 'text', + 'defaultValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day', + 'required' => true, + ], + 'filter' => [ + 'name' => 'Filter (regular expression)', + 'required' => false, + ], + 'filter_type' => [ + 'name' => 'Filter type', + 'type' => 'list', + 'required' => false, + 'values' => [ + 'Keep matching items' => 'permit', + 'Hide matching items' => 'block', + ], + 'defaultValue' => 'permit', + ], + 'case_insensitive' => [ + 'name' => 'Case-insensitive filter', + 'type' => 'checkbox', + 'required' => false, + ], + 'fix_encoding' => [ + 'name' => 'Attempt Latin1/UTF-8 fixes when evaluating filter', + 'type' => 'checkbox', + 'required' => false, + ], + 'target_title' => [ + 'name' => 'Apply filter on title', + 'type' => 'checkbox', + 'required' => false, + 'defaultValue' => 'checked' + ], + 'target_content' => [ + 'name' => 'Apply filter on content', + 'type' => 'checkbox', + 'required' => false, + ], + 'target_author' => [ + 'name' => 'Apply filter on author', + 'type' => 'checkbox', + 'required' => false, + ], + 'title_from_content' => [ + 'name' => 'Generate title from content (overwrite existing title)', + 'type' => 'checkbox', + 'required' => false, + ], + 'length_limit' => [ + 'name' => 'Max length analyzed by filter (-1: no limit)', + 'type' => 'number', + 'required' => false, + 'defaultValue' => -1, + ], + ]]; - const PARAMETERS = array(array( - 'url' => array( - 'name' => 'Feed URL', - 'type' => 'text', - 'defaultValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day', - 'required' => true, - ), - 'filter' => array( - 'name' => 'Filter (regular expression)', - 'required' => false, - ), - 'filter_type' => array( - 'name' => 'Filter type', - 'type' => 'list', - 'required' => false, - 'values' => array( - 'Keep matching items' => 'permit', - 'Hide matching items' => 'block', - ), - 'defaultValue' => 'permit', - ), - 'case_insensitive' => array( - 'name' => 'Case-insensitive filter', - 'type' => 'checkbox', - 'required' => false, - ), - 'fix_encoding' => array( - 'name' => 'Attempt Latin1/UTF-8 fixes when evaluating filter', - 'type' => 'checkbox', - 'required' => false, - ), - 'target_title' => array( - 'name' => 'Apply filter on title', - 'type' => 'checkbox', - 'required' => false, - 'defaultValue' => 'checked' - ), - 'target_content' => array( - 'name' => 'Apply filter on content', - 'type' => 'checkbox', - 'required' => false, - ), - 'target_author' => array( - 'name' => 'Apply filter on author', - 'type' => 'checkbox', - 'required' => false, - ), - 'title_from_content' => array( - 'name' => 'Generate title from content (overwrite existing title)', - 'type' => 'checkbox', - 'required' => false, - ), - 'length_limit' => array( - 'name' => 'Max length analyzed by filter (-1: no limit)', - 'type' => 'number', - 'required' => false, - 'defaultValue' => -1, - ), - )); + protected function parseItem($newItem) + { + $item = parent::parseItem($newItem); - protected function parseItem($newItem){ - $item = parent::parseItem($newItem); + // Generate title from first 50 characters of content? + if ($this->getInput('title_from_content') && array_key_exists('content', $item)) { + $content = str_get_html($item['content']); + $pos = strpos($item['content'], ' ', 50); + $item['title'] = substr($content->plaintext, 0, $pos); + if (strlen($content->plaintext) >= $pos) { + $item['title'] .= '...'; + } + } - // Generate title from first 50 characters of content? - if($this->getInput('title_from_content') && array_key_exists('content', $item)) { - $content = str_get_html($item['content']); - $pos = strpos($item['content'], ' ', 50); - $item['title'] = substr($content->plaintext, 0, $pos); - if(strlen($content->plaintext) >= $pos) { - $item['title'] .= '...'; - } - } + // Build regular expression + $regex = '/' . $this->getInput('filter') . '/'; + if ($this->getInput('case_insensitive')) { + $regex .= 'i'; + } - // Build regular expression - $regex = '/' . $this->getInput('filter') . '/'; - if($this->getInput('case_insensitive')) { - $regex .= 'i'; - } + // Retrieve fields to check + $filter_fields = []; + if ($this->getInput('target_title')) { + $filter_fields[] = $item['title']; + } + if ($this->getInput('target_content')) { + $filter_fields[] = $item['content']; + } + if ($this->getInput('target_author')) { + $filter_fields[] = $item['author']; + } - // Retrieve fields to check - $filter_fields = array(); - if($this->getInput('target_title')) { - $filter_fields[] = $item['title']; - } - if($this->getInput('target_content')) { - $filter_fields[] = $item['content']; - } - if($this->getInput('target_author')) { - $filter_fields[] = $item['author']; - } + // Apply filter on item + $keep_item = false; + $length_limit = intval($this->getInput('length_limit')); + foreach ($filter_fields as $field) { + if ($length_limit > 0) { + $field = substr($field, 0, $length_limit); + } + $keep_item |= boolval(preg_match($regex, $field)); + if ($this->getInput('fix_encoding')) { + $keep_item |= boolval(preg_match($regex, utf8_decode($field))); + $keep_item |= boolval(preg_match($regex, utf8_encode($field))); + } + } - // Apply filter on item - $keep_item = false; - $length_limit = intval($this->getInput('length_limit')); - foreach($filter_fields as $field) { - if($length_limit > 0) { - $field = substr($field, 0, $length_limit); - } - $keep_item |= boolval(preg_match($regex, $field)); - if($this->getInput('fix_encoding')) { - $keep_item |= boolval(preg_match($regex, utf8_decode($field))); - $keep_item |= boolval(preg_match($regex, utf8_encode($field))); - } - } + // Reverse result? (keep everything but matching items) + if ($this->getInput('filter_type') === 'block') { + $keep_item = !$keep_item; + } - // Reverse result? (keep everything but matching items) - if($this->getInput('filter_type') === 'block') { - $keep_item = !$keep_item; - } + return $keep_item ? $item : null; + } - return $keep_item ? $item : null; - } + public function getURI() + { + $url = $this->getInput('url'); - public function getURI(){ - $url = $this->getInput('url'); + if (empty($url)) { + $url = parent::getURI(); + } - if(empty($url)) { - $url = parent::getURI(); - } + return $url; + } - return $url; - } - - public function collectData(){ - if($this->getInput('url') && substr($this->getInput('url'), 0, 4) !== 'http') { - // just in case someone finds a way to access local files by playing with the url - returnClientError('The url parameter must either refer to http or https protocol.'); - } - $this->collectExpandableDatas($this->getURI()); - } + public function collectData() + { + if ($this->getInput('url') && substr($this->getInput('url'), 0, 4) !== 'http') { + // just in case someone finds a way to access local files by playing with the url + returnClientError('The url parameter must either refer to http or https protocol.'); + } + $this->collectExpandableDatas($this->getURI()); + } } diff --git a/bridges/FindACrewBridge.php b/bridges/FindACrewBridge.php index 8282ead1..9119535b 100644 --- a/bridges/FindACrewBridge.php +++ b/bridges/FindACrewBridge.php @@ -1,89 +1,93 @@ <?php -class FindACrewBridge extends BridgeAbstract { - const MAINTAINER = 'couraudt'; - const NAME = 'Find A Crew Bridge'; - const URI = 'https://www.findacrew.net'; - const DESCRIPTION = 'Returns the newest sailing offers.'; - const PARAMETERS = array( - array( - 'type' => array( - 'name' => 'Type of search', - 'title' => 'Choose between finding a boat or a crew', - 'type' => 'list', - 'values' => array( - 'Find a boat' => 'boat', - 'Find a crew' => 'crew' - ) - ), - 'long' => array( - 'name' => 'Longitude of the searched location', - 'title' => 'Center the search at that longitude (e.g: -42.02)' - ), - 'lat' => array( - 'name' => 'Latitude of the searched location', - 'title' => 'Center the search at that latitude (e.g: 12.42)' - ), - 'distance' => array( - 'name' => 'Limit boundary of search in KM', - 'title' => 'Boundary of the search in kilometers when using longitude and latitude' - ), - 'limit' => self::LIMIT, - ) - ); - public function collectData() { - $url = $this->getURI(); +class FindACrewBridge extends BridgeAbstract +{ + const MAINTAINER = 'couraudt'; + const NAME = 'Find A Crew Bridge'; + const URI = 'https://www.findacrew.net'; + const DESCRIPTION = 'Returns the newest sailing offers.'; + const PARAMETERS = [ + [ + 'type' => [ + 'name' => 'Type of search', + 'title' => 'Choose between finding a boat or a crew', + 'type' => 'list', + 'values' => [ + 'Find a boat' => 'boat', + 'Find a crew' => 'crew' + ] + ], + 'long' => [ + 'name' => 'Longitude of the searched location', + 'title' => 'Center the search at that longitude (e.g: -42.02)' + ], + 'lat' => [ + 'name' => 'Latitude of the searched location', + 'title' => 'Center the search at that latitude (e.g: 12.42)' + ], + 'distance' => [ + 'name' => 'Limit boundary of search in KM', + 'title' => 'Boundary of the search in kilometers when using longitude and latitude' + ], + 'limit' => self::LIMIT, + ] + ]; - if ($this->getInput('type') == 'boat') { - $data = array('SrhLstBtAction' => 'Create'); - } else { - $data = array('SrhLstCwAction' => 'Create'); - } + public function collectData() + { + $url = $this->getURI(); - if ($this->getInput('long') && $this->getInput('lat')) { - $data['real_LocSrh_Lng'] = $this->getInput('long'); - $data['real_LocSrh_Lat'] = $this->getInput('lat'); - if ($this->getInput('distance')) { - $data['LocDis'] = (int)$this->getInput('distance') * 1000; - } - } + if ($this->getInput('type') == 'boat') { + $data = ['SrhLstBtAction' => 'Create']; + } else { + $data = ['SrhLstCwAction' => 'Create']; + } - $header = array( - 'Content-Type: application/x-www-form-urlencoded' - ); + if ($this->getInput('long') && $this->getInput('lat')) { + $data['real_LocSrh_Lng'] = $this->getInput('long'); + $data['real_LocSrh_Lat'] = $this->getInput('lat'); + if ($this->getInput('distance')) { + $data['LocDis'] = (int)$this->getInput('distance') * 1000; + } + } - $opts = array( - CURLOPT_CUSTOMREQUEST => 'POST', - CURLOPT_POSTFIELDS => http_build_query($data) . "\n" - ); + $header = [ + 'Content-Type: application/x-www-form-urlencoded' + ]; - $html = getSimpleHTMLDOM($url, $header, $opts) or returnClientError('No results for this query.'); + $opts = [ + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => http_build_query($data) . "\n" + ]; - $annonces = $html->find('.css_SrhRst'); - $limit = $this->getInput('limit') ?? 10; - foreach (array_slice($annonces, 0, $limit) as $annonce) { - $item = array(); + $html = getSimpleHTMLDOM($url, $header, $opts) or returnClientError('No results for this query.'); - $link = parent::getURI() . $annonce->find('.lstsum-btn-con a', 0)->href; - $htmlDetail = getSimpleHTMLDOMCached($link . '?mdl=2'); // add ?mdl=2 for xhr content not full html page + $annonces = $html->find('.css_SrhRst'); + $limit = $this->getInput('limit') ?? 10; + foreach (array_slice($annonces, 0, $limit) as $annonce) { + $item = []; - $img = parent::getURI() . $htmlDetail->find('img.img-responsive', 0)->getAttribute('src'); - $item['title'] = $htmlDetail->find('div.label-account', 0)->plaintext; - $item['uri'] = $link; - $content = $htmlDetail->find('.panel-body div.clearfix.row > div', 1)->innertext; - $content .= $htmlDetail->find('.panel-body > div', 1)->innertext; - $content = defaultLinkTo($content, parent::getURI()); - $item['content'] = $content; - $item['enclosures'] = array($img); - $item['categories'] = array($annonce->find('.css_AccLocCur', 0)->plaintext); - $this->items[] = $item; - } - } + $link = parent::getURI() . $annonce->find('.lstsum-btn-con a', 0)->href; + $htmlDetail = getSimpleHTMLDOMCached($link . '?mdl=2'); // add ?mdl=2 for xhr content not full html page - public function getURI() { - $uri = parent::getURI(); - // Those params must be in the URL - $uri .= '/en/' . $this->getInput('type') . '/search?srhtyp=srhrst&mdl=2'; - return $uri; - } + $img = parent::getURI() . $htmlDetail->find('img.img-responsive', 0)->getAttribute('src'); + $item['title'] = $htmlDetail->find('div.label-account', 0)->plaintext; + $item['uri'] = $link; + $content = $htmlDetail->find('.panel-body div.clearfix.row > div', 1)->innertext; + $content .= $htmlDetail->find('.panel-body > div', 1)->innertext; + $content = defaultLinkTo($content, parent::getURI()); + $item['content'] = $content; + $item['enclosures'] = [$img]; + $item['categories'] = [$annonce->find('.css_AccLocCur', 0)->plaintext]; + $this->items[] = $item; + } + } + + public function getURI() + { + $uri = parent::getURI(); + // Those params must be in the URL + $uri .= '/en/' . $this->getInput('type') . '/search?srhtyp=srhrst&mdl=2'; + return $uri; + } } diff --git a/bridges/FirefoxAddonsBridge.php b/bridges/FirefoxAddonsBridge.php index ca237f77..f85c3ea4 100644 --- a/bridges/FirefoxAddonsBridge.php +++ b/bridges/FirefoxAddonsBridge.php @@ -1,75 +1,78 @@ <?php -class FirefoxAddonsBridge extends BridgeAbstract { - const NAME = 'Firefox Add-ons Bridge'; - const URI = 'https://addons.mozilla.org/'; - const DESCRIPTION = 'Returns version history for a Firefox Add-on.'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(array( - 'id' => array( - 'name' => 'Add-on ID', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'save-to-the-wayback-machine', - ) - ) - ); - const CACHE_TIMEOUT = 3600; - - private $feedName = ''; - private $releaseDateRegex = '/Released ([\w, ]+) - ([\w. ]+)/'; - private $xpiFileRegex = '/([A-Za-z0-9_.-]+)\.xpi$/'; - private $outgoingRegex = '/https:\/\/outgoing.prod.mozaws.net\/v1\/(?:[A-z0-9]+)\//'; - - private $urlRegex = '/addons\.mozilla\.org\/(?:[\w-]+\/)?firefox\/addon\/([\w-]+)/'; - - public function detectParameters($url) { - $params = array(); - - if(preg_match($this->urlRegex, $url, $matches)) { - $params['id'] = $matches[1]; - return $params; - } - - return null; - } - - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); - - $this->feedName = $html->find('h1[class="AddonTitle"] > a', 0)->innertext; - $author = $html->find('span.AddonTitle-author > a', 0)->plaintext; - - foreach ($html->find('li.AddonVersionCard') as $li) { - $item = array(); - - $item['title'] = $li->find('h2.AddonVersionCard-version', 0)->plaintext; - $item['uid'] = $item['title']; - $item['uri'] = $this->getURI(); - $item['author'] = $author; - - if (preg_match($this->releaseDateRegex, $li->find('div.AddonVersionCard-fileInfo', 0)->plaintext, $match)) { - $item['timestamp'] = $match[1]; - $size = $match[2]; - } - - $compatibility = $li->find('div.AddonVersionCard-compatibility', 0)->plaintext; - $license = $li->find('p.AddonVersionCard-license', 0)->innertext; - - if ($li->find('a.InstallButtonWrapper-download-link', 0)) { - $downloadlink = $li->find('a.InstallButtonWrapper-download-link', 0)->href; - - } elseif ($li->find('a.Button.Button--action.AMInstallButton-button.Button--puffy', 0)) { - $downloadlink = $li->find('a.Button.Button--action.AMInstallButton-button.Button--puffy', 0)->href; - } - - $releaseNotes = $this->removeOutgoinglink($li->find('div.AddonVersionCard-releaseNotes', 0)); - - if (preg_match($this->xpiFileRegex, $downloadlink, $match)) { - $xpiFilename = $match[0]; - } - - $item['content'] = <<<EOD +class FirefoxAddonsBridge extends BridgeAbstract +{ + const NAME = 'Firefox Add-ons Bridge'; + const URI = 'https://addons.mozilla.org/'; + const DESCRIPTION = 'Returns version history for a Firefox Add-on.'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [[ + 'id' => [ + 'name' => 'Add-on ID', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'save-to-the-wayback-machine', + ] + ] + ]; + + const CACHE_TIMEOUT = 3600; + + private $feedName = ''; + private $releaseDateRegex = '/Released ([\w, ]+) - ([\w. ]+)/'; + private $xpiFileRegex = '/([A-Za-z0-9_.-]+)\.xpi$/'; + private $outgoingRegex = '/https:\/\/outgoing.prod.mozaws.net\/v1\/(?:[A-z0-9]+)\//'; + + private $urlRegex = '/addons\.mozilla\.org\/(?:[\w-]+\/)?firefox\/addon\/([\w-]+)/'; + + public function detectParameters($url) + { + $params = []; + + if (preg_match($this->urlRegex, $url, $matches)) { + $params['id'] = $matches[1]; + return $params; + } + + return null; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $this->feedName = $html->find('h1[class="AddonTitle"] > a', 0)->innertext; + $author = $html->find('span.AddonTitle-author > a', 0)->plaintext; + + foreach ($html->find('li.AddonVersionCard') as $li) { + $item = []; + + $item['title'] = $li->find('h2.AddonVersionCard-version', 0)->plaintext; + $item['uid'] = $item['title']; + $item['uri'] = $this->getURI(); + $item['author'] = $author; + + if (preg_match($this->releaseDateRegex, $li->find('div.AddonVersionCard-fileInfo', 0)->plaintext, $match)) { + $item['timestamp'] = $match[1]; + $size = $match[2]; + } + + $compatibility = $li->find('div.AddonVersionCard-compatibility', 0)->plaintext; + $license = $li->find('p.AddonVersionCard-license', 0)->innertext; + + if ($li->find('a.InstallButtonWrapper-download-link', 0)) { + $downloadlink = $li->find('a.InstallButtonWrapper-download-link', 0)->href; + } elseif ($li->find('a.Button.Button--action.AMInstallButton-button.Button--puffy', 0)) { + $downloadlink = $li->find('a.Button.Button--action.AMInstallButton-button.Button--puffy', 0)->href; + } + + $releaseNotes = $this->removeOutgoinglink($li->find('div.AddonVersionCard-releaseNotes', 0)); + + if (preg_match($this->xpiFileRegex, $downloadlink, $match)) { + $xpiFilename = $match[0]; + } + + $item['content'] = <<<EOD <strong>Release Notes</strong> <p>{$releaseNotes}</p> <strong>Compatibility</strong> @@ -80,31 +83,34 @@ class FirefoxAddonsBridge extends BridgeAbstract { <p><a href="{$downloadlink}">{$xpiFilename}</a> ($size)</p> EOD; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getURI() { - if (!is_null($this->getInput('id'))) { - return self::URI . 'en-US/firefox/addon/' . $this->getInput('id') . '/versions/'; - } + public function getURI() + { + if (!is_null($this->getInput('id'))) { + return self::URI . 'en-US/firefox/addon/' . $this->getInput('id') . '/versions/'; + } - return parent::getURI(); - } + return parent::getURI(); + } - public function getName() { - if (!empty($this->feedName)) { - return $this->feedName . ' - Firefox Add-on'; - } + public function getName() + { + if (!empty($this->feedName)) { + return $this->feedName . ' - Firefox Add-on'; + } - return parent::getName(); - } + return parent::getName(); + } - private function removeOutgoinglink($html) { - foreach ($html->find('a') as $a) { - $a->href = urldecode(preg_replace($this->outgoingRegex, '', $a->href)); - } + private function removeOutgoinglink($html) + { + foreach ($html->find('a') as $a) { + $a->href = urldecode(preg_replace($this->outgoingRegex, '', $a->href)); + } - return $html->innertext; - } + return $html->innertext; + } } diff --git a/bridges/FirstLookMediaTechBridge.php b/bridges/FirstLookMediaTechBridge.php index 67d0ead1..f9963c6f 100644 --- a/bridges/FirstLookMediaTechBridge.php +++ b/bridges/FirstLookMediaTechBridge.php @@ -1,49 +1,52 @@ <?php -class FirstLookMediaTechBridge extends BridgeAbstract { - const NAME = 'First Look Media - Technology'; - const URI = 'https://tech.firstlook.media'; - const DESCRIPTION = 'First Look Media Technology page'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - array( - 'projects' => array( - 'type' => 'checkbox', - 'name' => 'Include Projects?', - ) - ) - ); - - public function collectData() { - $html = getSimpleHTMLDOM(self::URI); - - if ($this->getInput('projects')) { - $top_projects = $html->find('.PromoList-ul', 0); - foreach($top_projects->find('li.PromoList-item') as $element) { - $item = array(); - - $item_uri = $element->find('a', 0); - $item['uri'] = $item_uri->href; - $item['title'] = strip_tags($item_uri->innertext); - $item['content'] = $element->find('div > div', 0); - - $this->items[] = $item; - } - } - - $top_articles = $html->find('.PromoList-ul', 1); - foreach($top_articles->find('li.PromoList-item') as $element) { - $item = array(); - - $item_left = $element->find('div > div', 0); - $item_date = $element->find('.PromoList-date', 0); - $item['timestamp'] = strtotime($item_date->innertext); - $item_date->outertext = ''; /* Remove */ - $item['author'] = $item_left->innertext; - $item_uri = $element->find('a', 0); - $item['uri'] = self::URI . $item_uri->href; - $item['title'] = strip_tags($item_uri); - - $this->items[] = $item; - } - } + +class FirstLookMediaTechBridge extends BridgeAbstract +{ + const NAME = 'First Look Media - Technology'; + const URI = 'https://tech.firstlook.media'; + const DESCRIPTION = 'First Look Media Technology page'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + [ + 'projects' => [ + 'type' => 'checkbox', + 'name' => 'Include Projects?', + ] + ] + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); + + if ($this->getInput('projects')) { + $top_projects = $html->find('.PromoList-ul', 0); + foreach ($top_projects->find('li.PromoList-item') as $element) { + $item = []; + + $item_uri = $element->find('a', 0); + $item['uri'] = $item_uri->href; + $item['title'] = strip_tags($item_uri->innertext); + $item['content'] = $element->find('div > div', 0); + + $this->items[] = $item; + } + } + + $top_articles = $html->find('.PromoList-ul', 1); + foreach ($top_articles->find('li.PromoList-item') as $element) { + $item = []; + + $item_left = $element->find('div > div', 0); + $item_date = $element->find('.PromoList-date', 0); + $item['timestamp'] = strtotime($item_date->innertext); + $item_date->outertext = ''; /* Remove */ + $item['author'] = $item_left->innertext; + $item_uri = $element->find('a', 0); + $item['uri'] = self::URI . $item_uri->href; + $item['title'] = strip_tags($item_uri); + + $this->items[] = $item; + } + } } diff --git a/bridges/FlashbackBridge.php b/bridges/FlashbackBridge.php index 3d64f852..f3bff5ff 100644 --- a/bridges/FlashbackBridge.php +++ b/bridges/FlashbackBridge.php @@ -2,184 +2,190 @@ class FlashbackBridge extends BridgeAbstract { - const MAINTAINER = 'fatuus'; - const NAME = 'Flashback forum'; - const URI = 'https://www.flashback.org'; - const DESCRIPTION = 'Returns post from forum'; - const CACHE_TIMEOUT = 10800; // 3h + const MAINTAINER = 'fatuus'; + const NAME = 'Flashback forum'; + const URI = 'https://www.flashback.org'; + const DESCRIPTION = 'Returns post from forum'; + const CACHE_TIMEOUT = 10800; // 3h - const PARAMETERS = array( - 'Category' => array( - 'c' => array( - 'name' => 'Category number', - 'type' => 'number', - 'exampleValue' => '249', - 'required' => true - ) - ), - 'Tag' => array( - 'a' => array( - 'name' => 'Tag', - 'type' => 'text', - 'exampleValue' => 'stockholm', - 'required' => true - ) - ), - 'Thread' => array( - 't' => array( - 'name' => 'Thread number', - 'type' => 'number', - 'exampleValue' => '1420554', - 'required' => true - ) - ), - /*'User' => array( - 'u' => array( - 'name' => 'User number', - 'type' => 'text', - 'exampleValue' => 'not working, need login', - 'required' => true - ) - ),*/ - 'Search string' => array( - 's' => array( - 'name' => 'Words', - 'type' => 'text', - 'exampleValue' => 'sök', - 'required' => true - ), - 'type' => array( - 'name' => 'Type of search', - 'type' => 'list', - 'defaultValue' => 'Posts', - 'values' => array( - 'Posts' => 'posts', - 'Subjects' => 'subjects' - ) - ) - ) - ); + const PARAMETERS = [ + 'Category' => [ + 'c' => [ + 'name' => 'Category number', + 'type' => 'number', + 'exampleValue' => '249', + 'required' => true + ] + ], + 'Tag' => [ + 'a' => [ + 'name' => 'Tag', + 'type' => 'text', + 'exampleValue' => 'stockholm', + 'required' => true + ] + ], + 'Thread' => [ + 't' => [ + 'name' => 'Thread number', + 'type' => 'number', + 'exampleValue' => '1420554', + 'required' => true + ] + ], + /*'User' => array( + 'u' => array( + 'name' => 'User number', + 'type' => 'text', + 'exampleValue' => 'not working, need login', + 'required' => true + ) + ),*/ + 'Search string' => [ + 's' => [ + 'name' => 'Words', + 'type' => 'text', + 'exampleValue' => 'sök', + 'required' => true + ], + 'type' => [ + 'name' => 'Type of search', + 'type' => 'list', + 'defaultValue' => 'Posts', + 'values' => [ + 'Posts' => 'posts', + 'Subjects' => 'subjects' + ] + ] + ] + ]; - public function getName() - { - if ($this->getInput('c')) { - $category = $this->getInput('c'); - return 'Category ' . $category . ' - Flashback'; - } elseif ($this->getInput('a')) { - $tag = $this->getInput('a'); - return 'Tag: ' . $tag . ' - Flashback'; - } elseif ($this->getInput('t')) { - $thread = $this->getInput('t'); - return 'Thread ' . $thread . ' - Flashback'; - } elseif ($this->getInput('u')) { - $user = $this->getInput('u'); - return 'User ' . $user . ' - Flashback'; - } elseif ($this->getInput('s')) { - $search = $this->getInput('s'); - return 'Search: ' . $search . ' - Flashback'; - } + public function getName() + { + if ($this->getInput('c')) { + $category = $this->getInput('c'); + return 'Category ' . $category . ' - Flashback'; + } elseif ($this->getInput('a')) { + $tag = $this->getInput('a'); + return 'Tag: ' . $tag . ' - Flashback'; + } elseif ($this->getInput('t')) { + $thread = $this->getInput('t'); + return 'Thread ' . $thread . ' - Flashback'; + } elseif ($this->getInput('u')) { + $user = $this->getInput('u'); + return 'User ' . $user . ' - Flashback'; + } elseif ($this->getInput('s')) { + $search = $this->getInput('s'); + return 'Search: ' . $search . ' - Flashback'; + } - return self::NAME; - } + return self::NAME; + } - public function collectData() - { - if ($this->getInput('c')) { - $page = self::URI . '/f' . $this->getInput('c'); - } elseif ($this->getInput('a')) { - $page = self::URI . '/find_threads_by_tag.php?tag=' . $this->getInput('a'); - } elseif ($this->getInput('t')) { - $page = self::URI . '/t' . $this->getInput('t'); - $page = $page . 's'; // last-page - } elseif ($this->getInput('u')) { - $page = self::URI . '/find_posts_by_user.php?userid=' . $this->getInput('u'); - } elseif ($this->getInput('s')) { - if ($this->getInput('type') == 'posts') { - $page = self::URI . '/sok/?query=' . $this->getInput('s') . '&search_post=1&sp=1&so=pd'; - } else { - $page = self::URI . '/sok/?query=' . $this->getInput('s') . '&search_post=0&sp=1&so=pd'; - } - } + public function collectData() + { + if ($this->getInput('c')) { + $page = self::URI . '/f' . $this->getInput('c'); + } elseif ($this->getInput('a')) { + $page = self::URI . '/find_threads_by_tag.php?tag=' . $this->getInput('a'); + } elseif ($this->getInput('t')) { + $page = self::URI . '/t' . $this->getInput('t'); + $page = $page . 's'; // last-page + } elseif ($this->getInput('u')) { + $page = self::URI . '/find_posts_by_user.php?userid=' . $this->getInput('u'); + } elseif ($this->getInput('s')) { + if ($this->getInput('type') == 'posts') { + $page = self::URI . '/sok/?query=' . $this->getInput('s') . '&search_post=1&sp=1&so=pd'; + } else { + $page = self::URI . '/sok/?query=' . $this->getInput('s') . '&search_post=0&sp=1&so=pd'; + } + } - $html = getSimpleHTMLDOM($page); + $html = getSimpleHTMLDOM($page); - if ($this->getInput('c') || $this->getInput('a')) { - $category = $this->getInput('c'); - $array = $html->find('table#threadslist tbody tr'); - foreach ($array as $key => $element) { - $item = array(); - $item['uri'] = self::URI . $element->find('td.td_title a', 0)->href; - $item['title'] = trim(utf8_encode($element->find('td.td_title a', 0)->innertext)); - $item['author'] = trim(utf8_encode( - $element->find('td.td_title span.thread-poster span', 0)->innertext) - ); - $timestamp = $element->find('td.td_last_post div', 0); - if (isset($timestamp->plaintext)) { - $item['timestamp'] = strtotime(str_replace(array('Igår', 'Idag'), - array('yesterday', 'today'), trim($timestamp->plaintext))); - } - $item['content'] = $item['title'] . '<br />' . trim(preg_replace('/\t+/', '', - $element->find('td.td_replies', 0)->innertext)); - $item['uid'] = preg_split('/(\/)/', $element->find('td.td_title a', 0)->href)[1]; - $this->items[] = $item; - } - } elseif ($this->getInput('t')) { - $tags = $html->find('div.hidden-xs a.tag'); - $array = $html->find('div.post'); + if ($this->getInput('c') || $this->getInput('a')) { + $category = $this->getInput('c'); + $array = $html->find('table#threadslist tbody tr'); + foreach ($array as $key => $element) { + $item = []; + $item['uri'] = self::URI . $element->find('td.td_title a', 0)->href; + $item['title'] = trim(utf8_encode($element->find('td.td_title a', 0)->innertext)); + $item['author'] = trim(utf8_encode( + $element->find('td.td_title span.thread-poster span', 0)->innertext + )); + $timestamp = $element->find('td.td_last_post div', 0); + if (isset($timestamp->plaintext)) { + $item['timestamp'] = strtotime(str_replace( + ['Igår', 'Idag'], + ['yesterday', 'today'], + trim($timestamp->plaintext) + )); + } + $item['content'] = $item['title'] . '<br />' . trim(preg_replace( + '/\t+/', + '', + $element->find('td.td_replies', 0)->innertext + )); + $item['uid'] = preg_split('/(\/)/', $element->find('td.td_title a', 0)->href)[1]; + $this->items[] = $item; + } + } elseif ($this->getInput('t')) { + $tags = $html->find('div.hidden-xs a.tag'); + $array = $html->find('div.post'); - foreach ($array as $key => $element) { - $item = array(); - $item['uri_post'] = self::URI . $element->find('div.post-heading a', 2)->href; - $item['uri'] = self::URI . '/' . preg_split('/(\/s)/', $item['uri_post'])[1] . '#' . - preg_split('/(\/s)/', $item['uri_post'])[1]; - $item['uri_thread'] = $page; - $item['author'] = utf8_encode($element->find('div.post-user ul li', 0)->innertext); - $item['author_link'] = self::URI . $element->find('div.post-user ul li a', 0)->href; - $item['post_nr'] = $element->find('div.post-heading a strong', 0)->innertext; - $item['timestamp'] = strtotime( - str_replace( - array('Igår', 'Idag'), array('yesterday', 'today'), - current(explode("\t", str_replace("\t\t", "\t", trim( - $element->find('div.post-heading', 0)->plaintext) - ))) - ) - ); - if ($element->find('div.smallfont strong', 0)) { - $item['title'] = trim(utf8_encode($element->find('div.smallfont strong', 0)->innertext)); - } - if (empty($item['title'])) { - $item['title'] = date('D j M y H:i', $item['timestamp']); - } - $item['content'] = trim(preg_replace('/\t+/', '', $element->find('div.post_message', 0))); - $item['uid'] = preg_split('/(\#|\/)/', $element->find('div.post-heading a', 2)->href)[1]; - foreach ($tags as $tag_key => $tag) { - $item['categories'][] = trim(utf8_encode($tag->innertext)); - } - $this->items[] = $item; - } - // } elseif ( $this->getInput('u') ) { + foreach ($array as $key => $element) { + $item = []; + $item['uri_post'] = self::URI . $element->find('div.post-heading a', 2)->href; + $item['uri'] = self::URI . '/' . preg_split('/(\/s)/', $item['uri_post'])[1] . '#' . + preg_split('/(\/s)/', $item['uri_post'])[1]; + $item['uri_thread'] = $page; + $item['author'] = utf8_encode($element->find('div.post-user ul li', 0)->innertext); + $item['author_link'] = self::URI . $element->find('div.post-user ul li a', 0)->href; + $item['post_nr'] = $element->find('div.post-heading a strong', 0)->innertext; + $item['timestamp'] = strtotime( + str_replace( + ['Igår', 'Idag'], + ['yesterday', 'today'], + current(explode("\t", str_replace("\t\t", "\t", trim( + $element->find('div.post-heading', 0)->plaintext + )))) + ) + ); + if ($element->find('div.smallfont strong', 0)) { + $item['title'] = trim(utf8_encode($element->find('div.smallfont strong', 0)->innertext)); + } + if (empty($item['title'])) { + $item['title'] = date('D j M y H:i', $item['timestamp']); + } + $item['content'] = trim(preg_replace('/\t+/', '', $element->find('div.post_message', 0))); + $item['uid'] = preg_split('/(\#|\/)/', $element->find('div.post-heading a', 2)->href)[1]; + foreach ($tags as $tag_key => $tag) { + $item['categories'][] = trim(utf8_encode($tag->innertext)); + } + $this->items[] = $item; + } + // } elseif ( $this->getInput('u') ) { + } elseif ($this->getInput('s')) { + $array = $html->find('div.post'); + foreach ($array as $key => $element) { + $item = []; + $item['uri'] = self::URI . $element->find('div.post-body a', 0)->href; + $item['uri_thread'] = $page . $element->find('div.post-heading a', 0)->href . 's'; + $item['author'] = $element->find('div.post-body a', 1)->innertext; + $item['author_link'] = self::URI . $element->find('div.post-body a', 1)->href; + $time = preg_split('/(\>)/', $element->find('div.post-heading', 0)->innertext); + $item['timestamp'] = strtotime(trim(end($time))); + $item['title'] = trim(utf8_encode($element->find('div.post-body strong', 0)->innertext)); + if (empty($item['title'])) { + $item['title'] = date('D j M y H:i', $item['timestamp']); + } - } elseif ($this->getInput('s')) { - $array = $html->find('div.post'); - foreach ($array as $key => $element) { - $item = array(); - $item['uri'] = self::URI . $element->find('div.post-body a', 0)->href; - $item['uri_thread'] = $page . $element->find('div.post-heading a', 0)->href . 's'; - $item['author'] = $element->find('div.post-body a', 1)->innertext; - $item['author_link'] = self::URI . $element->find('div.post-body a', 1)->href; - $time = preg_split('/(\>)/', $element->find('div.post-heading', 0)->innertext); - $item['timestamp'] = strtotime(trim(end($time))); - $item['title'] = trim(utf8_encode($element->find('div.post-body strong', 0)->innertext)); - if (empty($item['title'])) { - $item['title'] = date('D j M y H:i', $item['timestamp']); - } - - $item['datetime'] = (trim(end($time))); - $item['categories'][] = trim(utf8_encode($element->find('div.post-heading a', 0)->innertext)); - $item['content'] = trim(preg_replace('/\t+/', '', $element->find('div.post_message', 0))); - $item['uid'] = preg_split('/(\#|\/)/', $element->find('div.post-body a', 0)->href)[1]; - $this->items[] = $item; - } - } - } + $item['datetime'] = (trim(end($time))); + $item['categories'][] = trim(utf8_encode($element->find('div.post-heading a', 0)->innertext)); + $item['content'] = trim(preg_replace('/\t+/', '', $element->find('div.post_message', 0))); + $item['uid'] = preg_split('/(\#|\/)/', $element->find('div.post-body a', 0)->href)[1]; + $this->items[] = $item; + } + } + } } diff --git a/bridges/FlickrBridge.php b/bridges/FlickrBridge.php index 39351ea4..99cd811d 100644 --- a/bridges/FlickrBridge.php +++ b/bridges/FlickrBridge.php @@ -3,290 +3,276 @@ /* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge * by erwang, providing the functionality of both in one. */ -class FlickrBridge extends BridgeAbstract { - - const MAINTAINER = 'logmanoriginal'; - const NAME = 'Flickr Bridge'; - const URI = 'https://www.flickr.com/'; - const CACHE_TIMEOUT = 21600; // 6 hours - const DESCRIPTION = 'Returns images from Flickr'; - - const PARAMETERS = array( - 'Explore' => array(), - 'By keyword' => array( - 'q' => array( - 'name' => 'Keyword', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert keyword', - 'exampleValue' => 'bird' - ), - 'media' => array( - 'name' => 'Media', - 'type' => 'list', - 'values' => array( - 'All (Photos & videos)' => 'all', - 'Photos' => 'photos', - 'Videos' => 'videos', - ), - 'defaultValue' => 'all', - ), - 'sort' => array( - 'name' => 'Sort By', - 'type' => 'list', - 'values' => array( - 'Relevance' => 'relevance', - 'Date uploaded' => 'date-posted-desc', - 'Date taken' => 'date-taken-desc', - 'Interesting' => 'interestingness-desc', - ), - 'defaultValue' => 'relevance', - ) - ), - 'By username' => array( - 'u' => array( - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert username (as shown in the address bar)', - 'exampleValue' => 'flickr' - ), - 'content' => array( - 'name' => 'Content', - 'type' => 'list', - 'values' => array( - 'Uploads' => 'uploads', - 'Favorites' => 'faves', - ), - 'defaultValue' => 'uploads', - ), - 'media' => array( - 'name' => 'Media', - 'type' => 'list', - 'values' => array( - 'All (Photos & videos)' => 'all', - 'Photos' => 'photos', - 'Videos' => 'videos', - ), - 'defaultValue' => 'all', - ), - 'sort' => array( - 'name' => 'Sort By', - 'type' => 'list', - 'values' => array( - 'Relevance' => 'relevance', - 'Date uploaded' => 'date-posted-desc', - 'Date taken' => 'date-taken-desc', - 'Interesting' => 'interestingness-desc', - ), - 'defaultValue' => 'date-posted-desc', - ) - ) - ); - - private $username = ''; - - public function collectData() { - - switch($this->queriedContext) { - - case 'Explore': - $filter = 'photo-lite-models'; - $html = getSimpleHTMLDOM($this->getURI()); - break; - - case 'By keyword': - $filter = 'photo-lite-models'; - $html = getSimpleHTMLDOM($this->getURI()); - break; - - case 'By username': - //$filter = 'photo-models'; - $filter = 'photo-lite-models'; - $html = getSimpleHTMLDOM($this->getURI()); - - $this->username = $this->getInput('u'); - - if ($html->find('span.search-pill-name', 0)) { - $this->username = $html->find('span.search-pill-name', 0)->plaintext; - } - break; - - default: - returnClientError('Invalid context: ' . $this->queriedContext); - - } - - $model_json = $this->extractJsonModel($html); - $photo_models = $this->getPhotoModels($model_json, $filter); - - foreach($photo_models as $model) { - $item = array(); - - /* Author name depends on scope. On a keyword search the - * author is part of the picture data. On a username search - * the author is part of the owner data. - */ - if(array_key_exists('username', $model)) { - $item['author'] = urldecode($model['username']); - } elseif (array_key_exists('owner', reset($model_json)[0])) { - $item['author'] = urldecode(reset($model_json)[0]['owner']['username']); - } - - $item['title'] = urldecode((array_key_exists('title', $model) ? $model['title'] : 'Untitled')); - $item['uri'] = self::URI . 'photo.gne?id=' . $model['id']; - - $description = (array_key_exists('description', $model) ? $model['description'] : ''); - - $item['content'] = '<a href="' - . $item['uri'] - . '"><img src="' - . $this->extractContentImage($model) - . '" style="max-width: 640px; max-height: 480px;"/></a><br><p>' - . urldecode($description) - . '</p>'; - - $item['enclosures'] = $this->extractEnclosures($model); - - $this->items[] = $item; - - } - - } - - public function getURI() { - - switch($this->queriedContext) { - case 'Explore': - return self::URI . 'explore'; - break; - case 'By keyword': - return self::URI . 'search/?q=' . urlencode($this->getInput('q')) - . '&sort=' . $this->getInput('sort') . '&media=' . $this->getInput('media'); - break; - case 'By username': - $uri = self::URI . 'search/?user_id=' . urlencode($this->getInput('u')) - . '&sort=date-posted-desc&media=' . $this->getInput('media'); - - if ($this->getInput('content') === 'faves') { - return $uri . '&faves=1'; - } - - return $uri; - break; - - default: - return parent::getURI(); - } - } - - public function getName() { - - switch($this->queriedContext) { - case 'Explore': - return 'Explore - ' . self::NAME; - break; - case 'By keyword': - return $this->getInput('q') . ' - keyword - ' . self::NAME; - break; - case 'By username': - - if ($this->getInput('content') === 'faves') { - return $this->username . ' - favorites - ' . self::NAME; - } - - return $this->username . ' - ' . self::NAME; - break; - - default: - return parent::getName(); - } - - return parent::getName(); - } - - private function extractJsonModel($html) { - - // Find SCRIPT containing JSON data - $model = $html->find('.modelExport', 0); - $model_text = $model->innertext; - - // Find start and end of JSON data - $start = strpos($model_text, 'modelExport:') + strlen('modelExport:'); - $end = strpos($model_text, 'auth:') - strlen('auth:'); - - // Extract JSON data, remove trailing comma - $model_text = trim(substr($model_text, $start, $end - $start)); - $model_text = substr($model_text, 0, strlen($model_text) - 1); - - return json_decode($model_text, true); - - } - - private function getPhotoModels($json, $filter) { - - // The JSON model contains a "legend" array, where each element contains - // the path to an element in the "main" object - $photo_models = array(); - - foreach($json['legend'] as $legend) { - - $photo_model = $json['main']; - - foreach($legend as $element) { // Traverse tree - $photo_model = $photo_model[$element]; - } - - // We are only interested in content - if($photo_model['_flickrModelRegistry'] === $filter) { - $photo_models[] = $photo_model; - } - - } - - return $photo_models; - - } - - private function extractEnclosures($model) { - - $areas = array(); - - foreach($model['sizes'] as $size) { - $areas[$size['width'] * $size['height']] = $size['url']; - } - - return array($this->fixURL(max($areas))); - - } - - private function extractContentImage($model) { - - $areas = array(); - $limit = 320 * 240; - - foreach($model['sizes'] as $size) { - - $image_area = $size['width'] * $size['height']; - - if($image_area >= $limit) { - $areas[$image_area] = $size['url']; - } - - } - - return $this->fixURL(min($areas)); - - } - - private function fixURL($url) { - - // For some reason the image URLs don't include the protocol (https) - if(strpos($url, '//') === 0) { - $url = 'https:' . $url; - } - - return $url; - - } +class FlickrBridge extends BridgeAbstract +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'Flickr Bridge'; + const URI = 'https://www.flickr.com/'; + const CACHE_TIMEOUT = 21600; // 6 hours + const DESCRIPTION = 'Returns images from Flickr'; + + const PARAMETERS = [ + 'Explore' => [], + 'By keyword' => [ + 'q' => [ + 'name' => 'Keyword', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert keyword', + 'exampleValue' => 'bird' + ], + 'media' => [ + 'name' => 'Media', + 'type' => 'list', + 'values' => [ + 'All (Photos & videos)' => 'all', + 'Photos' => 'photos', + 'Videos' => 'videos', + ], + 'defaultValue' => 'all', + ], + 'sort' => [ + 'name' => 'Sort By', + 'type' => 'list', + 'values' => [ + 'Relevance' => 'relevance', + 'Date uploaded' => 'date-posted-desc', + 'Date taken' => 'date-taken-desc', + 'Interesting' => 'interestingness-desc', + ], + 'defaultValue' => 'relevance', + ] + ], + 'By username' => [ + 'u' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert username (as shown in the address bar)', + 'exampleValue' => 'flickr' + ], + 'content' => [ + 'name' => 'Content', + 'type' => 'list', + 'values' => [ + 'Uploads' => 'uploads', + 'Favorites' => 'faves', + ], + 'defaultValue' => 'uploads', + ], + 'media' => [ + 'name' => 'Media', + 'type' => 'list', + 'values' => [ + 'All (Photos & videos)' => 'all', + 'Photos' => 'photos', + 'Videos' => 'videos', + ], + 'defaultValue' => 'all', + ], + 'sort' => [ + 'name' => 'Sort By', + 'type' => 'list', + 'values' => [ + 'Relevance' => 'relevance', + 'Date uploaded' => 'date-posted-desc', + 'Date taken' => 'date-taken-desc', + 'Interesting' => 'interestingness-desc', + ], + 'defaultValue' => 'date-posted-desc', + ] + ] + ]; + + private $username = ''; + + public function collectData() + { + switch ($this->queriedContext) { + case 'Explore': + $filter = 'photo-lite-models'; + $html = getSimpleHTMLDOM($this->getURI()); + break; + + case 'By keyword': + $filter = 'photo-lite-models'; + $html = getSimpleHTMLDOM($this->getURI()); + break; + + case 'By username': + //$filter = 'photo-models'; + $filter = 'photo-lite-models'; + $html = getSimpleHTMLDOM($this->getURI()); + + $this->username = $this->getInput('u'); + + if ($html->find('span.search-pill-name', 0)) { + $this->username = $html->find('span.search-pill-name', 0)->plaintext; + } + break; + + default: + returnClientError('Invalid context: ' . $this->queriedContext); + } + + $model_json = $this->extractJsonModel($html); + $photo_models = $this->getPhotoModels($model_json, $filter); + + foreach ($photo_models as $model) { + $item = []; + + /* Author name depends on scope. On a keyword search the + * author is part of the picture data. On a username search + * the author is part of the owner data. + */ + if (array_key_exists('username', $model)) { + $item['author'] = urldecode($model['username']); + } elseif (array_key_exists('owner', reset($model_json)[0])) { + $item['author'] = urldecode(reset($model_json)[0]['owner']['username']); + } + + $item['title'] = urldecode((array_key_exists('title', $model) ? $model['title'] : 'Untitled')); + $item['uri'] = self::URI . 'photo.gne?id=' . $model['id']; + + $description = (array_key_exists('description', $model) ? $model['description'] : ''); + + $item['content'] = '<a href="' + . $item['uri'] + . '"><img src="' + . $this->extractContentImage($model) + . '" style="max-width: 640px; max-height: 480px;"/></a><br><p>' + . urldecode($description) + . '</p>'; + + $item['enclosures'] = $this->extractEnclosures($model); + + $this->items[] = $item; + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Explore': + return self::URI . 'explore'; + break; + case 'By keyword': + return self::URI . 'search/?q=' . urlencode($this->getInput('q')) + . '&sort=' . $this->getInput('sort') . '&media=' . $this->getInput('media'); + break; + case 'By username': + $uri = self::URI . 'search/?user_id=' . urlencode($this->getInput('u')) + . '&sort=date-posted-desc&media=' . $this->getInput('media'); + + if ($this->getInput('content') === 'faves') { + return $uri . '&faves=1'; + } + + return $uri; + break; + + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Explore': + return 'Explore - ' . self::NAME; + break; + case 'By keyword': + return $this->getInput('q') . ' - keyword - ' . self::NAME; + break; + case 'By username': + if ($this->getInput('content') === 'faves') { + return $this->username . ' - favorites - ' . self::NAME; + } + + return $this->username . ' - ' . self::NAME; + break; + + default: + return parent::getName(); + } + + return parent::getName(); + } + + private function extractJsonModel($html) + { + // Find SCRIPT containing JSON data + $model = $html->find('.modelExport', 0); + $model_text = $model->innertext; + + // Find start and end of JSON data + $start = strpos($model_text, 'modelExport:') + strlen('modelExport:'); + $end = strpos($model_text, 'auth:') - strlen('auth:'); + + // Extract JSON data, remove trailing comma + $model_text = trim(substr($model_text, $start, $end - $start)); + $model_text = substr($model_text, 0, strlen($model_text) - 1); + + return json_decode($model_text, true); + } + + private function getPhotoModels($json, $filter) + { + // The JSON model contains a "legend" array, where each element contains + // the path to an element in the "main" object + $photo_models = []; + + foreach ($json['legend'] as $legend) { + $photo_model = $json['main']; + + foreach ($legend as $element) { // Traverse tree + $photo_model = $photo_model[$element]; + } + + // We are only interested in content + if ($photo_model['_flickrModelRegistry'] === $filter) { + $photo_models[] = $photo_model; + } + } + + return $photo_models; + } + + private function extractEnclosures($model) + { + $areas = []; + + foreach ($model['sizes'] as $size) { + $areas[$size['width'] * $size['height']] = $size['url']; + } + + return [$this->fixURL(max($areas))]; + } + + private function extractContentImage($model) + { + $areas = []; + $limit = 320 * 240; + + foreach ($model['sizes'] as $size) { + $image_area = $size['width'] * $size['height']; + + if ($image_area >= $limit) { + $areas[$image_area] = $size['url']; + } + } + + return $this->fixURL(min($areas)); + } + + private function fixURL($url) + { + // For some reason the image URLs don't include the protocol (https) + if (strpos($url, '//') === 0) { + $url = 'https:' . $url; + } + + return $url; + } } diff --git a/bridges/FolhaDeSaoPauloBridge.php b/bridges/FolhaDeSaoPauloBridge.php index 6506fdba..d8d93c4f 100644 --- a/bridges/FolhaDeSaoPauloBridge.php +++ b/bridges/FolhaDeSaoPauloBridge.php @@ -1,69 +1,73 @@ <?php -class FolhaDeSaoPauloBridge extends FeedExpander { - const MAINTAINER = 'somini'; - const NAME = 'Folha de São Paulo'; - const URI = 'https://www1.folha.uol.com.br'; - const DESCRIPTION = 'Returns the newest posts from Folha de São Paulo (full text)'; - const PARAMETERS = array( - array( - 'feed' => array( - 'name' => 'Feed sub-URL', - 'type' => 'text', - 'required' => true, - 'title' => 'Select the sub-feed (see https://www1.folha.uol.com.br/feed/)', - 'exampleValue' => 'emcimadahora/rss091.xml', - ), - 'amount' => array( - 'name' => 'Amount of items to fetch', - 'type' => 'number', - 'defaultValue' => 15, - ), - 'deep_crawl' => array( - 'name' => 'Deep Crawl', - 'description' => 'Crawl each item "deeply", that is, return the article contents', - 'type' => 'checkbox', - 'defaultValue' => true, - ), - ) - ); - protected function parseItem($item){ - $item = parent::parseItem($item); +class FolhaDeSaoPauloBridge extends FeedExpander +{ + const MAINTAINER = 'somini'; + const NAME = 'Folha de São Paulo'; + const URI = 'https://www1.folha.uol.com.br'; + const DESCRIPTION = 'Returns the newest posts from Folha de São Paulo (full text)'; + const PARAMETERS = [ + [ + 'feed' => [ + 'name' => 'Feed sub-URL', + 'type' => 'text', + 'required' => true, + 'title' => 'Select the sub-feed (see https://www1.folha.uol.com.br/feed/)', + 'exampleValue' => 'emcimadahora/rss091.xml', + ], + 'amount' => [ + 'name' => 'Amount of items to fetch', + 'type' => 'number', + 'defaultValue' => 15, + ], + 'deep_crawl' => [ + 'name' => 'Deep Crawl', + 'description' => 'Crawl each item "deeply", that is, return the article contents', + 'type' => 'checkbox', + 'defaultValue' => true, + ], + ] + ]; - if ($this->getInput('deep_crawl')) { - $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']); - if($articleHTMLContent) { - foreach ($articleHTMLContent->find('div.c-news__body .is-hidden') as $toRemove) { - $toRemove->innertext = ''; - } - $item_content = $articleHTMLContent->find('div.c-news__body', 0); - if ($item_content) { - $text = $item_content->innertext; - $text = strip_tags($text, '<p><b><a><blockquote><figure><figcaption><img><strong><em><ul><li>'); - $item['content'] = $text; - $item['uri'] = explode('*', $item['uri'])[1]; - } - } else { - Debug::log('???: ' . $item['uri']); - } - } else { - $item['uri'] = explode('*', $item['uri'])[1]; - } + protected function parseItem($item) + { + $item = parent::parseItem($item); - return $item; - } + if ($this->getInput('deep_crawl')) { + $articleHTMLContent = getSimpleHTMLDOMCached($item['uri']); + if ($articleHTMLContent) { + foreach ($articleHTMLContent->find('div.c-news__body .is-hidden') as $toRemove) { + $toRemove->innertext = ''; + } + $item_content = $articleHTMLContent->find('div.c-news__body', 0); + if ($item_content) { + $text = $item_content->innertext; + $text = strip_tags($text, '<p><b><a><blockquote><figure><figcaption><img><strong><em><ul><li>'); + $item['content'] = $text; + $item['uri'] = explode('*', $item['uri'])[1]; + } + } else { + Debug::log('???: ' . $item['uri']); + } + } else { + $item['uri'] = explode('*', $item['uri'])[1]; + } - public function collectData(){ - $feed_input = $this->getInput('feed'); - if (substr($feed_input, 0, strlen(self::URI)) === self::URI) { - Debug::log('Input:: ' . $feed_input); - $feed_url = $feed_input; - } else { - /* TODO: prepend `/` if missing */ - $feed_url = self::URI . '/' . $this->getInput('feed'); - } - Debug::log('URL: ' . $feed_url); - $limit = $this->getInput('amount'); - $this->collectExpandableDatas($feed_url, $limit); - } + return $item; + } + + public function collectData() + { + $feed_input = $this->getInput('feed'); + if (substr($feed_input, 0, strlen(self::URI)) === self::URI) { + Debug::log('Input:: ' . $feed_input); + $feed_url = $feed_input; + } else { + /* TODO: prepend `/` if missing */ + $feed_url = self::URI . '/' . $this->getInput('feed'); + } + Debug::log('URL: ' . $feed_url); + $limit = $this->getInput('amount'); + $this->collectExpandableDatas($feed_url, $limit); + } } diff --git a/bridges/ForGifsBridge.php b/bridges/ForGifsBridge.php index ea599b9b..03848d04 100644 --- a/bridges/ForGifsBridge.php +++ b/bridges/ForGifsBridge.php @@ -1,40 +1,41 @@ <?php -class ForGifsBridge extends FeedExpander { - const MAINTAINER = 'logmanoriginal'; - const NAME = 'forgifs Bridge'; - const URI = 'https://forgifs.com'; - const DESCRIPTION = 'Returns the forgifs feed with actual gifs instead of images'; - - public function collectData() { - $this->collectExpandableDatas('https://forgifs.com/gallery/srss/7'); - } - - protected function parseItem($feedItem) { - - $item = parent::parseItem($feedItem); - - $content = str_get_html($item['content']); - $img = $content->find('img', 0); - $poster = $img->src; - - // The actual gif is the same path but its id must be decremented by one. - // Example: - // http://forgifs.com/gallery/d/279419-2/Reporter-videobombed-shoulder-checks.gif - // http://forgifs.com/gallery/d/279418-2/Reporter-videobombed-shoulder-checks.gif - // Notice how this changes ----------^ - // Now let's extract that number and do some math - // Notice: Technically we could also load the content page but that would - // require unnecessary traffic. As long as it works... - $num = substr($img->src, 29, 6); - $num -= 1; - $img->src = substr_replace($img->src, $num, 29, strlen($num)); - $img->width = 'auto'; - $img->height = 'auto'; - - $item['content'] = $content; - - return $item; - - } +class ForGifsBridge extends FeedExpander +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'forgifs Bridge'; + const URI = 'https://forgifs.com'; + const DESCRIPTION = 'Returns the forgifs feed with actual gifs instead of images'; + + public function collectData() + { + $this->collectExpandableDatas('https://forgifs.com/gallery/srss/7'); + } + + protected function parseItem($feedItem) + { + $item = parent::parseItem($feedItem); + + $content = str_get_html($item['content']); + $img = $content->find('img', 0); + $poster = $img->src; + + // The actual gif is the same path but its id must be decremented by one. + // Example: + // http://forgifs.com/gallery/d/279419-2/Reporter-videobombed-shoulder-checks.gif + // http://forgifs.com/gallery/d/279418-2/Reporter-videobombed-shoulder-checks.gif + // Notice how this changes ----------^ + // Now let's extract that number and do some math + // Notice: Technically we could also load the content page but that would + // require unnecessary traffic. As long as it works... + $num = substr($img->src, 29, 6); + $num -= 1; + $img->src = substr_replace($img->src, $num, 29, strlen($num)); + $img->width = 'auto'; + $img->height = 'auto'; + + $item['content'] = $content; + + return $item; + } } diff --git a/bridges/Formula1Bridge.php b/bridges/Formula1Bridge.php index e34c3411..2adce583 100644 --- a/bridges/Formula1Bridge.php +++ b/bridges/Formula1Bridge.php @@ -1,68 +1,71 @@ <?php -class Formula1Bridge extends BridgeAbstract { - const NAME = 'Formula1 Bridge'; - const URI = 'https://formula1.com/'; - const DESCRIPTION = 'Returns latest official Formula 1 news'; - const MAINTAINER = 'AxorPL'; - const API_KEY = 'qPgPPRJyGCIPxFT3el4MF7thXHyJCzAP'; - const API_URL = 'https://api.formula1.com/v1/editorial/articles?limit=%u'; +class Formula1Bridge extends BridgeAbstract +{ + const NAME = 'Formula1 Bridge'; + const URI = 'https://formula1.com/'; + const DESCRIPTION = 'Returns latest official Formula 1 news'; + const MAINTAINER = 'AxorPL'; - const ARTICLE_AUTHOR = 'Formula 1'; - const ARTICLE_HTML = '<p>%s</p><a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>'; - const ARTICLE_URL = 'https://formula1.com/en/latest/article.%s.%s.html'; + const API_KEY = 'qPgPPRJyGCIPxFT3el4MF7thXHyJCzAP'; + const API_URL = 'https://api.formula1.com/v1/editorial/articles?limit=%u'; - const LIMIT_MIN = 1; - const LIMIT_DEFAULT = 10; - const LIMIT_MAX = 100; + const ARTICLE_AUTHOR = 'Formula 1'; + const ARTICLE_HTML = '<p>%s</p><a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>'; + const ARTICLE_URL = 'https://formula1.com/en/latest/article.%s.%s.html'; - const PARAMETERS = array( - array( - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Number of articles to return', - 'exampleValue' => self::LIMIT_DEFAULT, - 'defaultValue' => self::LIMIT_DEFAULT - ) - ) - ); + const LIMIT_MIN = 1; + const LIMIT_DEFAULT = 10; + const LIMIT_MAX = 100; - public function collectData() { - $limit = $this->getInput('limit') ?: self::LIMIT_DEFAULT; - $limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit)); - $url = sprintf(self::API_URL, $limit); + const PARAMETERS = [ + [ + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Number of articles to return', + 'exampleValue' => self::LIMIT_DEFAULT, + 'defaultValue' => self::LIMIT_DEFAULT + ] + ] + ]; - $json = json_decode(getContents($url, array('apikey: ' . self::API_KEY))); - if(property_exists($json, 'error')) { - returnServerError($json->message); - } - $list = $json->items; + public function collectData() + { + $limit = $this->getInput('limit') ?: self::LIMIT_DEFAULT; + $limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit)); + $url = sprintf(self::API_URL, $limit); - foreach($list as $article) { - if(property_exists($article->thumbnail, 'caption')) { - $caption = $article->thumbnail->caption; - } else { - $caption = $article->thumbnail->image->title; - } + $json = json_decode(getContents($url, ['apikey: ' . self::API_KEY])); + if (property_exists($json, 'error')) { + returnServerError($json->message); + } + $list = $json->items; - $item = array(); - $item['uri'] = sprintf(self::ARTICLE_URL, $article->slug, $article->id); - $item['title'] = $article->title; - $item['timestamp'] = $article->updatedAt; - $item['author'] = self::ARTICLE_AUTHOR; - $item['enclosures'] = array($article->thumbnail->image->url); - $item['uid'] = $article->id; - $item['content'] = sprintf( - self::ARTICLE_HTML, - $article->metaDescription, - $item['uri'], - $item['enclosures'][0], - $caption, - $caption - ); - $this->items[] = $item; - } - } + foreach ($list as $article) { + if (property_exists($article->thumbnail, 'caption')) { + $caption = $article->thumbnail->caption; + } else { + $caption = $article->thumbnail->image->title; + } + + $item = []; + $item['uri'] = sprintf(self::ARTICLE_URL, $article->slug, $article->id); + $item['title'] = $article->title; + $item['timestamp'] = $article->updatedAt; + $item['author'] = self::ARTICLE_AUTHOR; + $item['enclosures'] = [$article->thumbnail->image->url]; + $item['uid'] = $article->id; + $item['content'] = sprintf( + self::ARTICLE_HTML, + $article->metaDescription, + $item['uri'], + $item['enclosures'][0], + $caption, + $caption + ); + $this->items[] = $item; + } + } } diff --git a/bridges/FourchanBridge.php b/bridges/FourchanBridge.php index 4680475e..179ae91f 100644 --- a/bridges/FourchanBridge.php +++ b/bridges/FourchanBridge.php @@ -1,79 +1,82 @@ <?php -class FourchanBridge extends BridgeAbstract { - const MAINTAINER = 'mitsukarenai'; - const NAME = '4chan'; - const URI = 'https://boards.4chan.org/'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Returns posts from the specified thread'; +class FourchanBridge extends BridgeAbstract +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = '4chan'; + const URI = 'https://boards.4chan.org/'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Returns posts from the specified thread'; - const PARAMETERS = array( array( - 'c' => array( - 'name' => 'Thread category', - 'required' => true, - 'exampleValue' => 'po', - ), - 't' => array( - 'name' => 'Thread number', - 'type' => 'number', - 'exampleValue' => '597271', - 'required' => true - ) - )); + const PARAMETERS = [ [ + 'c' => [ + 'name' => 'Thread category', + 'required' => true, + 'exampleValue' => 'po', + ], + 't' => [ + 'name' => 'Thread number', + 'type' => 'number', + 'exampleValue' => '597271', + 'required' => true + ] + ]]; - public function getURI(){ - if(!is_null($this->getInput('c')) && !is_null($this->getInput('t'))) { - return static::URI . $this->getInput('c') . '/thread/' . $this->getInput('t'); - } + public function getURI() + { + if (!is_null($this->getInput('c')) && !is_null($this->getInput('t'))) { + return static::URI . $this->getInput('c') . '/thread/' . $this->getInput('t'); + } - return parent::getURI(); - } + return parent::getURI(); + } - public function collectData(){ + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); - $html = getSimpleHTMLDOM($this->getURI()); + foreach ($html->find('div.postContainer') as $element) { + $item = []; + $item['id'] = $element->find('.post', 0)->getAttribute('id'); + $item['uri'] = $this->getURI() . '#' . $item['id']; + $item['timestamp'] = $element->find('span.dateTime', 0)->getAttribute('data-utc'); + $item['author'] = $element->find('span.name', 0)->plaintext; - foreach($html->find('div.postContainer') as $element) { - $item = array(); - $item['id'] = $element->find('.post', 0)->getAttribute('id'); - $item['uri'] = $this->getURI() . '#' . $item['id']; - $item['timestamp'] = $element->find('span.dateTime', 0)->getAttribute('data-utc'); - $item['author'] = $element->find('span.name', 0)->plaintext; + $file = $element->find('.file', 0); - $file = $element->find('.file', 0); + if (!empty($file)) { + $item['image'] = $element->find('.file a', 0)->href; + $item['imageThumb'] = $element->find('.file img', 0)->src; + if (!isset($item['imageThumb']) and strpos($item['image'], '.swf') !== false) { + $item['imageThumb'] = 'http://i.imgur.com/eO0cxf9.jpg'; + } + } - if(!empty($file)) { - $item['image'] = $element->find('.file a', 0)->href; - $item['imageThumb'] = $element->find('.file img', 0)->src; - if(!isset($item['imageThumb']) and strpos($item['image'], '.swf') !== false) - $item['imageThumb'] = 'http://i.imgur.com/eO0cxf9.jpg'; - } + if (!empty($element->find('span.subject', 0)->innertext)) { + $item['subject'] = $element->find('span.subject', 0)->innertext; + } - if(!empty($element->find('span.subject', 0)->innertext)) { - $item['subject'] = $element->find('span.subject', 0)->innertext; - } + $item['title'] = 'reply ' . $item['id'] . ' | ' . $item['author']; + if (isset($item['subject'])) { + $item['title'] = $item['subject'] . ' - ' . $item['title']; + } - $item['title'] = 'reply ' . $item['id'] . ' | ' . $item['author']; - if(isset($item['subject'])) { - $item['title'] = $item['subject'] . ' - ' . $item['title']; - } + $content = $element->find('.postMessage', 0)->innertext; + $content = str_replace('href="#p', 'href="' . $this->getURI() . '#p', $content); + $item['content'] = '<span id="' . $item['id'] . '">' . $content . '</span>'; - $content = $element->find('.postMessage', 0)->innertext; - $content = str_replace('href="#p', 'href="' . $this->getURI() . '#p', $content); - $item['content'] = '<span id="' . $item['id'] . '">' . $content . '</span>'; - - if(isset($item['image'])) { - $item['content'] = '<a href="' - . $item['image'] - . '"><img alt="' - . $item['id'] - . '" src="' - . $item['imageThumb'] - . '" /></a><br>' - . $item['content']; - } - $this->items[] = $item; - } - $this->items = array_reverse($this->items); - } + if (isset($item['image'])) { + $item['content'] = '<a href="' + . $item['image'] + . '"><img alt="' + . $item['id'] + . '" src="' + . $item['imageThumb'] + . '" /></a><br>' + . $item['content']; + } + $this->items[] = $item; + } + $this->items = array_reverse($this->items); + } } diff --git a/bridges/FreeCodeCampBridge.php b/bridges/FreeCodeCampBridge.php index da0b5c7d..89d8c53a 100644 --- a/bridges/FreeCodeCampBridge.php +++ b/bridges/FreeCodeCampBridge.php @@ -1,27 +1,31 @@ <?php -class FreeCodeCampBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'FreeCodecamp Bridge'; - const URI = 'https://www.freecodecamp.org'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'RSS feed for FreeCodeCamp'; - // Freecodecamp removed their old full content rss feed and replaced it with one liner content. +class FreeCodeCampBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'FreeCodecamp Bridge'; + const URI = 'https://www.freecodecamp.org'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'RSS feed for FreeCodeCamp'; + // Freecodecamp removed their old full content rss feed and replaced it with one liner content. - public function collectData(){ - $this->collectExpandableDatas('https://www.freecodecamp.org/news/rss/', 15); - } + public function collectData() + { + $this->collectExpandableDatas('https://www.freecodecamp.org/news/rss/', 15); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); - // figure contain's the main article image - $article = $articlePage->find('figure', 0); - // the actual article - foreach($articlePage->find('.post-full-content') as $element) - $article = $article . $element; - $item['content'] = $article; - return $item; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + // figure contain's the main article image + $article = $articlePage->find('figure', 0); + // the actual article + foreach ($articlePage->find('.post-full-content') as $element) { + $article = $article . $element; + } + $item['content'] = $article; + return $item; + } } diff --git a/bridges/FunkBridge.php b/bridges/FunkBridge.php index 65a61e74..69be4928 100644 --- a/bridges/FunkBridge.php +++ b/bridges/FunkBridge.php @@ -1,84 +1,90 @@ <?php -class FunkBridge extends BridgeAbstract { - const MAINTAINER = 'µKöff'; - const NAME = 'Funk'; - const URI = 'https://www.funk.net/'; - const DESCRIPTION = 'Videos per channel of German public video-on-demand service Funk'; +class FunkBridge extends BridgeAbstract +{ + const MAINTAINER = 'µKöff'; + const NAME = 'Funk'; + const URI = 'https://www.funk.net/'; + const DESCRIPTION = 'Videos per channel of German public video-on-demand service Funk'; - const PARAMETERS = array( - 'Channel' => array( - 'channel' => array( - 'name' => 'Slug', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'game-two-856' - ), - 'max' => array( - 'name' => 'Maximum', - 'type' => 'number', - 'defaultValue' => '-1' - ) - ) - ); + const PARAMETERS = [ + 'Channel' => [ + 'channel' => [ + 'name' => 'Slug', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'game-two-856' + ], + 'max' => [ + 'name' => 'Maximum', + 'type' => 'number', + 'defaultValue' => '-1' + ] + ] + ]; - public function collectData(){ - switch($this->queriedContext) { - case 'Channel': - $url = static::URI . 'data/videos/byChannelAlias/' . $this->getInput('channel') . '/'; - if(!empty($this->getInput('max')) && $this->getInput('max') >= 0) { - $url .= '?size=' . $this->getInput('max'); - } + public function collectData() + { + switch ($this->queriedContext) { + case 'Channel': + $url = static::URI . 'data/videos/byChannelAlias/' . $this->getInput('channel') . '/'; + if (!empty($this->getInput('max')) && $this->getInput('max') >= 0) { + $url .= '?size=' . $this->getInput('max'); + } - $jsonString = getContents($url) or returnServerError('No contents received!'); - $json = json_decode($jsonString, true); + $jsonString = getContents($url) or returnServerError('No contents received!'); + $json = json_decode($jsonString, true); - foreach($json['list'] as $element) { - $this->items[] = $this->collectArticle($element); - } - break; - default: - returnServerError('Unknown context!'); - } - } + foreach ($json['list'] as $element) { + $this->items[] = $this->collectArticle($element); + } + break; + default: + returnServerError('Unknown context!'); + } + } - private function collectArticle($element) { - $item = array(); - $item['uri'] = static::URI . 'channel/' . $element['channelAlias'] . '/' . $element['alias']; - $item['title'] = $element['title']; - $item['timestamp'] = $element['publicationDate']; - $item['author'] = str_replace('-' . $element['channelId'], '', $element['channelAlias']); - $item['content'] = $element['shortDescription']; - $item['enclosures'] = array( - 'https://www.funk.net/api/v4.0/thumbnails/' . $element['imageLandscape'] - ); - $item['uid'] = $element['entityId']; - return $item; - } + private function collectArticle($element) + { + $item = []; + $item['uri'] = static::URI . 'channel/' . $element['channelAlias'] . '/' . $element['alias']; + $item['title'] = $element['title']; + $item['timestamp'] = $element['publicationDate']; + $item['author'] = str_replace('-' . $element['channelId'], '', $element['channelAlias']); + $item['content'] = $element['shortDescription']; + $item['enclosures'] = [ + 'https://www.funk.net/api/v4.0/thumbnails/' . $element['imageLandscape'] + ]; + $item['uid'] = $element['entityId']; + return $item; + } - public function detectParameters($url) { - $regex = '/^https?:\/\/(?:www\.)?funk\.net\/channel\/([^\/]+).*$/'; - if(preg_match($regex, $url, $urlMatches) > 0) { - return array( - 'channel' => $urlMatches[1] - ); - } else { - return null; - } - } + public function detectParameters($url) + { + $regex = '/^https?:\/\/(?:www\.)?funk\.net\/channel\/([^\/]+).*$/'; + if (preg_match($regex, $url, $urlMatches) > 0) { + return [ + 'channel' => $urlMatches[1] + ]; + } else { + return null; + } + } - public function getIcon() { - return 'https://www.funk.net/img/favicons/favicon-192x192.png'; - } + public function getIcon() + { + return 'https://www.funk.net/img/favicons/favicon-192x192.png'; + } - public function getName(){ - switch($this->queriedContext) { - case 'Channel': - if(!empty($this->getInput('channel'))) { - return $this->getInput('channel'); - } - break; - } - return parent::getName(); - } + public function getName() + { + switch ($this->queriedContext) { + case 'Channel': + if (!empty($this->getInput('channel'))) { + return $this->getInput('channel'); + } + break; + } + return parent::getName(); + } } diff --git a/bridges/FurAffinityBridge.php b/bridges/FurAffinityBridge.php index b5bd3ead..7e1dfd82 100644 --- a/bridges/FurAffinityBridge.php +++ b/bridges/FurAffinityBridge.php @@ -1,903 +1,925 @@ <?php -class FurAffinityBridge extends BridgeAbstract { - const NAME = 'FurAffinity Bridge'; - const URI = 'https://www.furaffinity.net'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Returns posts from various sections of FurAffinity'; - const MAINTAINER = 'Roliga'; - const PARAMETERS = array( - 'Search' => array( - 'q' => array( - 'name' => 'Query', - 'required' => true, - 'exampleValue' => 'dog', - ), - 'rating-general' => array( - 'name' => 'General', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'rating-mature' => array( - 'name' => 'Mature', - 'type' => 'checkbox', - ), - 'rating-adult' => array( - 'name' => 'Adult', - 'type' => 'checkbox', - ), - 'range' => array( - 'name' => 'Time range', - 'type' => 'list', - 'values' => array( - 'A Day' => 'day', - '3 Days' => '3days', - 'A Week' => 'week', - 'A Month' => 'month', - 'All time' => 'all' - ), - 'defaultValue' => 'all' - ), - 'type-art' => array( - 'name' => 'Art', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'type-flash' => array( - 'name' => 'Flash', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'type-photo' => array( - 'name' => 'Photography', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'type-music' => array( - 'name' => 'Music', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'type-story' => array( - 'name' => 'Story', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'type-poetry' => array( - 'name' => 'Poetry', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'mode' => array( - 'name' => 'Match mode', - 'type' => 'list', - 'values' => array( - 'All of the words' => 'all', - 'Any of the words' => 'any', - 'Extended' => 'extended' - ), - 'defaultValue' => 'extended' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 10, - 'title' => 'Limit number of submissions to return. -1 for unlimited.' - ), - 'full' => array( - 'name' => 'Full view', - 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'cache' => array( - 'name' => 'Cache submission pages', - 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ) - ), - 'Browse' => array( - 'cat' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'Visual Art' => array( - 'All' => 1, - 'Artwork (Digital)' => 2, - 'Artwork (Traditional)' => 3, - 'Cellshading' => 4, - 'Crafting' => 5, - 'Designs' => 6, - 'Flash' => 7, - 'Fursuiting' => 8, - 'Icons' => 9, - 'Mosaics' => 10, - 'Photography' => 11, - 'Sculpting' => 12 - ), - 'Readable Art' => array( - 'Story' => 13, - 'Poetry' => 14, - 'Prose' => 15 - ), - 'Audio Art' => array( - 'Music' => 16, - 'Podcasts' => 17 - ), - 'Downloadable' => array( - 'Skins' => 18, - 'Handhelds' => 19, - 'Resources' => 20 - ), - 'Other Stuff' => array( - 'Adoptables' => 21, - 'Auctions' => 22, - 'Contests' => 23, - 'Current Events' => 24, - 'Desktops' => 25, - 'Stockart' => 26, - 'Screenshots' => 27, - 'Scraps' => 28, - 'Wallpaper' => 29, - 'YCH / Sale' => 30, - 'Other' => 31 - ) - ), - 'defaultValue' => 1 - ), - 'atype' => array( - 'name' => 'Type', - 'type' => 'list', - 'values' => array( - 'General Things' => array( - 'All' => 1, - 'Abstract' => 2, - 'Animal related (non-anthro)' => 3, - 'Anime' => 4, - 'Comics' => 5, - 'Doodle' => 6, - 'Fanart' => 7, - 'Fantasy' => 8, - 'Human' => 9, - 'Portraits' => 10, - 'Scenery' => 11, - 'Still Life' => 12, - 'Tutorials' => 13, - 'Miscellaneous' => 14 - ), - 'Fetish / Furry specialty' => array( - 'Baby fur' => 101, - 'Bondage' => 102, - 'Digimon' => 103, - 'Fat Furs' => 104, - 'Fetish Other' => 105, - 'Fursuit' => 106, - 'Gore / Macabre Art' => 119, - 'Hyper' => 107, - 'Inflation' => 108, - 'Macro / Micro' => 109, - 'Muscle' => 110, - 'My Little Pony / Brony' => 111, - 'Paw' => 112, - 'Pokemon' => 113, - 'Pregnancy' => 114, - 'Sonic' => 115, - 'Transformation' => 116, - 'Vore' => 117, - 'Water Sports' => 118, - 'General Furry Art' => 100 - ), - 'Music' => array( - 'Techno' => 201, - 'Trance' => 202, - 'House' => 203, - '90s' => 204, - '80s' => 205, - '70s' => 206, - '60s' => 207, - 'Pre-60s' => 208, - 'Classical' => 209, - 'Game Music' => 210, - 'Rock' => 211, - 'Pop' => 212, - 'Rap' => 213, - 'Industrial' => 214, - 'Other Music' => 200 - ) - ), - 'defaultValue' => 1 - ), - 'species' => array( - 'name' => 'Species', - 'type' => 'list', - 'values' => array( - 'Unspecified / Any' => 1, - 'Amphibian' => array( - 'Frog' => 1001, - 'Newt' => 1002, - 'Salamander' => 1003, - 'Amphibian (Other)' => 1000 - ), - 'Aquatic' => array( - 'Cephalopod' => 2001, - 'Dolphin' => 2002, - 'Fish' => 2005, - 'Porpoise' => 2004, - 'Seal' => 6068, - 'Shark' => 2006, - 'Whale' => 2003, - 'Aquatic (Other)' => 2000 - ), - 'Avian' => array( - 'Corvid' => 3001, - 'Crow' => 3002, - 'Duck' => 3003, - 'Eagle' => 3004, - 'Falcon' => 3005, - 'Goose' => 3006, - 'Gryphon' => 3007, - 'Hawk' => 3008, - 'Owl' => 3009, - 'Phoenix' => 3010, - 'Swan' => 3011, - 'Avian (Other)' => 3000 - ), - 'Bears & Ursines' => array( - 'Bear' => 6002 - ), - 'Camelids' => array( - 'Camel' => 6074, - 'Llama' => 6036 - ), - 'Canines & Lupines' => array( - 'Coyote' => 6008, - 'Doberman' => 6009, - 'Dog' => 6010, - 'Dingo' => 6011, - 'German Shepherd' => 6012, - 'Jackal' => 6013, - 'Husky' => 6014, - 'Wolf' => 6016, - 'Canine (Other)' => 6017 - ), - 'Cervines' => array( - 'Cervine (Other)' => 6018 - ), - 'Cows & Bovines' => array( - 'Antelope' => 6004, - 'Cows' => 6003, - 'Gazelle' => 6005, - 'Goat' => 6006, - 'Bovines (General)' => 6007 - ), - 'Dragons' => array( - 'Eastern Dragon' => 4001, - 'Hydra' => 4002, - 'Serpent' => 4003, - 'Western Dragon' => 4004, - 'Wyvern' => 4005, - 'Dragon (Other)' => 4000 - ), - 'Equestrians' => array( - 'Donkey' => 6019, - 'Horse' => 6034, - 'Pony' => 6073, - 'Zebra' => 6071 - ), - 'Exotic & Mythicals' => array( - 'Argonian' => 5002, - 'Chakat' => 5003, - 'Chocobo' => 5004, - 'Citra' => 5005, - 'Crux' => 5006, - 'Daemon' => 5007, - 'Digimon' => 5008, - 'Dracat' => 5009, - 'Draenei' => 5010, - 'Elf' => 5011, - 'Gargoyle' => 5012, - 'Iksar' => 5013, - 'Kaiju/Monster' => 5015, - 'Langurhali' => 5014, - 'Moogle' => 5017, - 'Naga' => 5016, - 'Orc' => 5018, - 'Pokemon' => 5019, - 'Satyr' => 5020, - 'Sergal' => 5021, - 'Tanuki' => 5022, - 'Unicorn' => 5023, - 'Xenomorph' => 5024, - 'Alien (Other)' => 5001, - 'Exotic (Other)' => 5000 - ), - 'Felines' => array( - 'Domestic Cat' => 6020, - 'Cheetah' => 6021, - 'Cougar' => 6022, - 'Jaguar' => 6023, - 'Leopard' => 6024, - 'Lion' => 6025, - 'Lynx' => 6026, - 'Ocelot' => 6027, - 'Panther' => 6028, - 'Tiger' => 6029, - 'Feline (Other)' => 6030 - ), - 'Insects' => array( - 'Arachnid' => 8000, - 'Mantid' => 8004, - 'Scorpion' => 8005, - 'Insect (Other)' => 8003 - ), - 'Mammals (Other)' => array( - 'Bat' => 6001, - 'Giraffe' => 6031, - 'Hedgehog' => 6032, - 'Hippopotamus' => 6033, - 'Hyena' => 6035, - 'Panda' => 6052, - 'Pig/Swine' => 6053, - 'Rabbit/Hare' => 6059, - 'Raccoon' => 6060, - 'Red Panda' => 6062, - 'Meerkat' => 6043, - 'Mongoose' => 6044, - 'Rhinoceros' => 6063, - 'Mammals (Other)' => 6000 - ), - 'Marsupials' => array( - 'Opossum' => 6037, - 'Kangaroo' => 6038, - 'Koala' => 6039, - 'Quoll' => 6040, - 'Wallaby' => 6041, - 'Marsupial (Other)' => 6042 - ), - 'Mustelids' => array( - 'Badger' => 6045, - 'Ferret' => 6046, - 'Mink' => 6048, - 'Otter' => 6047, - 'Skunk' => 6069, - 'Weasel' => 6049, - 'Mustelid (Other)' => 6051 - ), - 'Primates' => array( - 'Gorilla' => 6054, - 'Human' => 6055, - 'Lemur' => 6056, - 'Monkey' => 6057, - 'Primate (Other)' => 6058 - ), - 'Reptillian' => array( - 'Alligator & Crocodile' => 7001, - 'Gecko' => 7003, - 'Iguana' => 7004, - 'Lizard' => 7005, - 'Snakes & Serpents' => 7006, - 'Turtle' => 7007, - 'Reptilian (Other)' => 7000 - ), - 'Rodents' => array( - 'Beaver' => 6064, - 'Mouse' => 6065, - 'Rat' => 6061, - 'Squirrel' => 6070, - 'Rodent (Other)' => 6067 - ), - 'Vulpines' => array( - 'Fennec' => 6072, - 'Fox' => 6075, - 'Vulpine (Other)' => 6015 - ), - 'Other' => array( - 'Dinosaur' => 8001, - 'Wolverine' => 6050 - ) - ), - 'defaultValue' => 1 - ), - 'gender' => array( - 'name' => 'Gender', - 'type' => 'list', - 'values' => array( - 'Any' => 0, - 'Male' => 2, - 'Female' => 3, - 'Herm' => 4, - 'Transgender' => 5, - 'Multiple characters' => 6, - 'Other / Not Specified' => 7 - ), - 'defaultValue' => 0 - ), - 'rating_general' => array( - 'name' => 'General', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'rating_mature' => array( - 'name' => 'Mature', - 'type' => 'checkbox', - ), - 'rating_adult' => array( - 'name' => 'Adult', - 'type' => 'checkbox', - ), - 'limit-browse' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 10, - 'title' => 'Limit number of submissions to return. -1 for unlimited.' - ), - 'full' => array( - 'name' => 'Full view', - 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'cache' => array( - 'name' => 'Cache submission pages', - 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ) - - ), - 'Journals' => array( - 'username-journals' => array( - 'name' => 'Username', - 'required' => true, - 'exampleValue' => 'dhw', - 'title' => 'Lowercase username as seen in URLs' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'defaultValue' => -1, - 'title' => 'Limit number of journals to return. -1 for unlimited.' - ) - - ), - 'Single Journal' => array( - 'journal-id' => array( - 'name' => 'Journal ID', - 'required' => true, - 'exampleValue' => '10008853', - 'type' => 'number', - 'title' => 'Number seen in journal URL' - ) - ), - 'Gallery' => array( - 'username-gallery' => array( - 'name' => 'Username', - 'required' => true, - 'exampleValue' => 'dhw', - 'title' => 'Lowercase username as seen in URLs' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 10, - 'title' => 'Limit number of submissions to return. -1 for unlimited.' - ), - 'full' => array( - 'name' => 'Full view', - 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'cache' => array( - 'name' => 'Cache submission pages', - 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ) - ), - 'Scraps' => array( - 'username-scraps' => array( - 'name' => 'Username', - 'required' => true, - 'exampleValue' => 'dhw', - 'title' => 'Lowercase username as seen in URLs' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 10, - 'title' => 'Limit number of submissions to return. -1 for unlimited.' - ), - 'full' => array( - 'name' => 'Full view', - 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'cache' => array( - 'name' => 'Cache submission pages', - 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ) - ), - 'Favorites' => array( - 'username-favorites' => array( - 'name' => 'Username', - 'required' => true, - 'exampleValue' => 'dhw', - 'title' => 'Lowercase username as seen in URLs' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 10, - 'title' => 'Limit number of submissions to return. -1 for unlimited.' - ), - 'full' => array( - 'name' => 'Full view', - 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'cache' => array( - 'name' => 'Cache submission pages', - 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ) - ), - 'Gallery Folder' => array( - 'username-folder' => array( - 'name' => 'Username', - 'required' => true, - 'exampleValue' => 'kopk', - 'title' => 'Lowercase username as seen in URLs' - ), - 'folder-id' => array( - 'name' => 'Folder ID', - 'required' => true, - 'exampleValue' => '1031990', - 'type' => 'number', - 'title' => 'Number seen in folder URL' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 10, - 'title' => 'Limit number of submissions to return. -1 for unlimited.' - ), - 'full' => array( - 'name' => 'Full view', - 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'cache' => array( - 'name' => 'Cache submission pages', - 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ) - ) - ); - - /* - * This was aquired by creating a new user on FA then - * extracting the cookie from the browsers dev console. - */ - const FA_AUTH_COOKIE = 'b=4ce65691-b50f-4742-a990-bf28d6de16ee; a=ca6e4566-9d81-4263-9444-653b142e35f8'; - - public function detectParameters($url) { - $params = array(); - - // Single journal - $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journal\/(\d+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['journal-id'] = urldecode($matches[3]); - return $params; - } - - // Journals - $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journals\/([^\/&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['username-journals'] = urldecode($matches[3]); - return $params; - } - - // Gallery folder - $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/gallery\/([^\/&?\n]+)\/folder\/(\d+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['username-folder'] = urldecode($matches[3]); - $params['folder-id'] = urldecode($matches[4]); - $params['full'] = 'on'; - return $params; - } - - // Gallery (must be after gallery folder) - $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/(gallery|scraps|favorites)\/([^\/&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['username-' . $matches[3]] = urldecode($matches[4]); - $params['full'] = 'on'; - return $params; - } - - return null; - } - - public function getName() { - switch($this->queriedContext) { - case 'Search': - return 'Search For ' - . $this->getInput('q'); - case 'Browse': - return 'Browse'; - case 'Journals': - return $this->getInput('username-journals'); - case 'Single Journal': - return 'Journal ' - . $this->getInput('journal-id'); - case 'Gallery': - return $this->getInput('username-gallery'); - case 'Scraps': - return $this->getInput('username-scraps'); - case 'Favorites': - return $this->getInput('username-favorites'); - case 'Gallery Folder': - return $this->getInput('username-folder') - . '\'s Folder ' - . $this->getInput('folder-id'); - default: return parent::getName(); - } - } - - public function getDescription() { - switch($this->queriedContext) { - case 'Search': - return 'FurAffinity Search For ' - . $this->getInput('q'); - case 'Browse': - return 'FurAffinity Browse'; - case 'Journals': - return 'FurAffinity Journals By ' - . $this->getInput('username-journals'); - case 'Single Journal': - return 'FurAffinity Journal ' - . $this->getInput('journal-id'); - case 'Gallery': - return 'FurAffinity Gallery By ' - . $this->getInput('username-gallery'); - case 'Scraps': - return 'FurAffinity Scraps By ' - . $this->getInput('username-scraps'); - case 'Favorites': - return 'FurAffinity Favorites By ' - . $this->getInput('username-favorites'); - case 'Gallery Folder': - return 'FurAffinity Gallery Folder ' - . $this->getInput('folder-id') - . ' By ' - . $this->getInput('username-folder'); - default: return parent::getDescription(); - } - } - - public function getURI() { - switch($this->queriedContext) { - case 'Search': - return SELF::URI - . '/search'; - case 'Browse': - return SELF::URI - . '/browse'; - case 'Journals': - return SELF::URI - . '/journals/' - . $this->getInput('username-journals'); - case 'Single Journal': - return SELF::URI - . '/journal/' - . $this->getInput('journal-id'); - case 'Gallery': - return SELF::URI - . '/gallery/' - . $this->getInput('username-gallery'); - case 'Scraps': - return SELF::URI - . '/scraps/' - . $this->getInput('username-scraps'); - case 'Favorites': - return SELF::URI - . '/favorites/' - . $this->getInput('username-favorites'); - case 'Gallery Folder': - return SELF::URI - . '/gallery/' - . $this->getInput('username-folder') - . '/folder/' - . $this->getInput('folder-id'); - default: return parent::getURI(); - } - } - - public function collectData() { - switch($this->queriedContext) { - case 'Search': - $data = array( - 'q' => $this->getInput('q'), - 'perpage' => 72, - 'rating-general' => ($this->getInput('rating-general') === true ? 'on' : 0), - 'rating-mature' => ($this->getInput('rating-mature') === true ? 'on' : 0), - 'rating-adult' => ($this->getInput('rating-adult') === true ? 'on' : 0), - 'range' => $this->getInput('range'), - 'type-art' => ($this->getInput('type-art') === true ? 'on' : 0), - 'type-flash' => ($this->getInput('type-flash') === true ? 'on' : 0), - 'type-photo' => ($this->getInput('type-photo') === true ? 'on' : 0), - 'type-music' => ($this->getInput('type-music') === true ? 'on' : 0), - 'type-story' => ($this->getInput('type-story') === true ? 'on' : 0), - 'type-poetry' => ($this->getInput('type-poetry') === true ? 'on' : 0), - 'mode' => $this->getInput('mode') - ); - $html = $this->postFASimpleHTMLDOM($data); - $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10); - $this->itemsFromSubmissionList($html, $limit); - break; - case 'Browse': - $data = array( - 'cat' => $this->getInput('cat'), - 'atype' => $this->getInput('atype'), - 'species' => $this->getInput('species'), - 'gender' => $this->getInput('gender'), - 'perpage' => 72, - 'rating_general' => ($this->getInput('rating_general') === true ? 'on' : 0), - 'rating_mature' => ($this->getInput('rating_mature') === true ? 'on' : 0), - 'rating_adult' => ($this->getInput('rating_adult') === true ? 'on' : 0) - ); - $html = $this->postFASimpleHTMLDOM($data); - $limit = (is_int($this->getInput('limit-browse')) ? $this->getInput('limit-browse') : 10); - $this->itemsFromSubmissionList($html, $limit); - break; - case 'Journals': - $html = $this->getFASimpleHTMLDOM($this->getURI()); - $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : -1); - $this->itemsFromJournalList($html, $limit); - break; - case 'Single Journal': - $html = $this->getFASimpleHTMLDOM($this->getURI()); - $this->itemsFromJournal($html); - break; - case 'Gallery': - case 'Scraps': - case 'Favorites': - case 'Gallery Folder': - $html = $this->getFASimpleHTMLDOM($this->getURI()); - $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10); - $this->itemsFromSubmissionList($html, $limit); - break; - } - } - - private function postFASimpleHTMLDOM($data) { - $opts = array( - CURLOPT_CUSTOMREQUEST => 'POST', - CURLOPT_POSTFIELDS => http_build_query($data) - ); - $header = array( - 'Host: ' . parse_url(self::URI, PHP_URL_HOST), - 'Content-Type: application/x-www-form-urlencoded', - 'Cookie: ' . self::FA_AUTH_COOKIE - ); - - $html = getSimpleHTMLDOM($this->getURI(), $header, $opts); - $html = defaultLinkTo($html, $this->getURI()); - - return $html; - } - - private function getFASimpleHTMLDOM($url, $cache = false) { - $header = array( - 'Cookie: ' . self::FA_AUTH_COOKIE - ); - - if($cache) { - $html = getSimpleHTMLDOMCached($url, 86400, $header); // 24 hours - } else { - $html = getSimpleHTMLDOM($url, $header); - } - - $html = defaultLinkTo($html, $url); - - return $html; - } - - private function itemsFromJournalList($html, $limit) { - foreach($html->find('table[id^=jid:]') as $journal) { - # allows limit = -1 to mean 'unlimited' - if($limit-- === 0) break; - - $item = array(); - - $this->setReferrerPolicy($journal); - - $item['uri'] = $journal->find('a', 0)->href; - $item['title'] = html_entity_decode($journal->find('a', 0)->plaintext); - $item['author'] = $this->getInput('username-journals'); - $item['timestamp'] = strtotime( - $journal->find('span.popup_date', 0)->plaintext); - $item['content'] = $journal - ->find('.alt1 table div.no_overflow', 0) - ->innertext; - - $this->items[] = $item; - } - } - - private function itemsFromJournal($html) { - $this->setReferrerPolicy($html); - $item = array(); - - $item['uri'] = $this->getURI(); - - $title = $html->find('.journal-title-box .no_overflow', 0)->plaintext; - $title = html_entity_decode($title); - $title = trim($title, " \t\n\r\0\x0B" . chr(0xC2) . chr(0xA0)); - $item['title'] = $title; - - $item['author'] = $html->find('.journal-title-box a', 0)->plaintext; - $item['timestamp'] = strtotime( - $html->find('.journal-title-box span.popup_date', 0)->plaintext); - $item['content'] = $html->find('.journal-body', 0)->innertext; - - $this->items[] = $item; - } - - private function itemsFromSubmissionList($html, $limit) { - $cache = ($this->getInput('cache') === true); - - foreach($html->find('section.gallery figure') as $figure) { - # allows limit = -1 to mean 'unlimited' - if($limit-- === 0) break; - - $item = array(); - - $submissionURL = $figure->find('b u a', 0)->href; - $imgURL = 'https:' . $figure->find('b u a img', 0)->src; - - $item['uri'] = $submissionURL; - $item['title'] = html_entity_decode( - $figure->find('figcaption p a[href*=/view/]', 0)->title); - $item['author'] = $figure->find('figcaption p a[href*=/user/]', 0)->title; - - if($this->getInput('full') === true) { - $submissionHTML = $this->getFASimpleHTMLDOM($submissionURL, $cache); - - $stats = $submissionHTML->find('.stats-container', 0); - $item['timestamp'] = strtotime($stats->find('.popup_date', 0)->title); - $item['enclosures'] = array( - $submissionHTML->find('.actions a[href^=https://d.facdn]', 0)->href - ); - foreach($stats->find('#keywords a') as $keyword) { - $item['categories'][] = $keyword->plaintext; - } - - $previewSrc = $submissionHTML->find('#submissionImg', 0) - ->{'data-preview-src'}; - if($previewSrc) { - $imgURL = 'https:' . $previewSrc; - } - $description = $submissionHTML - ->find('.maintable .maintable tr td.alt1', -1); - $this->setReferrerPolicy($description); - $description = $description->innertext; - - $item['content'] = <<<EOD +class FurAffinityBridge extends BridgeAbstract +{ + const NAME = 'FurAffinity Bridge'; + const URI = 'https://www.furaffinity.net'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Returns posts from various sections of FurAffinity'; + const MAINTAINER = 'Roliga'; + const PARAMETERS = [ + 'Search' => [ + 'q' => [ + 'name' => 'Query', + 'required' => true, + 'exampleValue' => 'dog', + ], + 'rating-general' => [ + 'name' => 'General', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'rating-mature' => [ + 'name' => 'Mature', + 'type' => 'checkbox', + ], + 'rating-adult' => [ + 'name' => 'Adult', + 'type' => 'checkbox', + ], + 'range' => [ + 'name' => 'Time range', + 'type' => 'list', + 'values' => [ + 'A Day' => 'day', + '3 Days' => '3days', + 'A Week' => 'week', + 'A Month' => 'month', + 'All time' => 'all' + ], + 'defaultValue' => 'all' + ], + 'type-art' => [ + 'name' => 'Art', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'type-flash' => [ + 'name' => 'Flash', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'type-photo' => [ + 'name' => 'Photography', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'type-music' => [ + 'name' => 'Music', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'type-story' => [ + 'name' => 'Story', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'type-poetry' => [ + 'name' => 'Poetry', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'mode' => [ + 'name' => 'Match mode', + 'type' => 'list', + 'values' => [ + 'All of the words' => 'all', + 'Any of the words' => 'any', + 'Extended' => 'extended' + ], + 'defaultValue' => 'extended' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ], + 'full' => [ + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'cache' => [ + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ] + ], + 'Browse' => [ + 'cat' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'Visual Art' => [ + 'All' => 1, + 'Artwork (Digital)' => 2, + 'Artwork (Traditional)' => 3, + 'Cellshading' => 4, + 'Crafting' => 5, + 'Designs' => 6, + 'Flash' => 7, + 'Fursuiting' => 8, + 'Icons' => 9, + 'Mosaics' => 10, + 'Photography' => 11, + 'Sculpting' => 12 + ], + 'Readable Art' => [ + 'Story' => 13, + 'Poetry' => 14, + 'Prose' => 15 + ], + 'Audio Art' => [ + 'Music' => 16, + 'Podcasts' => 17 + ], + 'Downloadable' => [ + 'Skins' => 18, + 'Handhelds' => 19, + 'Resources' => 20 + ], + 'Other Stuff' => [ + 'Adoptables' => 21, + 'Auctions' => 22, + 'Contests' => 23, + 'Current Events' => 24, + 'Desktops' => 25, + 'Stockart' => 26, + 'Screenshots' => 27, + 'Scraps' => 28, + 'Wallpaper' => 29, + 'YCH / Sale' => 30, + 'Other' => 31 + ] + ], + 'defaultValue' => 1 + ], + 'atype' => [ + 'name' => 'Type', + 'type' => 'list', + 'values' => [ + 'General Things' => [ + 'All' => 1, + 'Abstract' => 2, + 'Animal related (non-anthro)' => 3, + 'Anime' => 4, + 'Comics' => 5, + 'Doodle' => 6, + 'Fanart' => 7, + 'Fantasy' => 8, + 'Human' => 9, + 'Portraits' => 10, + 'Scenery' => 11, + 'Still Life' => 12, + 'Tutorials' => 13, + 'Miscellaneous' => 14 + ], + 'Fetish / Furry specialty' => [ + 'Baby fur' => 101, + 'Bondage' => 102, + 'Digimon' => 103, + 'Fat Furs' => 104, + 'Fetish Other' => 105, + 'Fursuit' => 106, + 'Gore / Macabre Art' => 119, + 'Hyper' => 107, + 'Inflation' => 108, + 'Macro / Micro' => 109, + 'Muscle' => 110, + 'My Little Pony / Brony' => 111, + 'Paw' => 112, + 'Pokemon' => 113, + 'Pregnancy' => 114, + 'Sonic' => 115, + 'Transformation' => 116, + 'Vore' => 117, + 'Water Sports' => 118, + 'General Furry Art' => 100 + ], + 'Music' => [ + 'Techno' => 201, + 'Trance' => 202, + 'House' => 203, + '90s' => 204, + '80s' => 205, + '70s' => 206, + '60s' => 207, + 'Pre-60s' => 208, + 'Classical' => 209, + 'Game Music' => 210, + 'Rock' => 211, + 'Pop' => 212, + 'Rap' => 213, + 'Industrial' => 214, + 'Other Music' => 200 + ] + ], + 'defaultValue' => 1 + ], + 'species' => [ + 'name' => 'Species', + 'type' => 'list', + 'values' => [ + 'Unspecified / Any' => 1, + 'Amphibian' => [ + 'Frog' => 1001, + 'Newt' => 1002, + 'Salamander' => 1003, + 'Amphibian (Other)' => 1000 + ], + 'Aquatic' => [ + 'Cephalopod' => 2001, + 'Dolphin' => 2002, + 'Fish' => 2005, + 'Porpoise' => 2004, + 'Seal' => 6068, + 'Shark' => 2006, + 'Whale' => 2003, + 'Aquatic (Other)' => 2000 + ], + 'Avian' => [ + 'Corvid' => 3001, + 'Crow' => 3002, + 'Duck' => 3003, + 'Eagle' => 3004, + 'Falcon' => 3005, + 'Goose' => 3006, + 'Gryphon' => 3007, + 'Hawk' => 3008, + 'Owl' => 3009, + 'Phoenix' => 3010, + 'Swan' => 3011, + 'Avian (Other)' => 3000 + ], + 'Bears & Ursines' => [ + 'Bear' => 6002 + ], + 'Camelids' => [ + 'Camel' => 6074, + 'Llama' => 6036 + ], + 'Canines & Lupines' => [ + 'Coyote' => 6008, + 'Doberman' => 6009, + 'Dog' => 6010, + 'Dingo' => 6011, + 'German Shepherd' => 6012, + 'Jackal' => 6013, + 'Husky' => 6014, + 'Wolf' => 6016, + 'Canine (Other)' => 6017 + ], + 'Cervines' => [ + 'Cervine (Other)' => 6018 + ], + 'Cows & Bovines' => [ + 'Antelope' => 6004, + 'Cows' => 6003, + 'Gazelle' => 6005, + 'Goat' => 6006, + 'Bovines (General)' => 6007 + ], + 'Dragons' => [ + 'Eastern Dragon' => 4001, + 'Hydra' => 4002, + 'Serpent' => 4003, + 'Western Dragon' => 4004, + 'Wyvern' => 4005, + 'Dragon (Other)' => 4000 + ], + 'Equestrians' => [ + 'Donkey' => 6019, + 'Horse' => 6034, + 'Pony' => 6073, + 'Zebra' => 6071 + ], + 'Exotic & Mythicals' => [ + 'Argonian' => 5002, + 'Chakat' => 5003, + 'Chocobo' => 5004, + 'Citra' => 5005, + 'Crux' => 5006, + 'Daemon' => 5007, + 'Digimon' => 5008, + 'Dracat' => 5009, + 'Draenei' => 5010, + 'Elf' => 5011, + 'Gargoyle' => 5012, + 'Iksar' => 5013, + 'Kaiju/Monster' => 5015, + 'Langurhali' => 5014, + 'Moogle' => 5017, + 'Naga' => 5016, + 'Orc' => 5018, + 'Pokemon' => 5019, + 'Satyr' => 5020, + 'Sergal' => 5021, + 'Tanuki' => 5022, + 'Unicorn' => 5023, + 'Xenomorph' => 5024, + 'Alien (Other)' => 5001, + 'Exotic (Other)' => 5000 + ], + 'Felines' => [ + 'Domestic Cat' => 6020, + 'Cheetah' => 6021, + 'Cougar' => 6022, + 'Jaguar' => 6023, + 'Leopard' => 6024, + 'Lion' => 6025, + 'Lynx' => 6026, + 'Ocelot' => 6027, + 'Panther' => 6028, + 'Tiger' => 6029, + 'Feline (Other)' => 6030 + ], + 'Insects' => [ + 'Arachnid' => 8000, + 'Mantid' => 8004, + 'Scorpion' => 8005, + 'Insect (Other)' => 8003 + ], + 'Mammals (Other)' => [ + 'Bat' => 6001, + 'Giraffe' => 6031, + 'Hedgehog' => 6032, + 'Hippopotamus' => 6033, + 'Hyena' => 6035, + 'Panda' => 6052, + 'Pig/Swine' => 6053, + 'Rabbit/Hare' => 6059, + 'Raccoon' => 6060, + 'Red Panda' => 6062, + 'Meerkat' => 6043, + 'Mongoose' => 6044, + 'Rhinoceros' => 6063, + 'Mammals (Other)' => 6000 + ], + 'Marsupials' => [ + 'Opossum' => 6037, + 'Kangaroo' => 6038, + 'Koala' => 6039, + 'Quoll' => 6040, + 'Wallaby' => 6041, + 'Marsupial (Other)' => 6042 + ], + 'Mustelids' => [ + 'Badger' => 6045, + 'Ferret' => 6046, + 'Mink' => 6048, + 'Otter' => 6047, + 'Skunk' => 6069, + 'Weasel' => 6049, + 'Mustelid (Other)' => 6051 + ], + 'Primates' => [ + 'Gorilla' => 6054, + 'Human' => 6055, + 'Lemur' => 6056, + 'Monkey' => 6057, + 'Primate (Other)' => 6058 + ], + 'Reptillian' => [ + 'Alligator & Crocodile' => 7001, + 'Gecko' => 7003, + 'Iguana' => 7004, + 'Lizard' => 7005, + 'Snakes & Serpents' => 7006, + 'Turtle' => 7007, + 'Reptilian (Other)' => 7000 + ], + 'Rodents' => [ + 'Beaver' => 6064, + 'Mouse' => 6065, + 'Rat' => 6061, + 'Squirrel' => 6070, + 'Rodent (Other)' => 6067 + ], + 'Vulpines' => [ + 'Fennec' => 6072, + 'Fox' => 6075, + 'Vulpine (Other)' => 6015 + ], + 'Other' => [ + 'Dinosaur' => 8001, + 'Wolverine' => 6050 + ] + ], + 'defaultValue' => 1 + ], + 'gender' => [ + 'name' => 'Gender', + 'type' => 'list', + 'values' => [ + 'Any' => 0, + 'Male' => 2, + 'Female' => 3, + 'Herm' => 4, + 'Transgender' => 5, + 'Multiple characters' => 6, + 'Other / Not Specified' => 7 + ], + 'defaultValue' => 0 + ], + 'rating_general' => [ + 'name' => 'General', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'rating_mature' => [ + 'name' => 'Mature', + 'type' => 'checkbox', + ], + 'rating_adult' => [ + 'name' => 'Adult', + 'type' => 'checkbox', + ], + 'limit-browse' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ], + 'full' => [ + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'cache' => [ + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ] + + ], + 'Journals' => [ + 'username-journals' => [ + 'name' => 'Username', + 'required' => true, + 'exampleValue' => 'dhw', + 'title' => 'Lowercase username as seen in URLs' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => -1, + 'title' => 'Limit number of journals to return. -1 for unlimited.' + ] + + ], + 'Single Journal' => [ + 'journal-id' => [ + 'name' => 'Journal ID', + 'required' => true, + 'exampleValue' => '10008853', + 'type' => 'number', + 'title' => 'Number seen in journal URL' + ] + ], + 'Gallery' => [ + 'username-gallery' => [ + 'name' => 'Username', + 'required' => true, + 'exampleValue' => 'dhw', + 'title' => 'Lowercase username as seen in URLs' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ], + 'full' => [ + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'cache' => [ + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ] + ], + 'Scraps' => [ + 'username-scraps' => [ + 'name' => 'Username', + 'required' => true, + 'exampleValue' => 'dhw', + 'title' => 'Lowercase username as seen in URLs' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ], + 'full' => [ + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'cache' => [ + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ] + ], + 'Favorites' => [ + 'username-favorites' => [ + 'name' => 'Username', + 'required' => true, + 'exampleValue' => 'dhw', + 'title' => 'Lowercase username as seen in URLs' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ], + 'full' => [ + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'cache' => [ + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ] + ], + 'Gallery Folder' => [ + 'username-folder' => [ + 'name' => 'Username', + 'required' => true, + 'exampleValue' => 'kopk', + 'title' => 'Lowercase username as seen in URLs' + ], + 'folder-id' => [ + 'name' => 'Folder ID', + 'required' => true, + 'exampleValue' => '1031990', + 'type' => 'number', + 'title' => 'Number seen in folder URL' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 10, + 'title' => 'Limit number of submissions to return. -1 for unlimited.' + ], + 'full' => [ + 'name' => 'Full view', + 'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'cache' => [ + 'name' => 'Cache submission pages', + 'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ] + ] + ]; + + /* + * This was aquired by creating a new user on FA then + * extracting the cookie from the browsers dev console. + */ + const FA_AUTH_COOKIE = 'b=4ce65691-b50f-4742-a990-bf28d6de16ee; a=ca6e4566-9d81-4263-9444-653b142e35f8'; + + public function detectParameters($url) + { + $params = []; + + // Single journal + $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journal\/(\d+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['journal-id'] = urldecode($matches[3]); + return $params; + } + + // Journals + $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journals\/([^\/&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['username-journals'] = urldecode($matches[3]); + return $params; + } + + // Gallery folder + $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/gallery\/([^\/&?\n]+)\/folder\/(\d+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['username-folder'] = urldecode($matches[3]); + $params['folder-id'] = urldecode($matches[4]); + $params['full'] = 'on'; + return $params; + } + + // Gallery (must be after gallery folder) + $regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/(gallery|scraps|favorites)\/([^\/&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['username-' . $matches[3]] = urldecode($matches[4]); + $params['full'] = 'on'; + return $params; + } + + return null; + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Search': + return 'Search For ' + . $this->getInput('q'); + case 'Browse': + return 'Browse'; + case 'Journals': + return $this->getInput('username-journals'); + case 'Single Journal': + return 'Journal ' + . $this->getInput('journal-id'); + case 'Gallery': + return $this->getInput('username-gallery'); + case 'Scraps': + return $this->getInput('username-scraps'); + case 'Favorites': + return $this->getInput('username-favorites'); + case 'Gallery Folder': + return $this->getInput('username-folder') + . '\'s Folder ' + . $this->getInput('folder-id'); + default: + return parent::getName(); + } + } + + public function getDescription() + { + switch ($this->queriedContext) { + case 'Search': + return 'FurAffinity Search For ' + . $this->getInput('q'); + case 'Browse': + return 'FurAffinity Browse'; + case 'Journals': + return 'FurAffinity Journals By ' + . $this->getInput('username-journals'); + case 'Single Journal': + return 'FurAffinity Journal ' + . $this->getInput('journal-id'); + case 'Gallery': + return 'FurAffinity Gallery By ' + . $this->getInput('username-gallery'); + case 'Scraps': + return 'FurAffinity Scraps By ' + . $this->getInput('username-scraps'); + case 'Favorites': + return 'FurAffinity Favorites By ' + . $this->getInput('username-favorites'); + case 'Gallery Folder': + return 'FurAffinity Gallery Folder ' + . $this->getInput('folder-id') + . ' By ' + . $this->getInput('username-folder'); + default: + return parent::getDescription(); + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Search': + return self::URI + . '/search'; + case 'Browse': + return self::URI + . '/browse'; + case 'Journals': + return self::URI + . '/journals/' + . $this->getInput('username-journals'); + case 'Single Journal': + return self::URI + . '/journal/' + . $this->getInput('journal-id'); + case 'Gallery': + return self::URI + . '/gallery/' + . $this->getInput('username-gallery'); + case 'Scraps': + return self::URI + . '/scraps/' + . $this->getInput('username-scraps'); + case 'Favorites': + return self::URI + . '/favorites/' + . $this->getInput('username-favorites'); + case 'Gallery Folder': + return self::URI + . '/gallery/' + . $this->getInput('username-folder') + . '/folder/' + . $this->getInput('folder-id'); + default: + return parent::getURI(); + } + } + + public function collectData() + { + switch ($this->queriedContext) { + case 'Search': + $data = [ + 'q' => $this->getInput('q'), + 'perpage' => 72, + 'rating-general' => ($this->getInput('rating-general') === true ? 'on' : 0), + 'rating-mature' => ($this->getInput('rating-mature') === true ? 'on' : 0), + 'rating-adult' => ($this->getInput('rating-adult') === true ? 'on' : 0), + 'range' => $this->getInput('range'), + 'type-art' => ($this->getInput('type-art') === true ? 'on' : 0), + 'type-flash' => ($this->getInput('type-flash') === true ? 'on' : 0), + 'type-photo' => ($this->getInput('type-photo') === true ? 'on' : 0), + 'type-music' => ($this->getInput('type-music') === true ? 'on' : 0), + 'type-story' => ($this->getInput('type-story') === true ? 'on' : 0), + 'type-poetry' => ($this->getInput('type-poetry') === true ? 'on' : 0), + 'mode' => $this->getInput('mode') + ]; + $html = $this->postFASimpleHTMLDOM($data); + $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10); + $this->itemsFromSubmissionList($html, $limit); + break; + case 'Browse': + $data = [ + 'cat' => $this->getInput('cat'), + 'atype' => $this->getInput('atype'), + 'species' => $this->getInput('species'), + 'gender' => $this->getInput('gender'), + 'perpage' => 72, + 'rating_general' => ($this->getInput('rating_general') === true ? 'on' : 0), + 'rating_mature' => ($this->getInput('rating_mature') === true ? 'on' : 0), + 'rating_adult' => ($this->getInput('rating_adult') === true ? 'on' : 0) + ]; + $html = $this->postFASimpleHTMLDOM($data); + $limit = (is_int($this->getInput('limit-browse')) ? $this->getInput('limit-browse') : 10); + $this->itemsFromSubmissionList($html, $limit); + break; + case 'Journals': + $html = $this->getFASimpleHTMLDOM($this->getURI()); + $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : -1); + $this->itemsFromJournalList($html, $limit); + break; + case 'Single Journal': + $html = $this->getFASimpleHTMLDOM($this->getURI()); + $this->itemsFromJournal($html); + break; + case 'Gallery': + case 'Scraps': + case 'Favorites': + case 'Gallery Folder': + $html = $this->getFASimpleHTMLDOM($this->getURI()); + $limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10); + $this->itemsFromSubmissionList($html, $limit); + break; + } + } + + private function postFASimpleHTMLDOM($data) + { + $opts = [ + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => http_build_query($data) + ]; + $header = [ + 'Host: ' . parse_url(self::URI, PHP_URL_HOST), + 'Content-Type: application/x-www-form-urlencoded', + 'Cookie: ' . self::FA_AUTH_COOKIE + ]; + + $html = getSimpleHTMLDOM($this->getURI(), $header, $opts); + $html = defaultLinkTo($html, $this->getURI()); + + return $html; + } + + private function getFASimpleHTMLDOM($url, $cache = false) + { + $header = [ + 'Cookie: ' . self::FA_AUTH_COOKIE + ]; + + if ($cache) { + $html = getSimpleHTMLDOMCached($url, 86400, $header); // 24 hours + } else { + $html = getSimpleHTMLDOM($url, $header); + } + + $html = defaultLinkTo($html, $url); + + return $html; + } + + private function itemsFromJournalList($html, $limit) + { + foreach ($html->find('table[id^=jid:]') as $journal) { + # allows limit = -1 to mean 'unlimited' + if ($limit-- === 0) { + break; + } + + $item = []; + + $this->setReferrerPolicy($journal); + + $item['uri'] = $journal->find('a', 0)->href; + $item['title'] = html_entity_decode($journal->find('a', 0)->plaintext); + $item['author'] = $this->getInput('username-journals'); + $item['timestamp'] = strtotime( + $journal->find('span.popup_date', 0)->plaintext + ); + $item['content'] = $journal + ->find('.alt1 table div.no_overflow', 0) + ->innertext; + + $this->items[] = $item; + } + } + + private function itemsFromJournal($html) + { + $this->setReferrerPolicy($html); + $item = []; + + $item['uri'] = $this->getURI(); + + $title = $html->find('.journal-title-box .no_overflow', 0)->plaintext; + $title = html_entity_decode($title); + $title = trim($title, " \t\n\r\0\x0B" . chr(0xC2) . chr(0xA0)); + $item['title'] = $title; + + $item['author'] = $html->find('.journal-title-box a', 0)->plaintext; + $item['timestamp'] = strtotime( + $html->find('.journal-title-box span.popup_date', 0)->plaintext + ); + $item['content'] = $html->find('.journal-body', 0)->innertext; + + $this->items[] = $item; + } + + private function itemsFromSubmissionList($html, $limit) + { + $cache = ($this->getInput('cache') === true); + + foreach ($html->find('section.gallery figure') as $figure) { + # allows limit = -1 to mean 'unlimited' + if ($limit-- === 0) { + break; + } + + $item = []; + + $submissionURL = $figure->find('b u a', 0)->href; + $imgURL = 'https:' . $figure->find('b u a img', 0)->src; + + $item['uri'] = $submissionURL; + $item['title'] = html_entity_decode( + $figure->find('figcaption p a[href*=/view/]', 0)->title + ); + $item['author'] = $figure->find('figcaption p a[href*=/user/]', 0)->title; + + if ($this->getInput('full') === true) { + $submissionHTML = $this->getFASimpleHTMLDOM($submissionURL, $cache); + + $stats = $submissionHTML->find('.stats-container', 0); + $item['timestamp'] = strtotime($stats->find('.popup_date', 0)->title); + $item['enclosures'] = [ + $submissionHTML->find('.actions a[href^=https://d.facdn]', 0)->href + ]; + foreach ($stats->find('#keywords a') as $keyword) { + $item['categories'][] = $keyword->plaintext; + } + + $previewSrc = $submissionHTML->find('#submissionImg', 0) + ->{'data-preview-src'}; + if ($previewSrc) { + $imgURL = 'https:' . $previewSrc; + } + + $description = $submissionHTML + ->find('.maintable .maintable tr td.alt1', -1); + $this->setReferrerPolicy($description); + $description = $description->innertext; + + $item['content'] = <<<EOD <a href="$submissionURL"> <img src="{$imgURL}" referrerpolicy="no-referrer" /> </a> @@ -905,27 +927,28 @@ class FurAffinityBridge extends BridgeAbstract { {$description} </p> EOD; - } else { - $item['content'] = <<<EOD + } else { + $item['content'] = <<<EOD <a href="$submissionURL"> <img src="$imgURL" referrerpolicy="no-referrer" /> </a> EOD; - } - - $this->items[] = $item; - } - } - - private function setReferrerPolicy(&$html) { - foreach($html->find('img') as $img) { - /* - * Note: Without the no-referrer policy their CDN sometimes denies requests. - * We can't control this for enclosures sadly. - * At least tt-rss adds the referrerpolicy on its own. - * Alternatively we could not use https for images, but that's not ideal. - */ - $img->referrerpolicy = 'no-referrer'; - } - } + } + + $this->items[] = $item; + } + } + + private function setReferrerPolicy(&$html) + { + foreach ($html->find('img') as $img) { + /* + * Note: Without the no-referrer policy their CDN sometimes denies requests. + * We can't control this for enclosures sadly. + * At least tt-rss adds the referrerpolicy on its own. + * Alternatively we could not use https for images, but that's not ideal. + */ + $img->referrerpolicy = 'no-referrer'; + } + } } diff --git a/bridges/FurAffinityUserBridge.php b/bridges/FurAffinityUserBridge.php index d9214b84..fa10d7ae 100644 --- a/bridges/FurAffinityUserBridge.php +++ b/bridges/FurAffinityUserBridge.php @@ -1,58 +1,63 @@ <?php -class FurAffinityUserBridge extends BridgeAbstract { - const NAME = 'FurAffinity User Gallery'; - const URI = 'https://www.furaffinity.net'; - const MAINTAINER = 'CyberJacob'; - const DESCRIPTION = 'See https://rss-bridge.github.io/rss-bridge/Bridge_Specific/Furaffinityuser.html for explanation'; - const PARAMETERS = array( - array( - 'searchUsername' => array( - 'name' => 'Search Username', - 'type' => 'text', - 'required' => true, - 'title' => 'Username to fetch the gallery for', - 'exampleValue' => 'armundy', - ), - 'aCookie' => array( - 'name' => 'Login cookie \'a\'', - 'type' => 'text', - 'required' => true - ), - 'bCookie' => array( - 'name' => 'Login cookie \'b\'', - 'type' => 'text', - 'required' => true - ) - ) - ); - - public function collectData() { - $opt = array(CURLOPT_COOKIE => 'b=' . $this->getInput('bCookie') . '; a=' . $this->getInput('aCookie')); - - $url = self::URI . '/gallery/' . $this->getInput('searchUsername'); - - $html = getSimpleHTMLDOM($url, array(), $opt) - or returnServerError('Could not load the user\'s gallery page.'); - - $submissions = $html->find('section[id=gallery-gallery]', 0)->find('figure'); - foreach($submissions as $submission) { - $item = array(); - $item['title'] = $submission->find('figcaption', 0)->find('a', 0)->plaintext; - - $thumbnail = $submission->find('a', 0); - $thumbnail->href = self::URI . $thumbnail->href; - - $item['content'] = $submission->find('a', 0); - - $this->items[] = $item; - } - } - - public function getName() { - return self::NAME . ' for ' . $this->getInput('searchUsername'); - } - - public function getURI() { - return self::URI . '/user/' . $this->getInput('searchUsername'); - } + +class FurAffinityUserBridge extends BridgeAbstract +{ + const NAME = 'FurAffinity User Gallery'; + const URI = 'https://www.furaffinity.net'; + const MAINTAINER = 'CyberJacob'; + const DESCRIPTION = 'See https://rss-bridge.github.io/rss-bridge/Bridge_Specific/Furaffinityuser.html for explanation'; + const PARAMETERS = [ + [ + 'searchUsername' => [ + 'name' => 'Search Username', + 'type' => 'text', + 'required' => true, + 'title' => 'Username to fetch the gallery for', + 'exampleValue' => 'armundy', + ], + 'aCookie' => [ + 'name' => 'Login cookie \'a\'', + 'type' => 'text', + 'required' => true + ], + 'bCookie' => [ + 'name' => 'Login cookie \'b\'', + 'type' => 'text', + 'required' => true + ] + ] + ]; + + public function collectData() + { + $opt = [CURLOPT_COOKIE => 'b=' . $this->getInput('bCookie') . '; a=' . $this->getInput('aCookie')]; + + $url = self::URI . '/gallery/' . $this->getInput('searchUsername'); + + $html = getSimpleHTMLDOM($url, [], $opt) + or returnServerError('Could not load the user\'s gallery page.'); + + $submissions = $html->find('section[id=gallery-gallery]', 0)->find('figure'); + foreach ($submissions as $submission) { + $item = []; + $item['title'] = $submission->find('figcaption', 0)->find('a', 0)->plaintext; + + $thumbnail = $submission->find('a', 0); + $thumbnail->href = self::URI . $thumbnail->href; + + $item['content'] = $submission->find('a', 0); + + $this->items[] = $item; + } + } + + public function getName() + { + return self::NAME . ' for ' . $this->getInput('searchUsername'); + } + + public function getURI() + { + return self::URI . '/user/' . $this->getInput('searchUsername'); + } } diff --git a/bridges/FuturaSciencesBridge.php b/bridges/FuturaSciencesBridge.php index 541c4bac..97926bed 100644 --- a/bridges/FuturaSciencesBridge.php +++ b/bridges/FuturaSciencesBridge.php @@ -1,160 +1,170 @@ <?php -class FuturaSciencesBridge extends FeedExpander { - const MAINTAINER = 'ORelio'; - const NAME = 'Futura-Sciences Bridge'; - const URI = 'https://www.futura-sciences.com/'; - const DESCRIPTION = 'Returns the newest articles.'; +class FuturaSciencesBridge extends FeedExpander +{ + const MAINTAINER = 'ORelio'; + const NAME = 'Futura-Sciences Bridge'; + const URI = 'https://www.futura-sciences.com/'; + const DESCRIPTION = 'Returns the newest articles.'; - const PARAMETERS = array( array( - 'feed' => array( - 'name' => 'Feed', - 'type' => 'list', - 'values' => array( - 'Les flux multi-magazines' => array( - 'Les dernières actualités de Futura-Sciences' => 'actualites', - 'Les dernières définitions de Futura-Sciences' => 'definitions', - 'Les dernières photos de Futura-Sciences' => 'photos', - 'Les dernières questions - réponses de Futura-Sciences' => 'questions-reponses', - 'Les derniers dossiers de Futura-Sciences' => 'dossiers' - ), - 'Les flux Services' => array( - 'Les cartes virtuelles de Futura-Sciences' => 'services/cartes-virtuelles', - 'Les fonds d\'écran de Futura-Sciences' => 'services/fonds-ecran' - ), - 'Les flux Santé' => array( - 'Les dernières actualités de Futura-Santé' => 'sante/actualites', - 'Les dernières définitions de Futura-Santé' => 'sante/definitions', - 'Les dernières questions-réponses de Futura-Santé' => 'sante/question-reponses', - 'Les derniers dossiers de Futura-Santé' => 'sante/dossiers' - ), - 'Les flux High-Tech' => array( - 'Les dernières actualités de Futura-High-Tech' => 'high-tech/actualites', - 'Les dernières astuces de Futura-High-Tech' => 'high-tech/question-reponses', - 'Les dernières définitions de Futura-High-Tech' => 'high-tech/definitions', - 'Les derniers dossiers de Futura-High-Tech' => 'high-tech/dossiers' - ), - 'Les flux Espace' => array( - 'Les dernières actualités de Futura-Espace' => 'espace/actualites', - 'Les dernières définitions de Futura-Espace' => 'espace/definitions', - 'Les dernières questions-réponses de Futura-Espace' => 'espace/question-reponses', - 'Les derniers dossiers de Futura-Espace' => 'espace/dossiers' - ), - 'Les flux Environnement' => array( - 'Les dernières actualités de Futura-Environnement' => 'environnement/actualites', - 'Les dernières définitions de Futura-Environnement' => 'environnement/definitions', - 'Les dernières questions-réponses de Futura-Environnement' => 'environnement/question-reponses', - 'Les derniers dossiers de Futura-Environnement' => 'environnement/dossiers' - ), - 'Les flux Maison' => array( - 'Les dernières actualités de Futura-Maison' => 'maison/actualites', - 'Les dernières astuces de Futura-Maison' => 'maison/question-reponses', - 'Les dernières définitions de Futura-Maison' => 'maison/definitions', - 'Les derniers dossiers de Futura-Maison' => 'maison/dossiers' - ), - 'Les flux Nature' => array( - 'Les dernières actualités de Futura-Nature' => 'nature/actualites', - 'Les dernières définitions de Futura-Nature' => 'nature/definitions', - 'Les dernières questions-réponses de Futura-Nature' => 'nature/question-reponses', - 'Les derniers dossiers de Futura-Nature' => 'nature/dossiers' - ), - 'Les flux Terre' => array( - 'Les dernières actualités de Futura-Terre' => 'terre/actualites', - 'Les dernières définitions de Futura-Terre' => 'terre/definitions', - 'Les dernières questions-réponses de Futura-Terre' => 'terre/question-reponses', - 'Les derniers dossiers de Futura-Terre' => 'terre/dossiers' - ), - 'Les flux Matière' => array( - 'Les dernières actualités de Futura-Matière' => 'matiere/actualites', - 'Les dernières définitions de Futura-Matière' => 'matiere/definitions', - 'Les dernières questions-réponses de Futura-Matière' => 'matiere/question-reponses', - 'Les derniers dossiers de Futura-Matière' => 'matiere/dossiers' - ), - 'Les flux Mathématiques' => array( - 'Les dernières actualités de Futura-Mathématiques' => 'mathematiques/actualites', - 'Les derniers dossiers de Futura-Mathématiques' => 'mathematiques/dossiers' - ) - ) - ) - )); + const PARAMETERS = [ [ + 'feed' => [ + 'name' => 'Feed', + 'type' => 'list', + 'values' => [ + 'Les flux multi-magazines' => [ + 'Les dernières actualités de Futura-Sciences' => 'actualites', + 'Les dernières définitions de Futura-Sciences' => 'definitions', + 'Les dernières photos de Futura-Sciences' => 'photos', + 'Les dernières questions - réponses de Futura-Sciences' => 'questions-reponses', + 'Les derniers dossiers de Futura-Sciences' => 'dossiers' + ], + 'Les flux Services' => [ + 'Les cartes virtuelles de Futura-Sciences' => 'services/cartes-virtuelles', + 'Les fonds d\'écran de Futura-Sciences' => 'services/fonds-ecran' + ], + 'Les flux Santé' => [ + 'Les dernières actualités de Futura-Santé' => 'sante/actualites', + 'Les dernières définitions de Futura-Santé' => 'sante/definitions', + 'Les dernières questions-réponses de Futura-Santé' => 'sante/question-reponses', + 'Les derniers dossiers de Futura-Santé' => 'sante/dossiers' + ], + 'Les flux High-Tech' => [ + 'Les dernières actualités de Futura-High-Tech' => 'high-tech/actualites', + 'Les dernières astuces de Futura-High-Tech' => 'high-tech/question-reponses', + 'Les dernières définitions de Futura-High-Tech' => 'high-tech/definitions', + 'Les derniers dossiers de Futura-High-Tech' => 'high-tech/dossiers' + ], + 'Les flux Espace' => [ + 'Les dernières actualités de Futura-Espace' => 'espace/actualites', + 'Les dernières définitions de Futura-Espace' => 'espace/definitions', + 'Les dernières questions-réponses de Futura-Espace' => 'espace/question-reponses', + 'Les derniers dossiers de Futura-Espace' => 'espace/dossiers' + ], + 'Les flux Environnement' => [ + 'Les dernières actualités de Futura-Environnement' => 'environnement/actualites', + 'Les dernières définitions de Futura-Environnement' => 'environnement/definitions', + 'Les dernières questions-réponses de Futura-Environnement' => 'environnement/question-reponses', + 'Les derniers dossiers de Futura-Environnement' => 'environnement/dossiers' + ], + 'Les flux Maison' => [ + 'Les dernières actualités de Futura-Maison' => 'maison/actualites', + 'Les dernières astuces de Futura-Maison' => 'maison/question-reponses', + 'Les dernières définitions de Futura-Maison' => 'maison/definitions', + 'Les derniers dossiers de Futura-Maison' => 'maison/dossiers' + ], + 'Les flux Nature' => [ + 'Les dernières actualités de Futura-Nature' => 'nature/actualites', + 'Les dernières définitions de Futura-Nature' => 'nature/definitions', + 'Les dernières questions-réponses de Futura-Nature' => 'nature/question-reponses', + 'Les derniers dossiers de Futura-Nature' => 'nature/dossiers' + ], + 'Les flux Terre' => [ + 'Les dernières actualités de Futura-Terre' => 'terre/actualites', + 'Les dernières définitions de Futura-Terre' => 'terre/definitions', + 'Les dernières questions-réponses de Futura-Terre' => 'terre/question-reponses', + 'Les derniers dossiers de Futura-Terre' => 'terre/dossiers' + ], + 'Les flux Matière' => [ + 'Les dernières actualités de Futura-Matière' => 'matiere/actualites', + 'Les dernières définitions de Futura-Matière' => 'matiere/definitions', + 'Les dernières questions-réponses de Futura-Matière' => 'matiere/question-reponses', + 'Les derniers dossiers de Futura-Matière' => 'matiere/dossiers' + ], + 'Les flux Mathématiques' => [ + 'Les dernières actualités de Futura-Mathématiques' => 'mathematiques/actualites', + 'Les derniers dossiers de Futura-Mathématiques' => 'mathematiques/dossiers' + ] + ] + ] + ]]; - public function collectData(){ - $url = self::URI . 'rss/' . $this->getInput('feed') . '.xml'; - $this->collectExpandableDatas($url, 10); - } + public function collectData() + { + $url = self::URI . 'rss/' . $this->getInput('feed') . '.xml'; + $this->collectExpandableDatas($url, 10); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $item['uri'] = str_replace('#xtor%3DRSS-8', '', $item['uri']); - $article = getSimpleHTMLDOMCached($item['uri']); - $item['content'] = $this->extractArticleContent($article); - $author = $this->extractAuthor($article); - if (!empty($author)) - $item['author'] = $author; - return $item; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $item['uri'] = str_replace('#xtor%3DRSS-8', '', $item['uri']); + $article = getSimpleHTMLDOMCached($item['uri']); + $item['content'] = $this->extractArticleContent($article); + $author = $this->extractAuthor($article); + if (!empty($author)) { + $item['author'] = $author; + } + return $item; + } - private function extractArticleContent($article){ - $contents = $article->find('section.article-text', 1); + private function extractArticleContent($article) + { + $contents = $article->find('section.article-text', 1); - foreach($contents->find('img') as $img) { - if(!empty($img->getAttribute('data-src'))) { - $img->src = $img->getAttribute('data-src'); - } - } + foreach ($contents->find('img') as $img) { + if (!empty($img->getAttribute('data-src'))) { + $img->src = $img->getAttribute('data-src'); + } + } - foreach($contents->find('a.tooltip-link') as $a) { - $a->outertext = $a->plaintext; - } + foreach ($contents->find('a.tooltip-link') as $a) { + $a->outertext = $a->plaintext; + } - foreach(array( - 'clear', - 'sharebar2', - 'diaporamafullscreen', - 'module.social-button', - 'module.social-share', - 'ficheprevnext', - 'addthis_toolbox', - 'noprint', - 'hubbottom', - 'hubbottom2' - ) as $div_class_remove) { - foreach($contents->find('div.' . $div_class_remove) as $div) { - $keep_div = false; - foreach(array( - 'didyouknow' - ) as $div_class_dont_remove) { - if(strpos($div->getAttribute('class'), $div_class_dont_remove) !== false) { - $keep_div = true; - } - } - if(!$keep_div) { - $div->outertext = ''; - } - } - } + foreach ( + [ + 'clear', + 'sharebar2', + 'diaporamafullscreen', + 'module.social-button', + 'module.social-share', + 'ficheprevnext', + 'addthis_toolbox', + 'noprint', + 'hubbottom', + 'hubbottom2' + ] as $div_class_remove + ) { + foreach ($contents->find('div.' . $div_class_remove) as $div) { + $keep_div = false; + foreach ( + [ + 'didyouknow' + ] as $div_class_dont_remove + ) { + if (strpos($div->getAttribute('class'), $div_class_dont_remove) !== false) { + $keep_div = true; + } + } + if (!$keep_div) { + $div->outertext = ''; + } + } + } - $contents = $contents->innertext; + $contents = $contents->innertext; - $contents = stripWithDelimiters($contents, '<hr ', '/>'); - $contents = stripWithDelimiters($contents, '<p class="content-date', '</p>'); - $contents = stripWithDelimiters($contents, '<h1 class="content-title', '</h1>'); - $contents = stripWithDelimiters($contents, 'fs:definition="', '"'); - $contents = stripWithDelimiters($contents, 'fs:xt:clicktype="', '"'); - $contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"'); - $contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>'); - $contents = stripWithDelimiters($contents, '<script ', '</script>'); - $contents = stripWithDelimiters($contents, '<script>', '</script>'); + $contents = stripWithDelimiters($contents, '<hr ', '/>'); + $contents = stripWithDelimiters($contents, '<p class="content-date', '</p>'); + $contents = stripWithDelimiters($contents, '<h1 class="content-title', '</h1>'); + $contents = stripWithDelimiters($contents, 'fs:definition="', '"'); + $contents = stripWithDelimiters($contents, 'fs:xt:clicktype="', '"'); + $contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"'); + $contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>'); + $contents = stripWithDelimiters($contents, '<script ', '</script>'); + $contents = stripWithDelimiters($contents, '<script>', '</script>'); - return trim($contents); - } + return trim($contents); + } - // Extracts the author from an article or element - private function extractAuthor($article){ - $article_author = $article->find('h3.epsilon', 0); - if($article_author) { - return trim(str_replace(', Futura-Sciences', '', $article_author->plaintext)); - } - return ''; - } + // Extracts the author from an article or element + private function extractAuthor($article) + { + $article_author = $article->find('h3.epsilon', 0); + if ($article_author) { + return trim(str_replace(', Futura-Sciences', '', $article_author->plaintext)); + } + return ''; + } } diff --git a/bridges/GBAtempBridge.php b/bridges/GBAtempBridge.php index 79fe313f..98cafe7d 100644 --- a/bridges/GBAtempBridge.php +++ b/bridges/GBAtempBridge.php @@ -1,148 +1,156 @@ <?php -class GBAtempBridge extends BridgeAbstract { - const MAINTAINER = 'ORelio'; - const NAME = 'GBAtemp'; - const URI = 'https://gbatemp.net/'; - const DESCRIPTION = 'GBAtemp is a user friendly underground video game community.'; +class GBAtempBridge extends BridgeAbstract +{ + const MAINTAINER = 'ORelio'; + const NAME = 'GBAtemp'; + const URI = 'https://gbatemp.net/'; + const DESCRIPTION = 'GBAtemp is a user friendly underground video game community.'; - const PARAMETERS = array( array( - 'type' => array( - 'name' => 'Type', - 'type' => 'list', - 'values' => array( - 'News' => 'N', - 'Reviews' => 'R', - 'Tutorials' => 'T', - 'Forum' => 'F' - ) - ) - )); + const PARAMETERS = [ [ + 'type' => [ + 'name' => 'Type', + 'type' => 'list', + 'values' => [ + 'News' => 'N', + 'Reviews' => 'R', + 'Tutorials' => 'T', + 'Forum' => 'F' + ] + ] + ]]; - private function buildItem($uri, $title, $author, $timestamp, $thumbnail, $content){ - $item = array(); - $item['uri'] = $uri; - $item['title'] = $title; - $item['author'] = $author; - $item['timestamp'] = $timestamp; - $item['content'] = $content; - if (!empty($thumbnail)) { - $item['enclosures'] = array($thumbnail); - } - return $item; - } + private function buildItem($uri, $title, $author, $timestamp, $thumbnail, $content) + { + $item = []; + $item['uri'] = $uri; + $item['title'] = $title; + $item['author'] = $author; + $item['timestamp'] = $timestamp; + $item['content'] = $content; + if (!empty($thumbnail)) { + $item['enclosures'] = [$thumbnail]; + } + return $item; + } - private function decodeHtmlEntities($text) { - $text = html_entity_decode($text); - $convmap = array(0x0, 0x2FFFF, 0, 0xFFFF); - return trim(mb_decode_numericentity($text, $convmap, 'UTF-8')); - } + private function decodeHtmlEntities($text) + { + $text = html_entity_decode($text); + $convmap = [0x0, 0x2FFFF, 0, 0xFFFF]; + return trim(mb_decode_numericentity($text, $convmap, 'UTF-8')); + } - private function cleanupPostContent($content, $site_url){ - $content = defaultLinkTo($content, self::URI); - $content = stripWithDelimiters($content, '<script', '</script>'); - $content = stripWithDelimiters($content, '<svg', '</svg>'); - $content = stripRecursiveHTMLSection($content, 'div', '<div class="reactionsBar'); - return $this->decodeHtmlEntities($content); - } + private function cleanupPostContent($content, $site_url) + { + $content = defaultLinkTo($content, self::URI); + $content = stripWithDelimiters($content, '<script', '</script>'); + $content = stripWithDelimiters($content, '<svg', '</svg>'); + $content = stripRecursiveHTMLSection($content, 'div', '<div class="reactionsBar'); + return $this->decodeHtmlEntities($content); + } - private function findItemDate($item){ - $time = 0; - $dateField = $item->find('time', 0); - if (is_object($dateField)) { - $time = strtotime($dateField->datetime); - } - return $time; - } + private function findItemDate($item) + { + $time = 0; + $dateField = $item->find('time', 0); + if (is_object($dateField)) { + $time = strtotime($dateField->datetime); + } + return $time; + } - private function findItemImage($item, $selector){ - $img = extractFromDelimiters($item->find($selector, 0)->style, 'url(', ')'); - $paramPos = strpos($img, '?'); - if ($paramPos !== false) { - $img = substr($img, 0, $paramPos); - } - if (!str_ends_with($img, '.png') && !str_ends_with($img, '.jpg')) { - $img = $img . '#.image'; - } - return urljoin(self::URI, $img); - } + private function findItemImage($item, $selector) + { + $img = extractFromDelimiters($item->find($selector, 0)->style, 'url(', ')'); + $paramPos = strpos($img, '?'); + if ($paramPos !== false) { + $img = substr($img, 0, $paramPos); + } + if (!str_ends_with($img, '.png') && !str_ends_with($img, '.jpg')) { + $img = $img . '#.image'; + } + return urljoin(self::URI, $img); + } - private function fetchPostContent($uri, $site_url){ - $html = getSimpleHTMLDOMCached($uri); - if(!$html) { - return 'Could not request GBAtemp: ' . $uri; - } + private function fetchPostContent($uri, $site_url) + { + $html = getSimpleHTMLDOMCached($uri); + if (!$html) { + return 'Could not request GBAtemp: ' . $uri; + } - $content = $html->find('article.message-body', 0)->innertext; - return $this->cleanupPostContent($content, $site_url); - } + $content = $html->find('article.message-body', 0)->innertext; + return $this->cleanupPostContent($content, $site_url); + } - public function collectData(){ + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $html = getSimpleHTMLDOM(self::URI); + switch ($this->getInput('type')) { + case 'N': + foreach ($html->find('li.news_item.full') as $newsItem) { + $url = urljoin(self::URI, $newsItem->find('a', 0)->href); + $img = $this->findItemImage($newsItem, 'a.news_image'); + $time = $this->findItemDate($newsItem); + $author = $newsItem->find('a.username', 0)->plaintext; + $title = $this->decodeHtmlEntities($newsItem->find('h3.news_title', 0)->plaintext); + $content = $this->fetchPostContent($url, self::URI); + $this->items[] = $this->buildItem($url, $title, $author, $time, $img, $content); + unset($newsItem); // Some items are heavy, freeing the item proactively helps saving memory + } + break; + case 'R': + foreach ($html->find('li.portal_review') as $reviewItem) { + $url = urljoin(self::URI, $reviewItem->find('a.review_boxart', 0)->href); + $img = $this->findItemImage($reviewItem, 'a.review_boxart'); + $title = $this->decodeHtmlEntities($reviewItem->find('h2.review_title', 0)->plaintext); + $content = getSimpleHTMLDOMCached($url); + $author = $content->find('span.author--name', 0)->plaintext; + $time = $this->findItemDate($content); + $intro = '<p><b>' . ($content->find('div#review_introduction', 0)->plaintext) . '</b></p>'; + $review = $content->find('div#review_main', 0)->innertext; + $content = $this->cleanupPostContent($intro . $review, self::URI); + $this->items[] = $this->buildItem($url, $title, $author, $time, $img, $content); + unset($reviewItem); // Free up memory + } + break; + case 'T': + foreach ($html->find('li.portal-tutorial') as $tutorialItem) { + $url = urljoin(self::URI, $tutorialItem->find('a', 1)->href); + $title = $this->decodeHtmlEntities($tutorialItem->find('a', 1)->plaintext); + $time = $this->findItemDate($tutorialItem); + $author = $tutorialItem->find('a.username', 0)->plaintext; + $content = $this->fetchPostContent($url, self::URI); + $this->items[] = $this->buildItem($url, $title, $author, $time, null, $content); + unset($tutorialItem); // Free up memory + } + break; + case 'F': + foreach ($html->find('li.rc_item') as $postItem) { + $url = urljoin(self::URI, $postItem->find('a', 1)->href); + $title = $this->decodeHtmlEntities($postItem->find('a', 1)->plaintext); + $time = $this->findItemDate($postItem); + $author = $postItem->find('a.username', 0)->plaintext; + $content = $this->fetchPostContent($url, self::URI); + $this->items[] = $this->buildItem($url, $title, $author, $time, null, $content); + unset($postItem); // Free up memory + } + break; + } + } - switch($this->getInput('type')) { - case 'N': - foreach($html->find('li.news_item.full') as $newsItem) { - $url = urljoin(self::URI, $newsItem->find('a', 0)->href); - $img = $this->findItemImage($newsItem, 'a.news_image'); - $time = $this->findItemDate($newsItem); - $author = $newsItem->find('a.username', 0)->plaintext; - $title = $this->decodeHtmlEntities($newsItem->find('h3.news_title', 0)->plaintext); - $content = $this->fetchPostContent($url, self::URI); - $this->items[] = $this->buildItem($url, $title, $author, $time, $img, $content); - unset($newsItem); // Some items are heavy, freeing the item proactively helps saving memory - } - break; - case 'R': - foreach($html->find('li.portal_review') as $reviewItem) { - $url = urljoin(self::URI, $reviewItem->find('a.review_boxart', 0)->href); - $img = $this->findItemImage($reviewItem, 'a.review_boxart'); - $title = $this->decodeHtmlEntities($reviewItem->find('h2.review_title', 0)->plaintext); - $content = getSimpleHTMLDOMCached($url); - $author = $content->find('span.author--name', 0)->plaintext; - $time = $this->findItemDate($content); - $intro = '<p><b>' . ($content->find('div#review_introduction', 0)->plaintext) . '</b></p>'; - $review = $content->find('div#review_main', 0)->innertext; - $content = $this->cleanupPostContent($intro . $review, self::URI); - $this->items[] = $this->buildItem($url, $title, $author, $time, $img, $content); - unset($reviewItem); // Free up memory - } - break; - case 'T': - foreach($html->find('li.portal-tutorial') as $tutorialItem) { - $url = urljoin(self::URI, $tutorialItem->find('a', 1)->href); - $title = $this->decodeHtmlEntities($tutorialItem->find('a', 1)->plaintext); - $time = $this->findItemDate($tutorialItem); - $author = $tutorialItem->find('a.username', 0)->plaintext; - $content = $this->fetchPostContent($url, self::URI); - $this->items[] = $this->buildItem($url, $title, $author, $time, null, $content); - unset($tutorialItem); // Free up memory - } - break; - case 'F': - foreach($html->find('li.rc_item') as $postItem) { - $url = urljoin(self::URI, $postItem->find('a', 1)->href); - $title = $this->decodeHtmlEntities($postItem->find('a', 1)->plaintext); - $time = $this->findItemDate($postItem); - $author = $postItem->find('a.username', 0)->plaintext; - $content = $this->fetchPostContent($url, self::URI); - $this->items[] = $this->buildItem($url, $title, $author, $time, null, $content); - unset($postItem); // Free up memory - } - break; - } - } + public function getName() + { + if (!is_null($this->getInput('type'))) { + $type = array_search( + $this->getInput('type'), + self::PARAMETERS[$this->queriedContext]['type']['values'] + ); + return 'GBAtemp ' . $type . ' Bridge'; + } - public function getName() { - if(!is_null($this->getInput('type'))) { - $type = array_search( - $this->getInput('type'), - self::PARAMETERS[$this->queriedContext]['type']['values'] - ); - return 'GBAtemp ' . $type . ' Bridge'; - } - - return parent::getName(); - } + return parent::getName(); + } } diff --git a/bridges/GOGBridge.php b/bridges/GOGBridge.php index f906d552..eacff97f 100644 --- a/bridges/GOGBridge.php +++ b/bridges/GOGBridge.php @@ -1,63 +1,62 @@ <?php -class GOGBridge extends BridgeAbstract { - const NAME = 'GOGBridge'; - const MAINTAINER = 'teromene'; - const URI = 'https://gog.com'; - const DESCRIPTION = 'Returns the latest releases from GOG.com'; - - public function collectData() { - - $values = getContents('https://www.gog.com/games/ajax/filtered?limit=25&sort=new'); - $decodedValues = json_decode($values); - - $limit = 0; - foreach($decodedValues->products as $game) { - - $item = array(); - $item['author'] = $game->developer . ' / ' . $game->publisher; - $item['title'] = $game->title; - $item['id'] = $game->id; - $item['uri'] = self::URI . $game->url; - $item['content'] = $this->buildGameContentPage($game); - $item['timestamp'] = $game->globalReleaseDate; - - foreach($game->gallery as $image) { - $item['enclosures'][] = $image . '.jpg'; - } - - $this->items[] = $item; - $limit += 1; - - if($limit == 10) break; - - } - - } - - private function buildGameContentPage($game) { - - $gameDescriptionText = getContents('https://api.gog.com/products/' . $game->id . '?expand=description'); - - $gameDescriptionValue = json_decode($gameDescriptionText); - - $content = 'Genres: '; - $content .= implode(', ', $game->genres); - - $content .= '<br />Supported Platforms: '; - if($game->worksOn->Windows) { - $content .= 'Windows '; - } - if($game->worksOn->Mac) { - $content .= 'Mac '; - } - if($game->worksOn->Linux) { - $content .= 'Linux '; - } - - $content .= '<br />' . $gameDescriptionValue->description->full; - - return $content; - - } +class GOGBridge extends BridgeAbstract +{ + const NAME = 'GOGBridge'; + const MAINTAINER = 'teromene'; + const URI = 'https://gog.com'; + const DESCRIPTION = 'Returns the latest releases from GOG.com'; + + public function collectData() + { + $values = getContents('https://www.gog.com/games/ajax/filtered?limit=25&sort=new'); + $decodedValues = json_decode($values); + + $limit = 0; + foreach ($decodedValues->products as $game) { + $item = []; + $item['author'] = $game->developer . ' / ' . $game->publisher; + $item['title'] = $game->title; + $item['id'] = $game->id; + $item['uri'] = self::URI . $game->url; + $item['content'] = $this->buildGameContentPage($game); + $item['timestamp'] = $game->globalReleaseDate; + + foreach ($game->gallery as $image) { + $item['enclosures'][] = $image . '.jpg'; + } + + $this->items[] = $item; + $limit += 1; + + if ($limit == 10) { + break; + } + } + } + + private function buildGameContentPage($game) + { + $gameDescriptionText = getContents('https://api.gog.com/products/' . $game->id . '?expand=description'); + + $gameDescriptionValue = json_decode($gameDescriptionText); + + $content = 'Genres: '; + $content .= implode(', ', $game->genres); + + $content .= '<br />Supported Platforms: '; + if ($game->worksOn->Windows) { + $content .= 'Windows '; + } + if ($game->worksOn->Mac) { + $content .= 'Mac '; + } + if ($game->worksOn->Linux) { + $content .= 'Linux '; + } + + $content .= '<br />' . $gameDescriptionValue->description->full; + + return $content; + } } diff --git a/bridges/GQMagazineBridge.php b/bridges/GQMagazineBridge.php index cacd6159..256cfeb5 100644 --- a/bridges/GQMagazineBridge.php +++ b/bridges/GQMagazineBridge.php @@ -9,131 +9,137 @@ */ class GQMagazineBridge extends BridgeAbstract { - const MAINTAINER = 'Riduidel'; - - const NAME = 'GQMagazine'; - - // URI is no more valid, since we can address the whole gq galaxy - const URI = 'https://www.gqmagazine.fr'; - - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'GQMagazine section extractor bridge. This bridge allows you get only a specific section.'; - - const DEFAULT_DOMAIN = 'www.gqmagazine.fr'; - - const PARAMETERS = array( array( - 'domain' => array( - 'name' => 'Domain to use', - 'required' => true, - 'defaultValue' => self::DEFAULT_DOMAIN - ), - 'page' => array( - 'name' => 'Initial page to load', - 'required' => true, - 'exampleValue' => 'sexe/news' - ), - 'limit' => self::LIMIT, - )); - - const REPLACED_ATTRIBUTES = array( - 'href' => 'href', - 'src' => 'src', - 'data-original' => 'src' - ); - - const POSSIBLE_TITLES = array( - 'h2', - 'h3' - ); - - private function getDomain() { - $domain = $this->getInput('domain'); - if (empty($domain)) - $domain = self::DEFAULT_DOMAIN; - if (strpos($domain, '://') === false) - $domain = 'https://' . $domain; - return $domain; - } - - public function getURI() - { - return $this->getDomain() . '/' . $this->getInput('page'); - } - - private function findTitleOf($link) { - foreach (self::POSSIBLE_TITLES as $tag) { - $title = $link->parent()->find($tag, 0); - if($title !== null) { - if($title->plaintext !== null) { - return $title->plaintext; - } - } - } - } - - public function collectData() - { - $html = getSimpleHTMLDOM($this->getURI()); - - // Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content ! - $main = $html->find('main', 0); - $limit = $this->getInput('limit') ?? 10; - foreach ($main->find('a') as $link) { - if (count($this->items) >= $limit) { - break; - } - - $uri = $link->href; - $date = $link->parent()->find('time', 0); - - $item = array(); - $author = $link->parent()->find('span[itemprop=name]', 0); - if($author !== null) { - $item['author'] = $author->plaintext; - $item['title'] = $this->findTitleOf($link); - switch(substr($uri, 0, 1)) { - case 'h': // absolute uri - $item['uri'] = $uri; - break; - case '/': // domain relative uri - $item['uri'] = $this->getDomain() . $uri; - break; - default: - $item['uri'] = $this->getDomain() . '/' . $uri; - } - $article = $this->loadFullArticle($item['uri']); - if($article) { - $item['content'] = $this->replaceUriInHtmlElement($article); - } else { - $item['content'] = "<strong>Article body couldn't be loaded</strong>. It must be a bug!"; - } - $short_date = $date->datetime; - $item['timestamp'] = strtotime($short_date); - $this->items[] = $item; - } - } - } - - /** - * Loads the full article and returns the contents - * @param $uri The article URI - * @return The article content - */ - private function loadFullArticle($uri){ - $html = getSimpleHTMLDOMCached($uri); - return $html->find('article', 0); - } - - /** - * Replaces all relative URIs with absolute ones - * @param $element A simplehtmldom element - * @return The $element->innertext with all URIs replaced - */ - private function replaceUriInHtmlElement($element){ - $returned = $element->innertext; - foreach (self::REPLACED_ATTRIBUTES as $initial => $final) { - $returned = str_replace($initial . '="/', $final . '="' . self::URI . '/', $returned); - } - return $returned; - } + const MAINTAINER = 'Riduidel'; + + const NAME = 'GQMagazine'; + + // URI is no more valid, since we can address the whole gq galaxy + const URI = 'https://www.gqmagazine.fr'; + + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'GQMagazine section extractor bridge. This bridge allows you get only a specific section.'; + + const DEFAULT_DOMAIN = 'www.gqmagazine.fr'; + + const PARAMETERS = [ [ + 'domain' => [ + 'name' => 'Domain to use', + 'required' => true, + 'defaultValue' => self::DEFAULT_DOMAIN + ], + 'page' => [ + 'name' => 'Initial page to load', + 'required' => true, + 'exampleValue' => 'sexe/news' + ], + 'limit' => self::LIMIT, + ]]; + + const REPLACED_ATTRIBUTES = [ + 'href' => 'href', + 'src' => 'src', + 'data-original' => 'src' + ]; + + const POSSIBLE_TITLES = [ + 'h2', + 'h3' + ]; + + private function getDomain() + { + $domain = $this->getInput('domain'); + if (empty($domain)) { + $domain = self::DEFAULT_DOMAIN; + } + if (strpos($domain, '://') === false) { + $domain = 'https://' . $domain; + } + return $domain; + } + + public function getURI() + { + return $this->getDomain() . '/' . $this->getInput('page'); + } + + private function findTitleOf($link) + { + foreach (self::POSSIBLE_TITLES as $tag) { + $title = $link->parent()->find($tag, 0); + if ($title !== null) { + if ($title->plaintext !== null) { + return $title->plaintext; + } + } + } + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + // Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content ! + $main = $html->find('main', 0); + $limit = $this->getInput('limit') ?? 10; + foreach ($main->find('a') as $link) { + if (count($this->items) >= $limit) { + break; + } + + $uri = $link->href; + $date = $link->parent()->find('time', 0); + + $item = []; + $author = $link->parent()->find('span[itemprop=name]', 0); + if ($author !== null) { + $item['author'] = $author->plaintext; + $item['title'] = $this->findTitleOf($link); + switch (substr($uri, 0, 1)) { + case 'h': // absolute uri + $item['uri'] = $uri; + break; + case '/': // domain relative uri + $item['uri'] = $this->getDomain() . $uri; + break; + default: + $item['uri'] = $this->getDomain() . '/' . $uri; + } + $article = $this->loadFullArticle($item['uri']); + if ($article) { + $item['content'] = $this->replaceUriInHtmlElement($article); + } else { + $item['content'] = "<strong>Article body couldn't be loaded</strong>. It must be a bug!"; + } + $short_date = $date->datetime; + $item['timestamp'] = strtotime($short_date); + $this->items[] = $item; + } + } + } + + /** + * Loads the full article and returns the contents + * @param $uri The article URI + * @return The article content + */ + private function loadFullArticle($uri) + { + $html = getSimpleHTMLDOMCached($uri); + return $html->find('article', 0); + } + + /** + * Replaces all relative URIs with absolute ones + * @param $element A simplehtmldom element + * @return The $element->innertext with all URIs replaced + */ + private function replaceUriInHtmlElement($element) + { + $returned = $element->innertext; + foreach (self::REPLACED_ATTRIBUTES as $initial => $final) { + $returned = str_replace($initial . '="/', $final . '="' . self::URI . '/', $returned); + } + return $returned; + } } diff --git a/bridges/GatesNotesBridge.php b/bridges/GatesNotesBridge.php index bf456d26..8c988fcb 100644 --- a/bridges/GatesNotesBridge.php +++ b/bridges/GatesNotesBridge.php @@ -1,54 +1,55 @@ <?php -class GatesNotesBridge extends FeedExpander { - - const MAINTAINER = 'corenting'; - const NAME = 'Gates Notes'; - const URI = 'https://www.gatesnotes.com'; - const DESCRIPTION = 'Returns the newest articles.'; - const CACHE_TIMEOUT = 21600; // 6h - - protected function parseItem($item){ - $item = parent::parseItem($item); - - $article_html = getSimpleHTMLDOMCached($item['uri']); - if(!$article_html) { - $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; - return $item; - } - $article_html = defaultLinkTo($article_html, $this->getURI()); - - $top_description = '<p>' . $article_html->find('div.article_top_description', 0)->innertext . '</p>'; - $hero_image = '<img src=' . $article_html->find('img.article_top_DMT_Image', 0)->getAttribute('data-src') . '>'; - - $article_body = $article_html->find('div.TGN_Article_ReadTimeSection', 0); - // Convert iframe of Youtube videos to link - foreach($article_body->find('iframe') as $found) { - - $iframeUrl = $found->getAttribute('src'); - - if ($iframeUrl) { - $text = 'Embedded Youtube video, click here to watch on Youtube.com'; - $found->outertext = '<p><a href="' . $iframeUrl . '">' . $text . '</a></p>'; - } - } - // Remove <link> CSS ressources - foreach($article_body->find('link') as $found) { - - $linkedRessourceUrl = $found->getAttribute('href'); - - if (str_ends_with($linkedRessourceUrl, '.css')) { - $found->outertext = ''; - } - } - $article_body = sanitize($article_body->innertext); - - $item['content'] = $top_description . $hero_image . $article_body; - - return $item; - } - - public function collectData(){ - $feed = static::URI . '/rss'; - $this->collectExpandableDatas($feed); - } + +class GatesNotesBridge extends FeedExpander +{ + const MAINTAINER = 'corenting'; + const NAME = 'Gates Notes'; + const URI = 'https://www.gatesnotes.com'; + const DESCRIPTION = 'Returns the newest articles.'; + const CACHE_TIMEOUT = 21600; // 6h + + protected function parseItem($item) + { + $item = parent::parseItem($item); + + $article_html = getSimpleHTMLDOMCached($item['uri']); + if (!$article_html) { + $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; + return $item; + } + $article_html = defaultLinkTo($article_html, $this->getURI()); + + $top_description = '<p>' . $article_html->find('div.article_top_description', 0)->innertext . '</p>'; + $hero_image = '<img src=' . $article_html->find('img.article_top_DMT_Image', 0)->getAttribute('data-src') . '>'; + + $article_body = $article_html->find('div.TGN_Article_ReadTimeSection', 0); + // Convert iframe of Youtube videos to link + foreach ($article_body->find('iframe') as $found) { + $iframeUrl = $found->getAttribute('src'); + + if ($iframeUrl) { + $text = 'Embedded Youtube video, click here to watch on Youtube.com'; + $found->outertext = '<p><a href="' . $iframeUrl . '">' . $text . '</a></p>'; + } + } + // Remove <link> CSS ressources + foreach ($article_body->find('link') as $found) { + $linkedRessourceUrl = $found->getAttribute('href'); + + if (str_ends_with($linkedRessourceUrl, '.css')) { + $found->outertext = ''; + } + } + $article_body = sanitize($article_body->innertext); + + $item['content'] = $top_description . $hero_image . $article_body; + + return $item; + } + + public function collectData() + { + $feed = static::URI . '/rss'; + $this->collectExpandableDatas($feed); + } } diff --git a/bridges/GelbooruBridge.php b/bridges/GelbooruBridge.php index 7dcd44fc..93518843 100644 --- a/bridges/GelbooruBridge.php +++ b/bridges/GelbooruBridge.php @@ -1,88 +1,93 @@ <?php -class GelbooruBridge extends BridgeAbstract { - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Gelbooru'; - const URI = 'https://gelbooru.com/'; - const DESCRIPTION = 'Returns images from given page'; +class GelbooruBridge extends BridgeAbstract +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Gelbooru'; + const URI = 'https://gelbooru.com/'; + const DESCRIPTION = 'Returns images from given page'; - const PARAMETERS = array( - 'global' => array( - 'p' => array( - 'name' => 'page', - 'defaultValue' => 0, - 'type' => 'number' - ), - 't' => array( - 'name' => 'tags', - 'exampleValue' => 'solo', - 'title' => 'Tags to search for' - ), - 'l' => array( - 'name' => 'limit', - 'exampleValue' => 100, - 'title' => 'How many posts to retrieve (hard limit of 1000)' - ) - ), - 0 => array() - ); + const PARAMETERS = [ + 'global' => [ + 'p' => [ + 'name' => 'page', + 'defaultValue' => 0, + 'type' => 'number' + ], + 't' => [ + 'name' => 'tags', + 'exampleValue' => 'solo', + 'title' => 'Tags to search for' + ], + 'l' => [ + 'name' => 'limit', + 'exampleValue' => 100, + 'title' => 'How many posts to retrieve (hard limit of 1000)' + ] + ], + 0 => [] + ]; - protected function getFullURI(){ - return $this->getURI() - . 'index.php?&page=dapi&s=post&q=index&json=1&pid=' . $this->getInput('p') - . '&limit=' . $this->getInput('l') - . '&tags=' . urlencode($this->getInput('t')); - } + protected function getFullURI() + { + return $this->getURI() + . 'index.php?&page=dapi&s=post&q=index&json=1&pid=' . $this->getInput('p') + . '&limit=' . $this->getInput('l') + . '&tags=' . urlencode($this->getInput('t')); + } - /* - This function is superfluous for GelbooruBridge, but useful - for Bridges that inherit from it - */ - protected function buildThumbnailURI($element){ - return $this->getURI() . 'thumbnails/' . $element->directory - . '/thumbnail_' . $element->md5 . '.jpg'; - } + /* + This function is superfluous for GelbooruBridge, but useful + for Bridges that inherit from it + */ + protected function buildThumbnailURI($element) + { + return $this->getURI() . 'thumbnails/' . $element->directory + . '/thumbnail_' . $element->md5 . '.jpg'; + } - protected function getItemFromElement($element){ - $item = array(); - $item['uri'] = $this->getURI() . 'index.php?page=post&s=view&id=' - . $element->id; - $item['postid'] = $element->id; - $item['author'] = $element->owner; - $item['timestamp'] = date('d F Y H:i:s', $element->change); - $item['tags'] = $element->tags; - $item['title'] = $this->getName() . ' | ' . $item['postid']; + protected function getItemFromElement($element) + { + $item = []; + $item['uri'] = $this->getURI() . 'index.php?page=post&s=view&id=' + . $element->id; + $item['postid'] = $element->id; + $item['author'] = $element->owner; + $item['timestamp'] = date('d F Y H:i:s', $element->change); + $item['tags'] = $element->tags; + $item['title'] = $this->getName() . ' | ' . $item['postid']; - if (isset($element->preview_url)) { - $thumbnailUri = $element->preview_url; - } else{ - $thumbnailUri = $this->buildThumbnailURI($element); - } + if (isset($element->preview_url)) { + $thumbnailUri = $element->preview_url; + } else { + $thumbnailUri = $this->buildThumbnailURI($element); + } - $item['content'] = '<a href="' . $item['uri'] . '"><img src="' - . $thumbnailUri . '" /></a><br><br><b>Tags:</b> ' - . $item['tags'] . '<br><br>' . $item['timestamp']; + $item['content'] = '<a href="' . $item['uri'] . '"><img src="' + . $thumbnailUri . '" /></a><br><br><b>Tags:</b> ' + . $item['tags'] . '<br><br>' . $item['timestamp']; - return $item; - } + return $item; + } - public function collectData(){ - $content = getContents($this->getFullURI()); - // $content is empty string + public function collectData() + { + $content = getContents($this->getFullURI()); + // $content is empty string - // Most other Gelbooru-based boorus put their content in the root of - // the JSON. This check is here for Bridges that inherit from this one - $posts = json_decode($content); - if (isset($posts->post)) { - $posts = $posts->post; - } + // Most other Gelbooru-based boorus put their content in the root of + // the JSON. This check is here for Bridges that inherit from this one + $posts = json_decode($content); + if (isset($posts->post)) { + $posts = $posts->post; + } - if (is_null($posts)) { - returnServerError('No posts found.'); - } + if (is_null($posts)) { + returnServerError('No posts found.'); + } - foreach($posts as $post) { - $this->items[] = $this->getItemFromElement($post); - } - } + foreach ($posts as $post) { + $this->items[] = $this->getItemFromElement($post); + } + } } diff --git a/bridges/GenshinImpactBridge.php b/bridges/GenshinImpactBridge.php index 01ea12f0..cfa3dfe3 100644 --- a/bridges/GenshinImpactBridge.php +++ b/bridges/GenshinImpactBridge.php @@ -1,69 +1,73 @@ <?php -class GenshinImpactBridge extends BridgeAbstract { - const MAINTAINER = 'corenting'; - const NAME = 'Genshin Impact'; - const URI = 'https://genshin.mihoyo.com/en/news'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'News from the Genshin Impact website'; - const PARAMETERS = array( - array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'Latest' => 10, - 'Info' => 11, - 'Updates' => 12, - 'Events' => 13 - ), - 'defaultValue' => 10 - ) - ) - ); +class GenshinImpactBridge extends BridgeAbstract +{ + const MAINTAINER = 'corenting'; + const NAME = 'Genshin Impact'; + const URI = 'https://genshin.mihoyo.com/en/news'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'News from the Genshin Impact website'; + const PARAMETERS = [ + [ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'Latest' => 10, + 'Info' => 11, + 'Updates' => 12, + 'Events' => 13 + ], + 'defaultValue' => 10 + ] + ] + ]; - public function collectData(){ - $category = $this->getInput('category'); + public function collectData() + { + $category = $this->getInput('category'); - $url = 'https://genshin.mihoyo.com/content/yuanshen/getContentList'; - $url = $url . '?pageSize=3&pageNum=1&channelId=' . $category; - $api_response = getContents($url); - $json_list = json_decode($api_response, true); + $url = 'https://genshin.mihoyo.com/content/yuanshen/getContentList'; + $url = $url . '?pageSize=3&pageNum=1&channelId=' . $category; + $api_response = getContents($url); + $json_list = json_decode($api_response, true); - foreach($json_list['data']['list'] as $json_item) { - $article_url = 'https://genshin.mihoyo.com/content/yuanshen/getContent'; - $article_url = $article_url . '?contentId=' . $json_item['contentId']; - $article_res = getContents($article_url); - $article_json = json_decode($article_res, true); - $article_time = $article_json['data']['start_time']; - $timezone = 'Asia/Shanghai'; - $article_timestamp = new DateTime($article_time, new DateTimeZone($timezone)); + foreach ($json_list['data']['list'] as $json_item) { + $article_url = 'https://genshin.mihoyo.com/content/yuanshen/getContent'; + $article_url = $article_url . '?contentId=' . $json_item['contentId']; + $article_res = getContents($article_url); + $article_json = json_decode($article_res, true); + $article_time = $article_json['data']['start_time']; + $timezone = 'Asia/Shanghai'; + $article_timestamp = new DateTime($article_time, new DateTimeZone($timezone)); - $item = array(); + $item = []; - $item['title'] = $article_json['data']['title']; - $item['timestamp'] = $article_timestamp->format('U'); - $item['content'] = $article_json['data']['content']; - $item['uri'] = $this->getArticleUri($json_item); - $item['id'] = $json_item['contentId']; + $item['title'] = $article_json['data']['title']; + $item['timestamp'] = $article_timestamp->format('U'); + $item['content'] = $article_json['data']['content']; + $item['uri'] = $this->getArticleUri($json_item); + $item['id'] = $json_item['contentId']; - // Picture - foreach($article_json['data']['ext'] as $ext) { - if ($ext['arrtName'] == 'banner' && count($ext['value']) == 1) { - $item['enclosures'] = array($ext['value'][0]['url']); - break; - } - } + // Picture + foreach ($article_json['data']['ext'] as $ext) { + if ($ext['arrtName'] == 'banner' && count($ext['value']) == 1) { + $item['enclosures'] = [$ext['value'][0]['url']]; + break; + } + } - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getIcon() { - return 'https://genshin.mihoyo.com/favicon.ico'; - } + public function getIcon() + { + return 'https://genshin.mihoyo.com/favicon.ico'; + } - private function getArticleUri($json_item) { - return 'https://genshin.mihoyo.com/en/news/detail/' . $json_item['contentId']; - } + private function getArticleUri($json_item) + { + return 'https://genshin.mihoyo.com/en/news/detail/' . $json_item['contentId']; + } } diff --git a/bridges/GettrBridge.php b/bridges/GettrBridge.php index 5ecc5c83..2b019523 100644 --- a/bridges/GettrBridge.php +++ b/bridges/GettrBridge.php @@ -2,72 +2,72 @@ class GettrBridge extends BridgeAbstract { - const NAME = 'Gettr.com bridge'; - const URI = 'https://gettr.com'; - const DESCRIPTION = 'Fetches the latest posts from a GETTR user'; - const MAINTAINER = 'dvikan'; - const CACHE_TIMEOUT = 60 * 15; // 15m - const PARAMETERS = [ - [ - 'user' => [ - 'name' => 'User', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'joerogan', - ], - 'limit' => [ - 'name' => 'Limit', - 'type' => 'number', - 'title' => 'Maximum number of items to return (maximum 20)', - 'defaultValue' => 5, - 'required' => true, - ], - ] - ]; + const NAME = 'Gettr.com bridge'; + const URI = 'https://gettr.com'; + const DESCRIPTION = 'Fetches the latest posts from a GETTR user'; + const MAINTAINER = 'dvikan'; + const CACHE_TIMEOUT = 60 * 15; // 15m + const PARAMETERS = [ + [ + 'user' => [ + 'name' => 'User', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'joerogan', + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'title' => 'Maximum number of items to return (maximum 20)', + 'defaultValue' => 5, + 'required' => true, + ], + ] + ]; - public function collectData() - { - $api = sprintf( - 'https://api.gettr.com/u/user/%s/posts?offset=0&max=%s&dir=fwd&incl=posts&fp=f_uo', - $this->getInput('user'), - min($this->getInput('limit'), 20) - ); - $data = json_decode(getContents($api), false); + public function collectData() + { + $api = sprintf( + 'https://api.gettr.com/u/user/%s/posts?offset=0&max=%s&dir=fwd&incl=posts&fp=f_uo', + $this->getInput('user'), + min($this->getInput('limit'), 20) + ); + $data = json_decode(getContents($api), false); - foreach ($data->result->aux->post as $post) { - $this->items[] = [ - 'title' => mb_substr($post->txt ?? $post->uid . '@gettr.com', 0, 100), - 'uri' => sprintf('https://gettr.com/post/%s', $post->_id), - 'author' => $post->uid, - // Convert from ms to s - 'timestamp' => substr($post->cdate, 0, strlen($post->cdate) - 3), - 'uid' => $post->_id, - // Hashtags found within post text - 'categories' => $post->htgs ?? [], - 'content' => $this->createContent($post), - ]; - } - } + foreach ($data->result->aux->post as $post) { + $this->items[] = [ + 'title' => mb_substr($post->txt ?? $post->uid . '@gettr.com', 0, 100), + 'uri' => sprintf('https://gettr.com/post/%s', $post->_id), + 'author' => $post->uid, + // Convert from ms to s + 'timestamp' => substr($post->cdate, 0, strlen($post->cdate) - 3), + 'uid' => $post->_id, + // Hashtags found within post text + 'categories' => $post->htgs ?? [], + 'content' => $this->createContent($post), + ]; + } + } - /** - * Collect text, image and video, if they exist - */ - private function createContent(\stdClass $post): string - { - $content = ''; + /** + * Collect text, image and video, if they exist + */ + private function createContent(\stdClass $post): string + { + $content = ''; - // Text - if (isset($post->txt)) { - $isRepost = $this->getInput('user') !== $post->uid; - if ($isRepost) { - $content .= 'Reposted by ' . $this->getInput('user') . '@gettr.com<br><br>'; - } - $content .= "$post->txt <br><br>"; - } + // Text + if (isset($post->txt)) { + $isRepost = $this->getInput('user') !== $post->uid; + if ($isRepost) { + $content .= 'Reposted by ' . $this->getInput('user') . '@gettr.com<br><br>'; + } + $content .= "$post->txt <br><br>"; + } - // Preview image - if (isset($post->previmg)) { - $content .= <<<HTML + // Preview image + if (isset($post->previmg)) { + $content .= <<<HTML <a href="$post->prevsrc" target="_blank"> <img src='$post->previmg' @@ -77,11 +77,11 @@ class GettrBridge extends BridgeAbstract </a> <br><br> HTML; - } + } - // Images - foreach ($post->imgs ?? [] as $imageUrl) { - $content .= <<<HTML + // Images + foreach ($post->imgs ?? [] as $imageUrl) { + $content .= <<<HTML <img src='https://media.gettr.com/$imageUrl' alt='Unable to load image' @@ -89,13 +89,13 @@ HTML; > <br><br> HTML; - } + } - // Video - if (isset($post->ovid)) { - $mainImage = $post->main; + // Video + if (isset($post->ovid)) { + $mainImage = $post->main; - $content .= <<<HTML + $content .= <<<HTML <video style="max-width: 100%" controls @@ -106,30 +106,30 @@ HTML; Your browser does not support the video element. Kindly update it to latest version. </video > HTML; - // This is typically a m3u8 which I don't know how to present in a browser - $streamingUrl = $post->vid; - } - $this->processMetadata($post); + // This is typically a m3u8 which I don't know how to present in a browser + $streamingUrl = $post->vid; + } + $this->processMetadata($post); - return $content; - } + return $content; + } - public function getIcon() - { - return 'https://gettr.com/favicon.ico'; - } + public function getIcon() + { + return 'https://gettr.com/favicon.ico'; + } - /** - * @param stdClass $post - */ - private function processMetadata(stdClass $post): void - { - // Unused metadata, maybe used later - $textLanguage = $post->txt_lang ?? 'en'; - $replies = $post->cm ?? 0; - $likes = $post->lkbpst ?? 0; - $reposts = $post->shbpst ?? 0; - // I think a visibility of "p" means that it's public - $visibility = $post->vis ?? 'p'; - } + /** + * @param stdClass $post + */ + private function processMetadata(stdClass $post): void + { + // Unused metadata, maybe used later + $textLanguage = $post->txt_lang ?? 'en'; + $replies = $post->cm ?? 0; + $likes = $post->lkbpst ?? 0; + $reposts = $post->shbpst ?? 0; + // I think a visibility of "p" means that it's public + $visibility = $post->vis ?? 'p'; + } } diff --git a/bridges/GiphyBridge.php b/bridges/GiphyBridge.php index f823246b..4692e183 100644 --- a/bridges/GiphyBridge.php +++ b/bridges/GiphyBridge.php @@ -1,56 +1,57 @@ <?php -class GiphyBridge extends BridgeAbstract { +class GiphyBridge extends BridgeAbstract +{ + const MAINTAINER = 'dvikan'; + const NAME = 'Giphy Bridge'; + const URI = 'https://giphy.com/'; + const CACHE_TIMEOUT = 60 * 60 * 8; // 8h + const DESCRIPTION = 'Bridge for giphy.com'; - const MAINTAINER = 'dvikan'; - const NAME = 'Giphy Bridge'; - const URI = 'https://giphy.com/'; - const CACHE_TIMEOUT = 60 * 60 * 8; // 8h - const DESCRIPTION = 'Bridge for giphy.com'; + const PARAMETERS = [ [ + 's' => [ + 'name' => 'search tag', + 'exampleValue' => 'bird', + 'required' => true + ], + 'noGif' => [ + 'name' => 'Without gifs', + 'type' => 'checkbox', + 'title' => 'Exclude gifs from the results' + ], + 'noStick' => [ + 'name' => 'Without stickers', + 'type' => 'checkbox', + 'title' => 'Exclude stickers from the results' + ], + 'n' => [ + 'name' => 'max number of returned items (max 50)', + 'type' => 'number', + 'exampleValue' => 3, + ] + ]]; - const PARAMETERS = array( array( - 's' => array( - 'name' => 'search tag', - 'exampleValue' => 'bird', - 'required' => true - ), - 'noGif' => array( - 'name' => 'Without gifs', - 'type' => 'checkbox', - 'title' => 'Exclude gifs from the results' - ), - 'noStick' => array( - 'name' => 'Without stickers', - 'type' => 'checkbox', - 'title' => 'Exclude stickers from the results' - ), - 'n' => array( - 'name' => 'max number of returned items (max 50)', - 'type' => 'number', - 'exampleValue' => 3, - ) - )); + public function getName() + { + if (!is_null($this->getInput('s'))) { + return $this->getInput('s') . ' - ' . parent::getName(); + } - public function getName() - { - if (!is_null($this->getInput('s'))) { - return $this->getInput('s') . ' - ' . parent::getName(); - } + return parent::getName(); + } - return parent::getName(); - } + protected function getGiphyItems($entries) + { + foreach ($entries as $entry) { + $createdAt = new \DateTime($entry->import_datetime); - protected function getGiphyItems($entries){ - foreach($entries as $entry) { - $createdAt = new \DateTime($entry->import_datetime); - - $this->items[] = array( - 'id' => $entry->id, - 'uri' => $entry->url, - 'author' => $entry->username, - 'timestamp' => $createdAt->format('U'), - 'title' => $entry->title, - 'content' => <<<HTML + $this->items[] = [ + 'id' => $entry->id, + 'uri' => $entry->url, + 'author' => $entry->username, + 'timestamp' => $createdAt->format('U'), + 'title' => $entry->title, + 'content' => <<<HTML <a href="{$entry->url}"> <img loading="lazy" @@ -59,48 +60,49 @@ class GiphyBridge extends BridgeAbstract { height="{$entry->images->downsized->height}" /> </a> HTML - ); - } - } + ]; + } + } - public function collectData() { - /** - * This uses Giphy's own undocumented public prod api key, - * which should not have any rate limiting. - * There is a documented public beta api key (dc6zaTOxFJmzC), - * but it has severe rate limiting. - * - * https://giphy.api-docs.io/1.0/welcome/access-and-api-keys - * https://developers.giphy.com/branch/master/docs/api/endpoint/#search - */ - $apiKey = 'Gc7131jiJuvI7IdN0HZ1D7nh0ow5BU6g'; - $bundle = 'low_bandwidth'; - $limit = min($this->getInput('n') ?: 10, 50); - $endpoints = array(); - if (empty($this->getInput('noGif'))) { - $endpoints[] = 'gifs'; - } - if (empty($this->getInput('noStick'))) { - $endpoints[] = 'stickers'; - } + public function collectData() + { + /** + * This uses Giphy's own undocumented public prod api key, + * which should not have any rate limiting. + * There is a documented public beta api key (dc6zaTOxFJmzC), + * but it has severe rate limiting. + * + * https://giphy.api-docs.io/1.0/welcome/access-and-api-keys + * https://developers.giphy.com/branch/master/docs/api/endpoint/#search + */ + $apiKey = 'Gc7131jiJuvI7IdN0HZ1D7nh0ow5BU6g'; + $bundle = 'low_bandwidth'; + $limit = min($this->getInput('n') ?: 10, 50); + $endpoints = []; + if (empty($this->getInput('noGif'))) { + $endpoints[] = 'gifs'; + } + if (empty($this->getInput('noStick'))) { + $endpoints[] = 'stickers'; + } - foreach ($endpoints as $endpoint) { - $uri = sprintf( - 'https://api.giphy.com/v1/%s/search?q=%s&limit=%s&bundle=%s&api_key=%s', - $endpoint, - rawurlencode($this->getInput('s')), - $limit, - $bundle, - $apiKey - ); + foreach ($endpoints as $endpoint) { + $uri = sprintf( + 'https://api.giphy.com/v1/%s/search?q=%s&limit=%s&bundle=%s&api_key=%s', + $endpoint, + rawurlencode($this->getInput('s')), + $limit, + $bundle, + $apiKey + ); - $result = json_decode(getContents($uri)); + $result = json_decode(getContents($uri)); - $this->getGiphyItems($result->data); - } + $this->getGiphyItems($result->data); + } - usort($this->items, function ($a, $b) { - return $a['timestamp'] < $b['timestamp']; - }); - } + usort($this->items, function ($a, $b) { + return $a['timestamp'] < $b['timestamp']; + }); + } } diff --git a/bridges/GitHubGistBridge.php b/bridges/GitHubGistBridge.php index 5760d8a0..969ee3be 100644 --- a/bridges/GitHubGistBridge.php +++ b/bridges/GitHubGistBridge.php @@ -1,110 +1,106 @@ <?php -class GitHubGistBridge extends BridgeAbstract { - - const NAME = 'GitHubGist comment bridge'; - const URI = 'https://gist.github.com'; - const DESCRIPTION = 'Generates feeds for Gist comments'; - const MAINTAINER = 'logmanoriginal'; - const CACHE_TIMEOUT = 3600; - - const PARAMETERS = array(array( - 'id' => array( - 'name' => 'Gist', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert Gist ID or URI', - 'exampleValue' => '2646763' - ) - )); - - private $filename; - - public function getURI() { - - $id = $this->getInput('id') ?: ''; - - $urlpath = parse_url($id, PHP_URL_PATH); - - if($urlpath) { - - $components = explode('/', $urlpath); - $id = end($components); - - } - - return static::URI . '/' . $id; - - } - - public function getName() { - return $this->filename ? $this->filename . ' - ' . static::NAME : static::NAME; - } - - public function collectData() { - - $html = getSimpleHTMLDOM($this->getURI(), - null, - null, - true, - true, - DEFAULT_TARGET_CHARSET, - false, // Do NOT remove line breaks - DEFAULT_BR_TEXT, - DEFAULT_SPAN_TEXT); - - $html = defaultLinkTo($html, $this->getURI()); - - $fileinfo = $html->find('[class~="file-info"]', 0) - or returnServerError('Could not find file info!'); - - $this->filename = $fileinfo->plaintext; - - $comments = $html->find('div[class~="TimelineItem"]'); - - if(is_null($comments)) { // no comments yet - return; - } - - foreach($comments as $comment) { - - $uri = $comment->find('a[href*=#gistcomment]', 0) - or returnServerError('Could not find comment anchor!'); - - $title = $comment->find('h3', 0); - - $datetime = $comment->find('[datetime]', 0) - or returnServerError('Could not find comment datetime!'); - - $author = $comment->find('a.author', 0) - or returnServerError('Could not find author name!'); - - $message = $comment->find('[class~="comment-body"]', 0) - or returnServerError('Could not find comment body!'); - - $item = array(); - - $item['uri'] = $uri->href; - $item['title'] = str_replace('commented', 'commented on', $title->plaintext ?? ''); - $item['timestamp'] = strtotime($datetime->datetime); - $item['author'] = '<a href="' . $author->href . '">' . $author->plaintext . '</a>'; - $item['content'] = $this->fixContent($message); - // $item['enclosures'] = array(); - // $item['categories'] = array(); - - $this->items[] = $item; - - } - - } - - /** Removes all unnecessary tags and adds formatting */ - private function fixContent($content){ - - // Restore code (inside <pre />) highlighting - foreach($content->find('pre') as $pre) { - - $pre->style = <<<EOD +class GitHubGistBridge extends BridgeAbstract +{ + const NAME = 'GitHubGist comment bridge'; + const URI = 'https://gist.github.com'; + const DESCRIPTION = 'Generates feeds for Gist comments'; + const MAINTAINER = 'logmanoriginal'; + const CACHE_TIMEOUT = 3600; + + const PARAMETERS = [[ + 'id' => [ + 'name' => 'Gist', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert Gist ID or URI', + 'exampleValue' => '2646763' + ] + ]]; + + private $filename; + + public function getURI() + { + $id = $this->getInput('id') ?: ''; + + $urlpath = parse_url($id, PHP_URL_PATH); + + if ($urlpath) { + $components = explode('/', $urlpath); + $id = end($components); + } + + return static::URI . '/' . $id; + } + + public function getName() + { + return $this->filename ? $this->filename . ' - ' . static::NAME : static::NAME; + } + + public function collectData() + { + $html = getSimpleHTMLDOM( + $this->getURI(), + null, + null, + true, + true, + DEFAULT_TARGET_CHARSET, + false, // Do NOT remove line breaks + DEFAULT_BR_TEXT, + DEFAULT_SPAN_TEXT + ); + + $html = defaultLinkTo($html, $this->getURI()); + + $fileinfo = $html->find('[class~="file-info"]', 0) + or returnServerError('Could not find file info!'); + + $this->filename = $fileinfo->plaintext; + + $comments = $html->find('div[class~="TimelineItem"]'); + + if (is_null($comments)) { // no comments yet + return; + } + + foreach ($comments as $comment) { + $uri = $comment->find('a[href*=#gistcomment]', 0) + or returnServerError('Could not find comment anchor!'); + + $title = $comment->find('h3', 0); + + $datetime = $comment->find('[datetime]', 0) + or returnServerError('Could not find comment datetime!'); + + $author = $comment->find('a.author', 0) + or returnServerError('Could not find author name!'); + + $message = $comment->find('[class~="comment-body"]', 0) + or returnServerError('Could not find comment body!'); + + $item = []; + + $item['uri'] = $uri->href; + $item['title'] = str_replace('commented', 'commented on', $title->plaintext ?? ''); + $item['timestamp'] = strtotime($datetime->datetime); + $item['author'] = '<a href="' . $author->href . '">' . $author->plaintext . '</a>'; + $item['content'] = $this->fixContent($message); + // $item['enclosures'] = array(); + // $item['categories'] = array(); + + $this->items[] = $item; + } + } + + /** Removes all unnecessary tags and adds formatting */ + private function fixContent($content) + { + // Restore code (inside <pre />) highlighting + foreach ($content->find('pre') as $pre) { + $pre->style = <<<EOD padding: 16px; overflow: auto; font-size: 85%; @@ -116,46 +112,40 @@ box-sizing: border-box; margin-bottom: 16px; EOD; - $code = $pre->find('code', 0); + $code = $pre->find('code', 0); - if($code) { - - $code->style = <<<EOD + if ($code) { + $code->style = <<<EOD white-space: pre; word-break: normal; EOD; + } + } - } - - } + // find <code /> not inside <pre /> (`inline-code`) + foreach ($content->find('code') as $code) { + if ($code->parent()->tag === 'pre') { + continue; + } - // find <code /> not inside <pre /> (`inline-code`) - foreach($content->find('code') as $code) { - - if($code->parent()->tag === 'pre') { - continue; - } - - $code->style = <<<EOD + $code->style = <<<EOD background-color: rgba(27,31,35,0.05); padding: 0.2em 0.4em; border-radius: 3px; EOD; + } - } - - // restore text spacing - foreach($content->find('p') as $p) { - $p->style = 'margin-bottom: 16px;'; - } - - // Remove unnecessary tags - $content = strip_tags( - $content->innertext, - '<p><a><img><ol><ul><li><table><tr><th><td><string><pre><code><br><hr><h>' - ); + // restore text spacing + foreach ($content->find('p') as $p) { + $p->style = 'margin-bottom: 16px;'; + } - return $content; + // Remove unnecessary tags + $content = strip_tags( + $content->innertext, + '<p><a><img><ol><ul><li><table><tr><th><td><string><pre><code><br><hr><h>' + ); - } + return $content; + } } diff --git a/bridges/GiteaBridge.php b/bridges/GiteaBridge.php index f7b7b782..f7f426e9 100644 --- a/bridges/GiteaBridge.php +++ b/bridges/GiteaBridge.php @@ -1,305 +1,322 @@ <?php + /** * Gitea is a community managed lightweight code hosting solution. * https://docs.gitea.io/en-us/ */ -class GiteaBridge extends BridgeAbstract { - - const NAME = 'Gitea'; - const URI = 'https://gitea.io'; - const DESCRIPTION = 'Returns the latest issues, commits or releases'; - const MAINTAINER = 'gileri'; - const CACHE_TIMEOUT = 300; // 5 minutes - - const PARAMETERS = array( - 'global' => array( - 'host' => array( - 'name' => 'Host', - 'exampleValue' => 'https://gitea.com', - 'required' => true, - 'title' => 'Host name with its protocol, without trailing slash', - ), - 'user' => array( - 'name' => 'Username', - 'exampleValue' => 'gitea', - 'required' => true, - 'title' => 'User name as it appears in the URL', - ), - 'project' => array( - 'name' => 'Project name', - 'exampleValue' => 'helm-chart', - 'required' => true, - 'title' => 'Project name as it appears in the URL', - ), - ), - 'Commits' => array( - 'branch' => array( - 'name' => 'Branch name', - 'defaultValue' => 'master', - 'required' => true, - 'title' => 'Branch name as it appears in the URL', - ), - ), - 'Issues' => array( - 'include_description' => array( - 'name' => 'Include issue description', - 'type' => 'checkbox', - 'title' => 'Activate to include the issue description', - ), - ), - 'Single issue' => array( - 'issue' => array( - 'name' => 'Issue number', - 'type' => 'number', - 'exampleValue' => 100, - 'required' => true, - 'title' => 'Issue number from the issues list', - ), - ), - 'Single pull request' => array( - 'pull_request' => array( - 'name' => 'Pull request number', - 'type' => 'number', - 'exampleValue' => 100, - 'required' => true, - 'title' => 'Pull request number from the issues list', - ), - ), - 'Pull requests' => array( - 'include_description' => array( - 'name' => 'Include pull request description', - 'type' => 'checkbox', - 'title' => 'Activate to include the pull request description', - ), - ), - 'Releases' => array(), - 'Tags' => array(), - ); - - private $title = ''; - - public function getIcon() { - return 'https://gitea.io/images/gitea.png'; - } - - public function getName() { - switch($this->queriedContext) { - case 'Commits': - case 'Issues': - case 'Pull requests': - case 'Releases': - case 'Tags': return $this->title . ' ' . $this->queriedContext; - case 'Single issue': return 'Issue ' . $this->getInput('issue') . ': ' . $this->title; - case 'Single pull request': return 'Pull request ' . $this->getInput('pull_request') . ': ' . $this->title; - default: return parent::getName(); - } - } - - public function getURI() { - switch($this->queriedContext) { - case 'Commits': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/commits/' . $this->getInput('branch'); - - case 'Issues': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/issues/'; - - case 'Single issue': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/issues/' . $this->getInput('issue'); - - case 'Releases': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/releases/'; - - case 'Tags': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/tags/'; - - case 'Pull requests': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/pulls/'; - - case 'Single pull request': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/pulls/' . $this->getInput('pull_request'); - - default: return parent::getURI(); - } - } - - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()) - or returnServerError('Could not request ' . $this->getURI()); - $html = defaultLinkTo($html, $this->getURI()); - - $this->title = $html->find('[property="og:title"]', 0)->content; - - switch($this->queriedContext) { - case 'Commits': - $this->collectCommitsData($html); - break; - case 'Issues': - $this->collectIssuesData($html); - break; - case 'Pull requests': - $this->collectPullRequestsData($html); - break; - case 'Single issue': - $this->collectSingleIssueOrPrData($html); - break; - case 'Single pull request': - $this->collectSingleIssueOrPrData($html); - break; - case 'Releases': - $this->collectReleasesData($html); - break; - case 'Tags': - $this->collectTagsData($html); - break; - } - } - - protected function collectReleasesData($html) { - $releases = $html->find('#release-list > li') - or returnServerError('Unable to find releases'); - - foreach($releases as $release) { - $this->items[] = array( - 'author' => $release->find('.author', 0)->plaintext, - 'uri' => $release->find('a', 0)->href, - 'title' => 'Release ' . $release->find('h4', 0)->plaintext, - 'timestamp' => $release->find('.time-since', 0)->title, - ); - } - } - - protected function collectTagsData($html) { - $tags = $html->find('table#tags-table > tbody > tr') - or returnServerError('Unable to find tags'); - - foreach($tags as $tag) { - $this->items[] = array( - 'uri' => $tag->find('a', 0)->href, - 'title' => 'Tag ' . $tag->find('.release-tag-name > a', 0)->plaintext, - ); - } - } - - protected function collectCommitsData($html) { - $commits = $html->find('#commits-table tbody tr') - or returnServerError('Unable to find commits'); - - foreach($commits as $commit) { - $this->items[] = array( - 'uri' => $commit->find('a.sha', 0)->href, - 'title' => $commit->find('.message span', 0)->plaintext, - 'author' => $commit->find('.author', 0)->plaintext, - 'timestamp' => $commit->find('.time-since', 0)->title, - 'uid' => $commit->find('.sha', 0)->plaintext, - ); - } - } - - protected function collectIssuesData($html) { - $issues = $html->find('.issue.list li') - or returnServerError('Unable to find issues'); - - foreach($issues as $issue) { - $uri = $issue->find('a', 0)->href; - - $item = array( - 'uri' => $uri, - 'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext, - 'author' => $issue->find('.desc a', 1)->plaintext, - 'timestamp' => $issue->find('.time-since', 0)->title, - ); - - if($this->getInput('include_description')) { - $issue_html = getSimpleHTMLDOMCached($uri, 3600) - or returnServerError('Unable to load issue description'); - - $issue_html = defaultLinkTo($issue_html, $uri); - - $item['content'] = $issue_html->find('.comment .markup', 0); - } - - $this->items[] = $item; - } - } - - protected function collectSingleIssueOrPrData($html) { - $comments = $html->find('.comment') - or returnServerError('Unable to find comments'); - - foreach($comments as $comment) { - if (strpos($comment->getAttribute('class'), 'form') !== false - || strpos($comment->getAttribute('class'), 'merge') !== false - ) { - // Ignore comment form and merge information - continue; - } - $commentLink = $comment->find('a[href*="#issue"]', 0); - $item = array( - 'author' => $comment->find('a.author', 0)->plaintext, - 'content' => $comment->find('.render-content', 0), - ); - if ($commentLink !== null) { - // Regular comment - $item['uri'] = $commentLink->href; - $item['title'] = str_replace($commentLink->plaintext, '', $comment->find('span', 0)->plaintext); - $item['timestamp'] = $comment->find('.time-since', 0)->title; - } else { - // Change request comment - $item['uri'] = $this->getURI() . '#' . $comment->getAttribute('id'); - $item['title'] = $comment->find('.comment-header .text', 0)->plaintext; - } - $this->items[] = $item; - } - - $this->items = array_reverse($this->items); - } - - protected function collectPullRequestsData($html) { - $issues = $html->find('.issue.list li') - or returnServerError('Unable to find pull requests'); - - foreach($issues as $issue) { - $uri = $issue->find('a', 0)->href; - - $item = array( - 'uri' => $uri, - 'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext, - 'author' => $issue->find('.desc a', 1)->plaintext, - 'timestamp' => $issue->find('.time-since', 0)->title, - ); - - if($this->getInput('include_description')) { - $issue_html = getSimpleHTMLDOMCached($uri, 3600) - or returnServerError('Unable to load issue description'); - - $issue_html = defaultLinkTo($issue_html, $uri); - - $item['content'] = $issue_html->find('.comment .markup', 0); - } - - $this->items[] = $item; - } - } +class GiteaBridge extends BridgeAbstract +{ + const NAME = 'Gitea'; + const URI = 'https://gitea.io'; + const DESCRIPTION = 'Returns the latest issues, commits or releases'; + const MAINTAINER = 'gileri'; + const CACHE_TIMEOUT = 300; // 5 minutes + + const PARAMETERS = [ + 'global' => [ + 'host' => [ + 'name' => 'Host', + 'exampleValue' => 'https://gitea.com', + 'required' => true, + 'title' => 'Host name with its protocol, without trailing slash', + ], + 'user' => [ + 'name' => 'Username', + 'exampleValue' => 'gitea', + 'required' => true, + 'title' => 'User name as it appears in the URL', + ], + 'project' => [ + 'name' => 'Project name', + 'exampleValue' => 'helm-chart', + 'required' => true, + 'title' => 'Project name as it appears in the URL', + ], + ], + 'Commits' => [ + 'branch' => [ + 'name' => 'Branch name', + 'defaultValue' => 'master', + 'required' => true, + 'title' => 'Branch name as it appears in the URL', + ], + ], + 'Issues' => [ + 'include_description' => [ + 'name' => 'Include issue description', + 'type' => 'checkbox', + 'title' => 'Activate to include the issue description', + ], + ], + 'Single issue' => [ + 'issue' => [ + 'name' => 'Issue number', + 'type' => 'number', + 'exampleValue' => 100, + 'required' => true, + 'title' => 'Issue number from the issues list', + ], + ], + 'Single pull request' => [ + 'pull_request' => [ + 'name' => 'Pull request number', + 'type' => 'number', + 'exampleValue' => 100, + 'required' => true, + 'title' => 'Pull request number from the issues list', + ], + ], + 'Pull requests' => [ + 'include_description' => [ + 'name' => 'Include pull request description', + 'type' => 'checkbox', + 'title' => 'Activate to include the pull request description', + ], + ], + 'Releases' => [], + 'Tags' => [], + ]; + + private $title = ''; + + public function getIcon() + { + return 'https://gitea.io/images/gitea.png'; + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Commits': + case 'Issues': + case 'Pull requests': + case 'Releases': + case 'Tags': + return $this->title . ' ' . $this->queriedContext; + case 'Single issue': + return 'Issue ' . $this->getInput('issue') . ': ' . $this->title; + case 'Single pull request': + return 'Pull request ' . $this->getInput('pull_request') . ': ' . $this->title; + default: + return parent::getName(); + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Commits': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/commits/' . $this->getInput('branch'); + + case 'Issues': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/issues/'; + + case 'Single issue': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/issues/' . $this->getInput('issue'); + + case 'Releases': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/releases/'; + + case 'Tags': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/tags/'; + + case 'Pull requests': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/pulls/'; + + case 'Single pull request': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/pulls/' . $this->getInput('pull_request'); + + default: + return parent::getURI(); + } + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()) + or returnServerError('Could not request ' . $this->getURI()); + $html = defaultLinkTo($html, $this->getURI()); + + $this->title = $html->find('[property="og:title"]', 0)->content; + + switch ($this->queriedContext) { + case 'Commits': + $this->collectCommitsData($html); + break; + case 'Issues': + $this->collectIssuesData($html); + break; + case 'Pull requests': + $this->collectPullRequestsData($html); + break; + case 'Single issue': + $this->collectSingleIssueOrPrData($html); + break; + case 'Single pull request': + $this->collectSingleIssueOrPrData($html); + break; + case 'Releases': + $this->collectReleasesData($html); + break; + case 'Tags': + $this->collectTagsData($html); + break; + } + } + + protected function collectReleasesData($html) + { + $releases = $html->find('#release-list > li') + or returnServerError('Unable to find releases'); + + foreach ($releases as $release) { + $this->items[] = [ + 'author' => $release->find('.author', 0)->plaintext, + 'uri' => $release->find('a', 0)->href, + 'title' => 'Release ' . $release->find('h4', 0)->plaintext, + 'timestamp' => $release->find('.time-since', 0)->title, + ]; + } + } + + protected function collectTagsData($html) + { + $tags = $html->find('table#tags-table > tbody > tr') + or returnServerError('Unable to find tags'); + + foreach ($tags as $tag) { + $this->items[] = [ + 'uri' => $tag->find('a', 0)->href, + 'title' => 'Tag ' . $tag->find('.release-tag-name > a', 0)->plaintext, + ]; + } + } + + protected function collectCommitsData($html) + { + $commits = $html->find('#commits-table tbody tr') + or returnServerError('Unable to find commits'); + + foreach ($commits as $commit) { + $this->items[] = [ + 'uri' => $commit->find('a.sha', 0)->href, + 'title' => $commit->find('.message span', 0)->plaintext, + 'author' => $commit->find('.author', 0)->plaintext, + 'timestamp' => $commit->find('.time-since', 0)->title, + 'uid' => $commit->find('.sha', 0)->plaintext, + ]; + } + } + + protected function collectIssuesData($html) + { + $issues = $html->find('.issue.list li') + or returnServerError('Unable to find issues'); + + foreach ($issues as $issue) { + $uri = $issue->find('a', 0)->href; + + $item = [ + 'uri' => $uri, + 'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext, + 'author' => $issue->find('.desc a', 1)->plaintext, + 'timestamp' => $issue->find('.time-since', 0)->title, + ]; + + if ($this->getInput('include_description')) { + $issue_html = getSimpleHTMLDOMCached($uri, 3600) + or returnServerError('Unable to load issue description'); + + $issue_html = defaultLinkTo($issue_html, $uri); + + $item['content'] = $issue_html->find('.comment .markup', 0); + } + + $this->items[] = $item; + } + } + + protected function collectSingleIssueOrPrData($html) + { + $comments = $html->find('.comment') + or returnServerError('Unable to find comments'); + + foreach ($comments as $comment) { + if ( + strpos($comment->getAttribute('class'), 'form') !== false + || strpos($comment->getAttribute('class'), 'merge') !== false + ) { + // Ignore comment form and merge information + continue; + } + $commentLink = $comment->find('a[href*="#issue"]', 0); + $item = [ + 'author' => $comment->find('a.author', 0)->plaintext, + 'content' => $comment->find('.render-content', 0), + ]; + if ($commentLink !== null) { + // Regular comment + $item['uri'] = $commentLink->href; + $item['title'] = str_replace($commentLink->plaintext, '', $comment->find('span', 0)->plaintext); + $item['timestamp'] = $comment->find('.time-since', 0)->title; + } else { + // Change request comment + $item['uri'] = $this->getURI() . '#' . $comment->getAttribute('id'); + $item['title'] = $comment->find('.comment-header .text', 0)->plaintext; + } + $this->items[] = $item; + } + + $this->items = array_reverse($this->items); + } + + protected function collectPullRequestsData($html) + { + $issues = $html->find('.issue.list li') + or returnServerError('Unable to find pull requests'); + + foreach ($issues as $issue) { + $uri = $issue->find('a', 0)->href; + + $item = [ + 'uri' => $uri, + 'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext, + 'author' => $issue->find('.desc a', 1)->plaintext, + 'timestamp' => $issue->find('.time-since', 0)->title, + ]; + + if ($this->getInput('include_description')) { + $issue_html = getSimpleHTMLDOMCached($uri, 3600) + or returnServerError('Unable to load issue description'); + + $issue_html = defaultLinkTo($issue_html, $uri); + + $item['content'] = $issue_html->find('.comment .markup', 0); + } + + $this->items[] = $item; + } + } } diff --git a/bridges/GithubIssueBridge.php b/bridges/GithubIssueBridge.php index e3a0c73a..b90982c6 100644 --- a/bridges/GithubIssueBridge.php +++ b/bridges/GithubIssueBridge.php @@ -1,292 +1,301 @@ <?php -class GithubIssueBridge extends BridgeAbstract { - - const MAINTAINER = 'Pierre Mazière'; - const NAME = 'Github Issue'; - const URI = 'https://github.com/'; - const CACHE_TIMEOUT = 0; // 10min - const DESCRIPTION = 'Returns the issues or comments of an issue of a github project'; - - const PARAMETERS = array( - 'global' => array( - 'u' => array( - 'name' => 'User name', - 'exampleValue' => 'RSS-Bridge', - 'required' => true - ), - 'p' => array( - 'name' => 'Project name', - 'exampleValue' => 'rss-bridge', - 'required' => true - ) - ), - 'Project Issues' => array( - 'c' => array( - 'name' => 'Show Issues Comments', - 'type' => 'checkbox' - ), - 'q' => array( - 'name' => 'Search Query', - 'defaultValue' => 'is:issue is:open sort:updated-desc', - 'required' => true - ) - ), - 'Issue comments' => array( - 'i' => array( - 'name' => 'Issue number', - 'type' => 'number', - 'exampleValue' => '2099', - 'required' => true - ) - ) - ); - - // Allows generalization with GithubPullRequestBridge - const BRIDGE_OPTIONS = array(0 => 'Project Issues', 1 => 'Issue comments'); - const URL_PATH = 'issues'; - const SEARCH_QUERY_PATH = 'issues'; - - public function getName(){ - $name = $this->getInput('u') . '/' . $this->getInput('p'); - switch($this->queriedContext) { - case static::BRIDGE_OPTIONS[0]: // Project Issues - $prefix = static::NAME . 's for '; - if($this->getInput('c')) { - $prefix = static::NAME . 's comments for '; - } - $name = $prefix . $name; - break; - case static::BRIDGE_OPTIONS[1]: // Issue comments - $name = static::NAME . ' ' . $name . ' #' . $this->getInput('i'); - break; - default: return parent::getName(); - } - return $name; - } - - public function getURI() { - if(null !== $this->getInput('u') && null !== $this->getInput('p')) { - $uri = static::URI . $this->getInput('u') . '/' - . $this->getInput('p') . '/'; - if($this->queriedContext === static::BRIDGE_OPTIONS[1]) { - $uri .= static::URL_PATH . '/' . $this->getInput('i'); - } else { - $uri .= static::SEARCH_QUERY_PATH . '?q=' . urlencode($this->getInput('q')); - } - return $uri; - } - - return parent::getURI(); - } - - private function buildGitHubIssueCommentUri($issue_number, $comment_id) { - // https://github.com/<user>/<project>/issues/<issue-number>#<id> - return static::URI - . $this->getInput('u') - . '/' - . $this->getInput('p') - . '/' . static::URL_PATH . '/' - . $issue_number - . '#' - . $comment_id; - } - - private function extractIssueEvent($issueNbr, $title, $comment) { - - $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id); - - $author = $comment->find('.author, .avatar', 0); - if ($author) { - $author = trim($author->href, '/'); - } else { - $author = ''; - } - - $title .= ' / ' - . trim(str_replace( - array('octicon','-'), array(''), - $comment->find('.octicon', 0)->getAttribute('class') - )); - - $time = $comment->find('relative-time', 0); - if ($time === null) { - return; - } - - foreach($comment->find('.Details-content--hidden, .btn') as $el) { - $el->innertext = ''; - } - $content = $comment->plaintext; - - $item = array(); - $item['author'] = $author; - $item['uri'] = $uri; - $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8'); - $item['timestamp'] = strtotime($time->getAttribute('datetime')); - $item['content'] = $content; - return $item; - } - - private function extractIssueComment($issueNbr, $title, $comment) { - $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id); - - $author = $comment->find('.author', 0)->plaintext; - - $header = $comment->find('.timeline-comment-header > h3', 0); - $title .= ' / ' . ($header ? $header->plaintext : 'Activity'); - - $time = $comment->find('relative-time', 0); - if ($time === null) { - return; - } - - $content = $comment->find('.comment-body', 0)->innertext; - - $item = array(); - $item['author'] = $author; - $item['uri'] = $uri; - $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8'); - $item['timestamp'] = strtotime($time->getAttribute('datetime')); - $item['content'] = $content; - return $item; - } - - private function extractIssueComments($issue) { - $items = array(); - $title = $issue->find('.gh-header-title', 0)->plaintext; - $issueNbr = trim( - substr($issue->find('.gh-header-number', 0)->plaintext, 1) - ); - - $comments = $issue->find( - '.comment, .TimelineItem-badge' - ); - - foreach($comments as $comment) { - if ($comment->hasClass('comment')) { - $comment = $comment->parent; - $item = $this->extractIssueComment($issueNbr, $title, $comment); - if ($item !== null) { - $items[] = $item; - } - continue; - } else { - $comment = $comment->parent; - $item = $this->extractIssueEvent($issueNbr, $title, $comment); - if ($item !== null) { - $items[] = $item; - } - } - - } - return $items; - } - - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); - - switch($this->queriedContext) { - case static::BRIDGE_OPTIONS[1]: // Issue comments - $this->items = $this->extractIssueComments($html); - break; - case static::BRIDGE_OPTIONS[0]: // Project Issues - foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) { - $info = $issue->find('.opened-by', 0); - - preg_match('/\/([0-9]+)$/', $issue->find('a', 0)->href, $match); - $issueNbr = $match[1]; - - $item = array(); - $item['content'] = ''; - - if($this->getInput('c')) { - $uri = static::URI . $this->getInput('u') - . '/' . $this->getInput('p') . '/' . static::URL_PATH . '/' . $issueNbr; - $issue = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT); - if($issue) { - $this->items = array_merge( - $this->items, - $this->extractIssueComments($issue) - ); - continue; - } - $item['content'] = 'Can not extract comments from ' . $uri; - } - - $item['author'] = $info->find('a', 0)->plaintext; - $item['timestamp'] = strtotime( - $info->find('relative-time', 0)->getAttribute('datetime') - ); - $item['title'] = html_entity_decode( - $issue->find('.js-navigation-open', 0)->plaintext, - ENT_QUOTES, - 'UTF-8' - ); - - $comment_count = 0; - if($span = $issue->find('a[aria-label*="comment"] span', 0)) { - $comment_count = $span->plaintext; - } - - $item['content'] .= "\n" . 'Comments: ' . $comment_count; - $item['uri'] = self::URI - . trim($issue->find('.js-navigation-open', 0)->getAttribute('href'), '/'); - $this->items[] = $item; - } - break; - } - - array_walk($this->items, function(&$item){ - $item['content'] = preg_replace('/\s+/', ' ', $item['content']); - $item['content'] = str_replace( - 'href="/', - 'href="' . static::URI, - $item['content'] - ); - $item['content'] = str_replace( - 'href="#', - 'href="' . substr($item['uri'], 0, strpos($item['uri'], '#') + 1), - $item['content'] - ); - $item['title'] = preg_replace('/\s+/', ' ', $item['title']); - }); - } - - public function detectParameters($url) { - - if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false - || strpos($url, self::URI) !== 0) { - return null; - } - - $url_components = parse_url($url); - $path_segments = array_values(array_filter(explode('/', $url_components['path']))); - - switch(count($path_segments)) { - case 2: // Project issues - list($user, $project) = $path_segments; - $show_comments = 'off'; - break; - case 3: // Project issues with issue comments - if($path_segments[2] !== static::URL_PATH) { - return null; - } - list($user, $project) = $path_segments; - $show_comments = 'on'; - break; - case 4: // Issue comments - list($user, $project, /* issues */, $issue) = $path_segments; - break; - default: - return null; - } - - return array( - 'u' => $user, - 'p' => $project, - 'c' => isset($show_comments) ? $show_comments : null, - 'i' => isset($issue) ? $issue : null, - ); - - } + +class GithubIssueBridge extends BridgeAbstract +{ + const MAINTAINER = 'Pierre Mazière'; + const NAME = 'Github Issue'; + const URI = 'https://github.com/'; + const CACHE_TIMEOUT = 0; // 10min + const DESCRIPTION = 'Returns the issues or comments of an issue of a github project'; + + const PARAMETERS = [ + 'global' => [ + 'u' => [ + 'name' => 'User name', + 'exampleValue' => 'RSS-Bridge', + 'required' => true + ], + 'p' => [ + 'name' => 'Project name', + 'exampleValue' => 'rss-bridge', + 'required' => true + ] + ], + 'Project Issues' => [ + 'c' => [ + 'name' => 'Show Issues Comments', + 'type' => 'checkbox' + ], + 'q' => [ + 'name' => 'Search Query', + 'defaultValue' => 'is:issue is:open sort:updated-desc', + 'required' => true + ] + ], + 'Issue comments' => [ + 'i' => [ + 'name' => 'Issue number', + 'type' => 'number', + 'exampleValue' => '2099', + 'required' => true + ] + ] + ]; + + // Allows generalization with GithubPullRequestBridge + const BRIDGE_OPTIONS = [0 => 'Project Issues', 1 => 'Issue comments']; + const URL_PATH = 'issues'; + const SEARCH_QUERY_PATH = 'issues'; + + public function getName() + { + $name = $this->getInput('u') . '/' . $this->getInput('p'); + switch ($this->queriedContext) { + case static::BRIDGE_OPTIONS[0]: // Project Issues + $prefix = static::NAME . 's for '; + if ($this->getInput('c')) { + $prefix = static::NAME . 's comments for '; + } + $name = $prefix . $name; + break; + case static::BRIDGE_OPTIONS[1]: // Issue comments + $name = static::NAME . ' ' . $name . ' #' . $this->getInput('i'); + break; + default: + return parent::getName(); + } + return $name; + } + + public function getURI() + { + if (null !== $this->getInput('u') && null !== $this->getInput('p')) { + $uri = static::URI . $this->getInput('u') . '/' + . $this->getInput('p') . '/'; + if ($this->queriedContext === static::BRIDGE_OPTIONS[1]) { + $uri .= static::URL_PATH . '/' . $this->getInput('i'); + } else { + $uri .= static::SEARCH_QUERY_PATH . '?q=' . urlencode($this->getInput('q')); + } + return $uri; + } + + return parent::getURI(); + } + + private function buildGitHubIssueCommentUri($issue_number, $comment_id) + { + // https://github.com/<user>/<project>/issues/<issue-number>#<id> + return static::URI + . $this->getInput('u') + . '/' + . $this->getInput('p') + . '/' . static::URL_PATH . '/' + . $issue_number + . '#' + . $comment_id; + } + + private function extractIssueEvent($issueNbr, $title, $comment) + { + $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id); + + $author = $comment->find('.author, .avatar', 0); + if ($author) { + $author = trim($author->href, '/'); + } else { + $author = ''; + } + + $title .= ' / ' + . trim(str_replace( + ['octicon','-'], + [''], + $comment->find('.octicon', 0)->getAttribute('class') + )); + + $time = $comment->find('relative-time', 0); + if ($time === null) { + return; + } + + foreach ($comment->find('.Details-content--hidden, .btn') as $el) { + $el->innertext = ''; + } + $content = $comment->plaintext; + + $item = []; + $item['author'] = $author; + $item['uri'] = $uri; + $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8'); + $item['timestamp'] = strtotime($time->getAttribute('datetime')); + $item['content'] = $content; + return $item; + } + + private function extractIssueComment($issueNbr, $title, $comment) + { + $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id); + + $author = $comment->find('.author', 0)->plaintext; + + $header = $comment->find('.timeline-comment-header > h3', 0); + $title .= ' / ' . ($header ? $header->plaintext : 'Activity'); + + $time = $comment->find('relative-time', 0); + if ($time === null) { + return; + } + + $content = $comment->find('.comment-body', 0)->innertext; + + $item = []; + $item['author'] = $author; + $item['uri'] = $uri; + $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8'); + $item['timestamp'] = strtotime($time->getAttribute('datetime')); + $item['content'] = $content; + return $item; + } + + private function extractIssueComments($issue) + { + $items = []; + $title = $issue->find('.gh-header-title', 0)->plaintext; + $issueNbr = trim( + substr($issue->find('.gh-header-number', 0)->plaintext, 1) + ); + + $comments = $issue->find( + '.comment, .TimelineItem-badge' + ); + + foreach ($comments as $comment) { + if ($comment->hasClass('comment')) { + $comment = $comment->parent; + $item = $this->extractIssueComment($issueNbr, $title, $comment); + if ($item !== null) { + $items[] = $item; + } + continue; + } else { + $comment = $comment->parent; + $item = $this->extractIssueEvent($issueNbr, $title, $comment); + if ($item !== null) { + $items[] = $item; + } + } + } + return $items; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + switch ($this->queriedContext) { + case static::BRIDGE_OPTIONS[1]: // Issue comments + $this->items = $this->extractIssueComments($html); + break; + case static::BRIDGE_OPTIONS[0]: // Project Issues + foreach ($html->find('.js-active-navigation-container .js-navigation-item') as $issue) { + $info = $issue->find('.opened-by', 0); + + preg_match('/\/([0-9]+)$/', $issue->find('a', 0)->href, $match); + $issueNbr = $match[1]; + + $item = []; + $item['content'] = ''; + + if ($this->getInput('c')) { + $uri = static::URI . $this->getInput('u') + . '/' . $this->getInput('p') . '/' . static::URL_PATH . '/' . $issueNbr; + $issue = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT); + if ($issue) { + $this->items = array_merge( + $this->items, + $this->extractIssueComments($issue) + ); + continue; + } + $item['content'] = 'Can not extract comments from ' . $uri; + } + + $item['author'] = $info->find('a', 0)->plaintext; + $item['timestamp'] = strtotime( + $info->find('relative-time', 0)->getAttribute('datetime') + ); + $item['title'] = html_entity_decode( + $issue->find('.js-navigation-open', 0)->plaintext, + ENT_QUOTES, + 'UTF-8' + ); + + $comment_count = 0; + if ($span = $issue->find('a[aria-label*="comment"] span', 0)) { + $comment_count = $span->plaintext; + } + + $item['content'] .= "\n" . 'Comments: ' . $comment_count; + $item['uri'] = self::URI + . trim($issue->find('.js-navigation-open', 0)->getAttribute('href'), '/'); + $this->items[] = $item; + } + break; + } + + array_walk($this->items, function (&$item) { + $item['content'] = preg_replace('/\s+/', ' ', $item['content']); + $item['content'] = str_replace( + 'href="/', + 'href="' . static::URI, + $item['content'] + ); + $item['content'] = str_replace( + 'href="#', + 'href="' . substr($item['uri'], 0, strpos($item['uri'], '#') + 1), + $item['content'] + ); + $item['title'] = preg_replace('/\s+/', ' ', $item['title']); + }); + } + + public function detectParameters($url) + { + if ( + filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false + || strpos($url, self::URI) !== 0 + ) { + return null; + } + + $url_components = parse_url($url); + $path_segments = array_values(array_filter(explode('/', $url_components['path']))); + + switch (count($path_segments)) { + case 2: // Project issues + list($user, $project) = $path_segments; + $show_comments = 'off'; + break; + case 3: // Project issues with issue comments + if ($path_segments[2] !== static::URL_PATH) { + return null; + } + list($user, $project) = $path_segments; + $show_comments = 'on'; + break; + case 4: // Issue comments + list($user, $project, /* issues */, $issue) = $path_segments; + break; + default: + return null; + } + + return [ + 'u' => $user, + 'p' => $project, + 'c' => isset($show_comments) ? $show_comments : null, + 'i' => isset($issue) ? $issue : null, + ]; + } } diff --git a/bridges/GithubPullRequestBridge.php b/bridges/GithubPullRequestBridge.php index 82f901d1..b508a919 100644 --- a/bridges/GithubPullRequestBridge.php +++ b/bridges/GithubPullRequestBridge.php @@ -1,44 +1,45 @@ <?php -class GitHubPullRequestBridge extends GithubIssueBridge { - const NAME = 'GitHub Pull Request'; - const DESCRIPTION = 'Returns the pull request or comments of a pull request of a GitHub project'; +class GitHubPullRequestBridge extends GithubIssueBridge +{ + const NAME = 'GitHub Pull Request'; + const DESCRIPTION = 'Returns the pull request or comments of a pull request of a GitHub project'; - const PARAMETERS = array( - 'global' => array( - 'u' => array( - 'name' => 'User name', - 'exampleValue' => 'RSS-Bridge', - 'required' => true - ), - 'p' => array( - 'name' => 'Project name', - 'exampleValue' => 'rss-bridge', - 'required' => true - ) - ), - 'Project Pull Requests' => array( - 'c' => array( - 'name' => 'Show Pull Request Comments', - 'type' => 'checkbox' - ), - 'q' => array( - 'name' => 'Search Query', - 'defaultValue' => 'is:pr is:open sort:created-desc', - 'required' => true - ) - ), - 'Pull Request comments' => array( - 'i' => array( - 'name' => 'Pull Request number', - 'type' => 'number', - 'exampleValue' => '2100', - 'required' => true - ) - ) - ); + const PARAMETERS = [ + 'global' => [ + 'u' => [ + 'name' => 'User name', + 'exampleValue' => 'RSS-Bridge', + 'required' => true + ], + 'p' => [ + 'name' => 'Project name', + 'exampleValue' => 'rss-bridge', + 'required' => true + ] + ], + 'Project Pull Requests' => [ + 'c' => [ + 'name' => 'Show Pull Request Comments', + 'type' => 'checkbox' + ], + 'q' => [ + 'name' => 'Search Query', + 'defaultValue' => 'is:pr is:open sort:created-desc', + 'required' => true + ] + ], + 'Pull Request comments' => [ + 'i' => [ + 'name' => 'Pull Request number', + 'type' => 'number', + 'exampleValue' => '2100', + 'required' => true + ] + ] + ]; - const BRIDGE_OPTIONS = array(0 => 'Project Pull Requests', 1 => 'Pull Request comments'); - const URL_PATH = 'pull'; - const SEARCH_QUERY_PATH = 'pulls'; + const BRIDGE_OPTIONS = [0 => 'Project Pull Requests', 1 => 'Pull Request comments']; + const URL_PATH = 'pull'; + const SEARCH_QUERY_PATH = 'pulls'; } diff --git a/bridges/GithubSearchBridge.php b/bridges/GithubSearchBridge.php index fdabfc94..658c4d7c 100644 --- a/bridges/GithubSearchBridge.php +++ b/bridges/GithubSearchBridge.php @@ -1,67 +1,69 @@ <?php -class GithubSearchBridge extends BridgeAbstract { - const MAINTAINER = 'corenting'; - const NAME = 'Github Repositories Search'; - const URI = 'https://github.com/'; - const CACHE_TIMEOUT = 600; // 10min - const DESCRIPTION = 'Returns a specified repositories search (sorted by recently updated)'; - const PARAMETERS = array( array( - 's' => array( - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'rss-bridge', - 'name' => 'Search query' - ) - )); +class GithubSearchBridge extends BridgeAbstract +{ + const MAINTAINER = 'corenting'; + const NAME = 'Github Repositories Search'; + const URI = 'https://github.com/'; + const CACHE_TIMEOUT = 600; // 10min + const DESCRIPTION = 'Returns a specified repositories search (sorted by recently updated)'; + const PARAMETERS = [ [ + 's' => [ + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'rss-bridge', + 'name' => 'Search query' + ] + ]]; - public function collectData(){ - $params = array('utf8' => '✓', - 'q' => urlencode($this->getInput('s')), - 's' => 'updated', - 'o' => 'desc', - 'type' => 'Repositories'); - $url = self::URI . 'search?' . http_build_query($params); + public function collectData() + { + $params = ['utf8' => '✓', + 'q' => urlencode($this->getInput('s')), + 's' => 'updated', + 'o' => 'desc', + 'type' => 'Repositories']; + $url = self::URI . 'search?' . http_build_query($params); - $html = getSimpleHTMLDOM($url); + $html = getSimpleHTMLDOM($url); - foreach($html->find('li.repo-list-item') as $element) { - $item = array(); + foreach ($html->find('li.repo-list-item') as $element) { + $item = []; - $uri = $element->find('.f4 a', 0)->href; - $uri = substr(self::URI, 0, -1) . $uri; - $item['uri'] = $uri; + $uri = $element->find('.f4 a', 0)->href; + $uri = substr(self::URI, 0, -1) . $uri; + $item['uri'] = $uri; - $title = $element->find('.f4', 0)->plaintext; - $item['title'] = $title; + $title = $element->find('.f4', 0)->plaintext; + $item['title'] = $title; - // Description - if (count($element->find('p.mb-1')) != 0) { - $content = $element->find('p.mb-1', 0)->innertext; - } else{ - $content = 'No description'; - } + // Description + if (count($element->find('p.mb-1')) != 0) { + $content = $element->find('p.mb-1', 0)->innertext; + } else { + $content = 'No description'; + } - // Tags - $content = $content . '<br />'; - $tags = $element->find('a.topic-tag'); - $tags_array = array(); - if (count($tags) != 0) { - $content = $content . 'Tags : '; - foreach($tags as $tag_element) { - $tag_link = 'https://github.com' . $tag_element->href; - $tag_name = trim($tag_element->innertext); - $content = $content . '<a href="' . $tag_link . '">' . $tag_name . '</a> '; - array_push($tags_array, $tag_element->innertext); - } - } + // Tags + $content = $content . '<br />'; + $tags = $element->find('a.topic-tag'); + $tags_array = []; + if (count($tags) != 0) { + $content = $content . 'Tags : '; + foreach ($tags as $tag_element) { + $tag_link = 'https://github.com' . $tag_element->href; + $tag_name = trim($tag_element->innertext); + $content = $content . '<a href="' . $tag_link . '">' . $tag_name . '</a> '; + array_push($tags_array, $tag_element->innertext); + } + } - $item['categories'] = $tags_array; - $item['content'] = $content; - $date = $element->find('relative-time', 0)->datetime; - $item['timestamp'] = strtotime($date); + $item['categories'] = $tags_array; + $item['content'] = $content; + $date = $element->find('relative-time', 0)->datetime; + $item['timestamp'] = strtotime($date); - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/GithubTrendingBridge.php b/bridges/GithubTrendingBridge.php index 16705b80..50d7d37d 100644 --- a/bridges/GithubTrendingBridge.php +++ b/bridges/GithubTrendingBridge.php @@ -1,630 +1,634 @@ <?php -class GithubTrendingBridge extends BridgeAbstract { - const MAINTAINER = 'liamka'; - const NAME = 'Github Trending'; - const URI = 'https://github.com/trending'; - const URI_ITEM = 'https://github.com'; - const CACHE_TIMEOUT = 43200; // 12hr - const DESCRIPTION = 'See what the GitHub community is most excited repos.'; - const PARAMETERS = array( - // If you are changing context and/or parameter names, change them also in getName(). - 'By language' => array( - 'language' => array( - 'name' => 'Select language', - 'type' => 'list', - 'values' => array( - 'All languages' => '', - 'HTML' => 'html', - 'PHP' => 'php', - 'Python' => 'python', - 'Shell' => 'shell', - 'Unknown languages' => 'unknown', - '1C Enterprise' => '1c-enterprise', - '4D' => '4d', - 'ABAP' => 'abap', - 'ABNF' => 'abnf', - 'ActionScript' => 'actionscript', - 'Ada' => 'ada', - 'Adobe Font Metrics' => 'adobe-font-metrics', - 'Agda' => 'agda', - 'AGS Script' => 'ags-script', - 'Alloy' => 'alloy', - 'Alpine Abuild' => 'alpine-abuild', - 'Altium Designer' => 'altium-designer', - 'AMPL' => 'ampl', - 'AngelScript' => 'angelscript', - 'Ant Build System' => 'ant-build-system', - 'ANTLR' => 'antlr', - 'ApacheConf' => 'apacheconf', - 'Apex' => 'apex', - 'API Blueprint' => 'api-blueprint', - 'APL' => 'apl', - 'Apollo Guidance Computer' => 'apollo-guidance-computer', - 'AppleScript' => 'applescript', - 'Arc' => 'arc', - 'AsciiDoc' => 'asciidoc', - 'ASN.1' => 'asn.1', - 'ASP' => 'asp', - 'AspectJ' => 'aspectj', - 'Assembly' => 'assembly', - 'Asymptote' => 'asymptote', - 'ATS' => 'ats', - 'Augeas' => 'augeas', - 'AutoHotkey' => 'autohotkey', - 'AutoIt' => 'autoit', - 'Awk' => 'awk', - 'Ballerina' => 'ballerina', - 'Batchfile' => 'batchfile', - 'Befunge' => 'befunge', - 'BibTeX' => 'bibtex', - 'Bison' => 'bison', - 'BitBake' => 'bitbake', - 'Blade' => 'blade', - 'BlitzBasic' => 'blitzbasic', - 'BlitzMax' => 'blitzmax', - 'Bluespec' => 'bluespec', - 'Boo' => 'boo', - 'Brainfuck' => 'brainfuck', - 'Brightscript' => 'brightscript', - 'Zeek' => 'zeek', - 'C' => 'c', - 'C#' => 'c%23', // already URL encoded - 'C++' => 'c++', - 'C-ObjDump' => 'c-objdump', - 'C2hs Haskell' => 'c2hs-haskell', - 'Cabal Config' => 'cabal-config', - 'Cap\'n Proto' => 'cap\'n-proto', - 'CartoCSS' => 'cartocss', - 'Ceylon' => 'ceylon', - 'Chapel' => 'chapel', - 'Charity' => 'charity', - 'ChucK' => 'chuck', - 'Cirru' => 'cirru', - 'Clarion' => 'clarion', - 'Clean' => 'clean', - 'Click' => 'click', - 'CLIPS' => 'clips', - 'Clojure' => 'clojure', - 'Closure Templates' => 'closure-templates', - 'Cloud Firestore Security Rules' => 'cloud-firestore-security-rules', - 'CMake' => 'cmake', - 'COBOL' => 'cobol', - 'CodeQL' => 'codeql', - 'CoffeeScript' => 'coffeescript', - 'ColdFusion' => 'coldfusion', - 'ColdFusion CFC' => 'coldfusion-cfc', - 'COLLADA' => 'collada', - 'Common Lisp' => 'common-lisp', - 'Common Workflow Language' => 'common-workflow-language', - 'Component Pascal' => 'component-pascal', - 'CoNLL-U' => 'conll-u', - 'Cool' => 'cool', - 'Coq' => 'coq', - 'Cpp-ObjDump' => 'cpp-objdump', - 'Creole' => 'creole', - 'Crystal' => 'crystal', - 'CSON' => 'cson', - 'Csound' => 'csound', - 'Csound Document' => 'csound-document', - 'Csound Score' => 'csound-score', - 'CSS' => 'css', - 'CSV' => 'csv', - 'Cuda' => 'cuda', - 'cURL Config' => 'curl-config', - 'CWeb' => 'cweb', - 'Cycript' => 'cycript', - 'Cython' => 'cython', - 'D' => 'd', - 'D-ObjDump' => 'd-objdump', - 'Darcs Patch' => 'darcs-patch', - 'Dart' => 'dart', - 'DataWeave' => 'dataweave', - 'desktop' => 'desktop', - 'Dhall' => 'dhall', - 'Diff' => 'diff', - 'DIGITAL Command Language' => 'digital-command-language', - 'dircolors' => 'dircolors', - 'DirectX 3D File' => 'directx-3d-file', - 'DM' => 'dm', - 'DNS Zone' => 'dns-zone', - 'Dockerfile' => 'dockerfile', - 'Dogescript' => 'dogescript', - 'DTrace' => 'dtrace', - 'Dylan' => 'dylan', - 'E' => 'e', - 'Eagle' => 'eagle', - 'Easybuild' => 'easybuild', - 'EBNF' => 'ebnf', - 'eC' => 'ec', - 'Ecere Projects' => 'ecere-projects', - 'ECL' => 'ecl', - 'ECLiPSe' => 'eclipse', - 'EditorConfig' => 'editorconfig', - 'Edje Data Collection' => 'edje-data-collection', - 'edn' => 'edn', - 'Eiffel' => 'eiffel', - 'EJS' => 'ejs', - 'Elixir' => 'elixir', - 'Elm' => 'elm', - 'Emacs Lisp' => 'emacs-lisp', - 'EmberScript' => 'emberscript', - 'EML' => 'eml', - 'EQ' => 'eq', - 'Erlang' => 'erlang', - 'F#' => 'f%23', // already URL encoded - 'F*' => 'f*', - 'Factor' => 'factor', - 'Fancy' => 'fancy', - 'Fantom' => 'fantom', - 'Faust' => 'faust', - 'FIGlet Font' => 'figlet-font', - 'Filebench WML' => 'filebench-wml', - 'Filterscript' => 'filterscript', - 'fish' => 'fish', - 'FLUX' => 'flux', - 'Formatted' => 'formatted', - 'Forth' => 'forth', - 'Fortran' => 'fortran', - 'FreeMarker' => 'freemarker', - 'Frege' => 'frege', - 'G-code' => 'g-code', - 'Game Maker Language' => 'game-maker-language', - 'GAML' => 'gaml', - 'GAMS' => 'gams', - 'GAP' => 'gap', - 'GCC Machine Description' => 'gcc-machine-description', - 'GDB' => 'gdb', - 'GDScript' => 'gdscript', - 'Genie' => 'genie', - 'Genshi' => 'genshi', - 'Gentoo Ebuild' => 'gentoo-ebuild', - 'Gentoo Eclass' => 'gentoo-eclass', - 'Gerber Image' => 'gerber-image', - 'Gettext Catalog' => 'gettext-catalog', - 'Gherkin' => 'gherkin', - 'Git Attributes' => 'git-attributes', - 'Git Config' => 'git-config', - 'GLSL' => 'glsl', - 'Glyph' => 'glyph', - 'Glyph Bitmap Distribution Format' => 'glyph-bitmap-distribution-format', - 'GN' => 'gn', - 'Gnuplot' => 'gnuplot', - 'Go' => 'go', - 'Golo' => 'golo', - 'Gosu' => 'gosu', - 'Grace' => 'grace', - 'Gradle' => 'gradle', - 'Grammatical Framework' => 'grammatical-framework', - 'Graph Modeling Language' => 'graph-modeling-language', - 'GraphQL' => 'graphql', - 'Graphviz (DOT)' => 'graphviz-(dot)', - 'Groovy' => 'groovy', - 'Groovy Server Pages' => 'groovy-server-pages', - 'Hack' => 'hack', - 'Haml' => 'haml', - 'Handlebars' => 'handlebars', - 'HAProxy' => 'haproxy', - 'Harbour' => 'harbour', - 'Haskell' => 'haskell', - 'Haxe' => 'haxe', - 'HCL' => 'hcl', - 'HiveQL' => 'hiveql', - 'HLSL' => 'hlsl', - 'HolyC' => 'holyc', - 'HTML+Django' => 'html+django', - 'HTML+ECR' => 'html+ecr', - 'HTML+EEX' => 'html+eex', - 'HTML+ERB' => 'html+erb', - 'HTML+PHP' => 'html+php', - 'HTML+Razor' => 'html+razor', - 'HTTP' => 'http', - 'HXML' => 'hxml', - 'Hy' => 'hy', - 'HyPhy' => 'hyphy', - 'IDL' => 'idl', - 'Idris' => 'idris', - 'Ignore List' => 'ignore-list', - 'IGOR Pro' => 'igor-pro', - 'Inform 7' => 'inform-7', - 'INI' => 'ini', - 'Inno Setup' => 'inno-setup', - 'Io' => 'io', - 'Ioke' => 'ioke', - 'IRC log' => 'irc-log', - 'Isabelle' => 'isabelle', - 'Isabelle ROOT' => 'isabelle-root', - 'J' => 'j', - 'Jasmin' => 'jasmin', - 'Java' => 'java', - 'Java Properties' => 'java-properties', - 'Java Server Pages' => 'java-server-pages', - 'JavaScript' => 'javascript', - 'JavaScript+ERB' => 'javascript+erb', - 'JFlex' => 'jflex', - 'Jison' => 'jison', - 'Jison Lex' => 'jison-lex', - 'Jolie' => 'jolie', - 'JSON' => 'json', - 'JSON with Comments' => 'json-with-comments', - 'JSON5' => 'json5', - 'JSONiq' => 'jsoniq', - 'JSONLD' => 'jsonld', - 'Jsonnet' => 'jsonnet', - 'JSX' => 'jsx', - 'Julia' => 'julia', - 'Jupyter Notebook' => 'jupyter-notebook', - 'KiCad Layout' => 'kicad-layout', - 'KiCad Legacy Layout' => 'kicad-legacy-layout', - 'KiCad Schematic' => 'kicad-schematic', - 'Kit' => 'kit', - 'Kotlin' => 'kotlin', - 'KRL' => 'krl', - 'LabVIEW' => 'labview', - 'Lasso' => 'lasso', - 'Latte' => 'latte', - 'Lean' => 'lean', - 'Less' => 'less', - 'Lex' => 'lex', - 'LFE' => 'lfe', - 'LilyPond' => 'lilypond', - 'Limbo' => 'limbo', - 'Linker Script' => 'linker-script', - 'Linux Kernel Module' => 'linux-kernel-module', - 'Liquid' => 'liquid', - 'Literate Agda' => 'literate-agda', - 'Literate CoffeeScript' => 'literate-coffeescript', - 'Literate Haskell' => 'literate-haskell', - 'LiveScript' => 'livescript', - 'LLVM' => 'llvm', - 'Logos' => 'logos', - 'Logtalk' => 'logtalk', - 'LOLCODE' => 'lolcode', - 'LookML' => 'lookml', - 'LoomScript' => 'loomscript', - 'LSL' => 'lsl', - 'LTspice Symbol' => 'ltspice-symbol', - 'Lua' => 'lua', - 'M' => 'm', - 'M4' => 'm4', - 'M4Sugar' => 'm4sugar', - 'Makefile' => 'makefile', - 'Mako' => 'mako', - 'Markdown' => 'markdown', - 'Marko' => 'marko', - 'Mask' => 'mask', - 'Mathematica' => 'mathematica', - 'MATLAB' => 'matlab', - 'Maven POM' => 'maven-pom', - 'Max' => 'max', - 'MAXScript' => 'maxscript', - 'mcfunction' => 'mcfunction', - 'MediaWiki' => 'mediawiki', - 'Mercury' => 'mercury', - 'Meson' => 'meson', - 'Metal' => 'metal', - 'Microsoft Developer Studio Project' => 'microsoft-developer-studio-project', - 'MiniD' => 'minid', - 'Mirah' => 'mirah', - 'mIRC Script' => 'mirc-script', - 'MLIR' => 'mlir', - 'Modelica' => 'modelica', - 'Modula-2' => 'modula-2', - 'Modula-3' => 'modula-3', - 'Module Management System' => 'module-management-system', - 'Monkey' => 'monkey', - 'Moocode' => 'moocode', - 'MoonScript' => 'moonscript', - 'Motorola 68K Assembly' => 'motorola-68k-assembly', - 'MQL4' => 'mql4', - 'MQL5' => 'mql5', - 'MTML' => 'mtml', - 'MUF' => 'muf', - 'mupad' => 'mupad', - 'Muse' => 'muse', - 'Myghty' => 'myghty', - 'nanorc' => 'nanorc', - 'NASL' => 'nasl', - 'NCL' => 'ncl', - 'Nearley' => 'nearley', - 'Nemerle' => 'nemerle', - 'nesC' => 'nesc', - 'NetLinx' => 'netlinx', - 'NetLinx+ERB' => 'netlinx+erb', - 'NetLogo' => 'netlogo', - 'NewLisp' => 'newlisp', - 'Nextflow' => 'nextflow', - 'Nginx' => 'nginx', - 'Nim' => 'nim', - 'Ninja' => 'ninja', - 'Nit' => 'nit', - 'Nix' => 'nix', - 'NL' => 'nl', - 'NPM Config' => 'npm-config', - 'NSIS' => 'nsis', - 'Nu' => 'nu', - 'NumPy' => 'numpy', - 'ObjDump' => 'objdump', - 'Object Data Instance Notation' => 'object-data-instance-notation', - 'Objective-C' => 'objective-c', - 'Objective-C++' => 'objective-c++', - 'Objective-J' => 'objective-j', - 'ObjectScript' => 'objectscript', - 'OCaml' => 'ocaml', - 'Odin' => 'odin', - 'Omgrofl' => 'omgrofl', - 'ooc' => 'ooc', - 'Opa' => 'opa', - 'Opal' => 'opal', - 'Open Policy Agent' => 'open-policy-agent', - 'OpenCL' => 'opencl', - 'OpenEdge ABL' => 'openedge-abl', - 'OpenQASM' => 'openqasm', - 'OpenRC runscript' => 'openrc-runscript', - 'OpenSCAD' => 'openscad', - 'OpenStep Property List' => 'openstep-property-list', - 'OpenType Feature File' => 'opentype-feature-file', - 'Org' => 'org', - 'Ox' => 'ox', - 'Oxygene' => 'oxygene', - 'Oz' => 'oz', - 'P4' => 'p4', - 'Pan' => 'pan', - 'Papyrus' => 'papyrus', - 'Parrot' => 'parrot', - 'Parrot Assembly' => 'parrot-assembly', - 'Parrot Internal Representation' => 'parrot-internal-representation', - 'Pascal' => 'pascal', - 'Pawn' => 'pawn', - 'Pep8' => 'pep8', - 'Perl' => 'perl', - 'Pic' => 'pic', - 'Pickle' => 'pickle', - 'PicoLisp' => 'picolisp', - 'PigLatin' => 'piglatin', - 'Pike' => 'pike', - 'PLpgSQL' => 'plpgsql', - 'PLSQL' => 'plsql', - 'Pod' => 'pod', - 'Pod 6' => 'pod-6', - 'PogoScript' => 'pogoscript', - 'Pony' => 'pony', - 'PostCSS' => 'postcss', - 'PostScript' => 'postscript', - 'POV-Ray SDL' => 'pov-ray-sdl', - 'PowerBuilder' => 'powerbuilder', - 'PowerShell' => 'powershell', - 'Prisma' => 'prisma', - 'Processing' => 'processing', - 'Proguard' => 'proguard', - 'Prolog' => 'prolog', - 'Propeller Spin' => 'propeller-spin', - 'Protocol Buffer' => 'protocol-buffer', - 'Public Key' => 'public-key', - 'Pug' => 'pug', - 'Puppet' => 'puppet', - 'Pure Data' => 'pure-data', - 'PureBasic' => 'purebasic', - 'PureScript' => 'purescript', - 'Python console' => 'python-console', - 'Python traceback' => 'python-traceback', - 'q' => 'q', - 'QMake' => 'qmake', - 'QML' => 'qml', - 'Quake' => 'quake', - 'R' => 'r', - 'Racket' => 'racket', - 'Ragel' => 'ragel', - 'Raku' => 'raku', - 'RAML' => 'raml', - 'Rascal' => 'rascal', - 'Raw token data' => 'raw-token-data', - 'RDoc' => 'rdoc', - 'Readline Config' => 'readline-config', - 'REALbasic' => 'realbasic', - 'Reason' => 'reason', - 'Rebol' => 'rebol', - 'Red' => 'red', - 'Redcode' => 'redcode', - 'Regular Expression' => 'regular-expression', - 'Ren\'Py' => 'ren\'py', - 'RenderScript' => 'renderscript', - 'reStructuredText' => 'restructuredtext', - 'REXX' => 'rexx', - 'RHTML' => 'rhtml', - 'Rich Text Format' => 'rich-text-format', - 'Ring' => 'ring', - 'Riot' => 'riot', - 'RMarkdown' => 'rmarkdown', - 'RobotFramework' => 'robotframework', - 'Roff' => 'roff', - 'Roff Manpage' => 'roff-manpage', - 'Rouge' => 'rouge', - 'RPC' => 'rpc', - 'RPM Spec' => 'rpm-spec', - 'Ruby' => 'ruby', - 'RUNOFF' => 'runoff', - 'Rust' => 'rust', - 'Sage' => 'sage', - 'SaltStack' => 'saltstack', - 'SAS' => 'sas', - 'Sass' => 'sass', - 'Scala' => 'scala', - 'Scaml' => 'scaml', - 'Scheme' => 'scheme', - 'Scilab' => 'scilab', - 'SCSS' => 'scss', - 'sed' => 'sed', - 'Self' => 'self', - 'ShaderLab' => 'shaderlab', - 'ShellSession' => 'shellsession', - 'Shen' => 'shen', - 'Slash' => 'slash', - 'Slice' => 'slice', - 'Slim' => 'slim', - 'Smali' => 'smali', - 'Smalltalk' => 'smalltalk', - 'Smarty' => 'smarty', - 'SmPL' => 'smpl', - 'SMT' => 'smt', - 'Solidity' => 'solidity', - 'SourcePawn' => 'sourcepawn', - 'SPARQL' => 'sparql', - 'Spline Font Database' => 'spline-font-database', - 'SQF' => 'sqf', - 'SQL' => 'sql', - 'SQLPL' => 'sqlpl', - 'Squirrel' => 'squirrel', - 'SRecode Template' => 'srecode-template', - 'SSH Config' => 'ssh-config', - 'Stan' => 'stan', - 'Standard ML' => 'standard-ml', - 'Starlark' => 'starlark', - 'Stata' => 'stata', - 'STON' => 'ston', - 'Stylus' => 'stylus', - 'SubRip Text' => 'subrip-text', - 'SugarSS' => 'sugarss', - 'SuperCollider' => 'supercollider', - 'Svelte' => 'svelte', - 'SVG' => 'svg', - 'Swift' => 'swift', - 'SWIG' => 'swig', - 'SystemVerilog' => 'systemverilog', - 'Tcl' => 'tcl', - 'Tcsh' => 'tcsh', - 'Tea' => 'tea', - 'Terra' => 'terra', - 'TeX' => 'tex', - 'Texinfo' => 'texinfo', - 'Text' => 'text', - 'Textile' => 'textile', - 'Thrift' => 'thrift', - 'TI Program' => 'ti-program', - 'TLA' => 'tla', - 'TOML' => 'toml', - 'TSQL' => 'tsql', - 'TSX' => 'tsx', - 'Turing' => 'turing', - 'Turtle' => 'turtle', - 'Twig' => 'twig', - 'TXL' => 'txl', - 'Type Language' => 'type-language', - 'TypeScript' => 'typescript', - 'Unified Parallel C' => 'unified-parallel-c', - 'Unity3D Asset' => 'unity3d-asset', - 'Unix Assembly' => 'unix-assembly', - 'Uno' => 'uno', - 'UnrealScript' => 'unrealscript', - 'UrWeb' => 'urweb', - 'V' => 'v', - 'Vala' => 'vala', - 'VBA' => 'vba', - 'VBScript' => 'vbscript', - 'VCL' => 'vcl', - 'Verilog' => 'verilog', - 'VHDL' => 'vhdl', - 'Vim script' => 'vim-script', - 'Vim Snippet' => 'vim-snippet', - 'Visual Basic .NET' => 'visual-basic-.net', - 'Visual Basic .NET' => 'visual-basic-.net', - 'Volt' => 'volt', - 'Vue' => 'vue', - 'Wavefront Material' => 'wavefront-material', - 'Wavefront Object' => 'wavefront-object', - 'wdl' => 'wdl', - 'Web Ontology Language' => 'web-ontology-language', - 'WebAssembly' => 'webassembly', - 'WebIDL' => 'webidl', - 'WebVTT' => 'webvtt', - 'Wget Config' => 'wget-config', - 'Windows Registry Entries' => 'windows-registry-entries', - 'wisp' => 'wisp', - 'Wollok' => 'wollok', - 'World of Warcraft Addon Data' => 'world-of-warcraft-addon-data', - 'X BitMap' => 'x-bitmap', - 'X Font Directory Index' => 'x-font-directory-index', - 'X PixMap' => 'x-pixmap', - 'X10' => 'x10', - 'xBase' => 'xbase', - 'XC' => 'xc', - 'XCompose' => 'xcompose', - 'XML' => 'xml', - 'XML Property List' => 'xml-property-list', - 'Xojo' => 'xojo', - 'XPages' => 'xpages', - 'XProc' => 'xproc', - 'XQuery' => 'xquery', - 'XS' => 'xs', - 'XSLT' => 'xslt', - 'Xtend' => 'xtend', - 'Yacc' => 'yacc', - 'YAML' => 'yaml', - 'YANG' => 'yang', - 'YARA' => 'yara', - 'YASnippet' => 'yasnippet', - 'ZAP' => 'zap', - 'Zeek' => 'zeek', - 'ZenScript' => 'zenscript', - 'Zephir' => 'zephir', - 'Zig' => 'zig', - 'ZIL' => 'zil', - 'Zimpl' => 'zimpl', - ), - 'defaultValue' => 'All languages' - ) - ), +class GithubTrendingBridge extends BridgeAbstract +{ + const MAINTAINER = 'liamka'; + const NAME = 'Github Trending'; + const URI = 'https://github.com/trending'; + const URI_ITEM = 'https://github.com'; + const CACHE_TIMEOUT = 43200; // 12hr + const DESCRIPTION = 'See what the GitHub community is most excited repos.'; + const PARAMETERS = [ + // If you are changing context and/or parameter names, change them also in getName(). + 'By language' => [ + 'language' => [ + 'name' => 'Select language', + 'type' => 'list', + 'values' => [ + 'All languages' => '', + 'HTML' => 'html', + 'PHP' => 'php', + 'Python' => 'python', + 'Shell' => 'shell', + 'Unknown languages' => 'unknown', + '1C Enterprise' => '1c-enterprise', + '4D' => '4d', + 'ABAP' => 'abap', + 'ABNF' => 'abnf', + 'ActionScript' => 'actionscript', + 'Ada' => 'ada', + 'Adobe Font Metrics' => 'adobe-font-metrics', + 'Agda' => 'agda', + 'AGS Script' => 'ags-script', + 'Alloy' => 'alloy', + 'Alpine Abuild' => 'alpine-abuild', + 'Altium Designer' => 'altium-designer', + 'AMPL' => 'ampl', + 'AngelScript' => 'angelscript', + 'Ant Build System' => 'ant-build-system', + 'ANTLR' => 'antlr', + 'ApacheConf' => 'apacheconf', + 'Apex' => 'apex', + 'API Blueprint' => 'api-blueprint', + 'APL' => 'apl', + 'Apollo Guidance Computer' => 'apollo-guidance-computer', + 'AppleScript' => 'applescript', + 'Arc' => 'arc', + 'AsciiDoc' => 'asciidoc', + 'ASN.1' => 'asn.1', + 'ASP' => 'asp', + 'AspectJ' => 'aspectj', + 'Assembly' => 'assembly', + 'Asymptote' => 'asymptote', + 'ATS' => 'ats', + 'Augeas' => 'augeas', + 'AutoHotkey' => 'autohotkey', + 'AutoIt' => 'autoit', + 'Awk' => 'awk', + 'Ballerina' => 'ballerina', + 'Batchfile' => 'batchfile', + 'Befunge' => 'befunge', + 'BibTeX' => 'bibtex', + 'Bison' => 'bison', + 'BitBake' => 'bitbake', + 'Blade' => 'blade', + 'BlitzBasic' => 'blitzbasic', + 'BlitzMax' => 'blitzmax', + 'Bluespec' => 'bluespec', + 'Boo' => 'boo', + 'Brainfuck' => 'brainfuck', + 'Brightscript' => 'brightscript', + 'Zeek' => 'zeek', + 'C' => 'c', + 'C#' => 'c%23', // already URL encoded + 'C++' => 'c++', + 'C-ObjDump' => 'c-objdump', + 'C2hs Haskell' => 'c2hs-haskell', + 'Cabal Config' => 'cabal-config', + 'Cap\'n Proto' => 'cap\'n-proto', + 'CartoCSS' => 'cartocss', + 'Ceylon' => 'ceylon', + 'Chapel' => 'chapel', + 'Charity' => 'charity', + 'ChucK' => 'chuck', + 'Cirru' => 'cirru', + 'Clarion' => 'clarion', + 'Clean' => 'clean', + 'Click' => 'click', + 'CLIPS' => 'clips', + 'Clojure' => 'clojure', + 'Closure Templates' => 'closure-templates', + 'Cloud Firestore Security Rules' => 'cloud-firestore-security-rules', + 'CMake' => 'cmake', + 'COBOL' => 'cobol', + 'CodeQL' => 'codeql', + 'CoffeeScript' => 'coffeescript', + 'ColdFusion' => 'coldfusion', + 'ColdFusion CFC' => 'coldfusion-cfc', + 'COLLADA' => 'collada', + 'Common Lisp' => 'common-lisp', + 'Common Workflow Language' => 'common-workflow-language', + 'Component Pascal' => 'component-pascal', + 'CoNLL-U' => 'conll-u', + 'Cool' => 'cool', + 'Coq' => 'coq', + 'Cpp-ObjDump' => 'cpp-objdump', + 'Creole' => 'creole', + 'Crystal' => 'crystal', + 'CSON' => 'cson', + 'Csound' => 'csound', + 'Csound Document' => 'csound-document', + 'Csound Score' => 'csound-score', + 'CSS' => 'css', + 'CSV' => 'csv', + 'Cuda' => 'cuda', + 'cURL Config' => 'curl-config', + 'CWeb' => 'cweb', + 'Cycript' => 'cycript', + 'Cython' => 'cython', + 'D' => 'd', + 'D-ObjDump' => 'd-objdump', + 'Darcs Patch' => 'darcs-patch', + 'Dart' => 'dart', + 'DataWeave' => 'dataweave', + 'desktop' => 'desktop', + 'Dhall' => 'dhall', + 'Diff' => 'diff', + 'DIGITAL Command Language' => 'digital-command-language', + 'dircolors' => 'dircolors', + 'DirectX 3D File' => 'directx-3d-file', + 'DM' => 'dm', + 'DNS Zone' => 'dns-zone', + 'Dockerfile' => 'dockerfile', + 'Dogescript' => 'dogescript', + 'DTrace' => 'dtrace', + 'Dylan' => 'dylan', + 'E' => 'e', + 'Eagle' => 'eagle', + 'Easybuild' => 'easybuild', + 'EBNF' => 'ebnf', + 'eC' => 'ec', + 'Ecere Projects' => 'ecere-projects', + 'ECL' => 'ecl', + 'ECLiPSe' => 'eclipse', + 'EditorConfig' => 'editorconfig', + 'Edje Data Collection' => 'edje-data-collection', + 'edn' => 'edn', + 'Eiffel' => 'eiffel', + 'EJS' => 'ejs', + 'Elixir' => 'elixir', + 'Elm' => 'elm', + 'Emacs Lisp' => 'emacs-lisp', + 'EmberScript' => 'emberscript', + 'EML' => 'eml', + 'EQ' => 'eq', + 'Erlang' => 'erlang', + 'F#' => 'f%23', // already URL encoded + 'F*' => 'f*', + 'Factor' => 'factor', + 'Fancy' => 'fancy', + 'Fantom' => 'fantom', + 'Faust' => 'faust', + 'FIGlet Font' => 'figlet-font', + 'Filebench WML' => 'filebench-wml', + 'Filterscript' => 'filterscript', + 'fish' => 'fish', + 'FLUX' => 'flux', + 'Formatted' => 'formatted', + 'Forth' => 'forth', + 'Fortran' => 'fortran', + 'FreeMarker' => 'freemarker', + 'Frege' => 'frege', + 'G-code' => 'g-code', + 'Game Maker Language' => 'game-maker-language', + 'GAML' => 'gaml', + 'GAMS' => 'gams', + 'GAP' => 'gap', + 'GCC Machine Description' => 'gcc-machine-description', + 'GDB' => 'gdb', + 'GDScript' => 'gdscript', + 'Genie' => 'genie', + 'Genshi' => 'genshi', + 'Gentoo Ebuild' => 'gentoo-ebuild', + 'Gentoo Eclass' => 'gentoo-eclass', + 'Gerber Image' => 'gerber-image', + 'Gettext Catalog' => 'gettext-catalog', + 'Gherkin' => 'gherkin', + 'Git Attributes' => 'git-attributes', + 'Git Config' => 'git-config', + 'GLSL' => 'glsl', + 'Glyph' => 'glyph', + 'Glyph Bitmap Distribution Format' => 'glyph-bitmap-distribution-format', + 'GN' => 'gn', + 'Gnuplot' => 'gnuplot', + 'Go' => 'go', + 'Golo' => 'golo', + 'Gosu' => 'gosu', + 'Grace' => 'grace', + 'Gradle' => 'gradle', + 'Grammatical Framework' => 'grammatical-framework', + 'Graph Modeling Language' => 'graph-modeling-language', + 'GraphQL' => 'graphql', + 'Graphviz (DOT)' => 'graphviz-(dot)', + 'Groovy' => 'groovy', + 'Groovy Server Pages' => 'groovy-server-pages', + 'Hack' => 'hack', + 'Haml' => 'haml', + 'Handlebars' => 'handlebars', + 'HAProxy' => 'haproxy', + 'Harbour' => 'harbour', + 'Haskell' => 'haskell', + 'Haxe' => 'haxe', + 'HCL' => 'hcl', + 'HiveQL' => 'hiveql', + 'HLSL' => 'hlsl', + 'HolyC' => 'holyc', + 'HTML+Django' => 'html+django', + 'HTML+ECR' => 'html+ecr', + 'HTML+EEX' => 'html+eex', + 'HTML+ERB' => 'html+erb', + 'HTML+PHP' => 'html+php', + 'HTML+Razor' => 'html+razor', + 'HTTP' => 'http', + 'HXML' => 'hxml', + 'Hy' => 'hy', + 'HyPhy' => 'hyphy', + 'IDL' => 'idl', + 'Idris' => 'idris', + 'Ignore List' => 'ignore-list', + 'IGOR Pro' => 'igor-pro', + 'Inform 7' => 'inform-7', + 'INI' => 'ini', + 'Inno Setup' => 'inno-setup', + 'Io' => 'io', + 'Ioke' => 'ioke', + 'IRC log' => 'irc-log', + 'Isabelle' => 'isabelle', + 'Isabelle ROOT' => 'isabelle-root', + 'J' => 'j', + 'Jasmin' => 'jasmin', + 'Java' => 'java', + 'Java Properties' => 'java-properties', + 'Java Server Pages' => 'java-server-pages', + 'JavaScript' => 'javascript', + 'JavaScript+ERB' => 'javascript+erb', + 'JFlex' => 'jflex', + 'Jison' => 'jison', + 'Jison Lex' => 'jison-lex', + 'Jolie' => 'jolie', + 'JSON' => 'json', + 'JSON with Comments' => 'json-with-comments', + 'JSON5' => 'json5', + 'JSONiq' => 'jsoniq', + 'JSONLD' => 'jsonld', + 'Jsonnet' => 'jsonnet', + 'JSX' => 'jsx', + 'Julia' => 'julia', + 'Jupyter Notebook' => 'jupyter-notebook', + 'KiCad Layout' => 'kicad-layout', + 'KiCad Legacy Layout' => 'kicad-legacy-layout', + 'KiCad Schematic' => 'kicad-schematic', + 'Kit' => 'kit', + 'Kotlin' => 'kotlin', + 'KRL' => 'krl', + 'LabVIEW' => 'labview', + 'Lasso' => 'lasso', + 'Latte' => 'latte', + 'Lean' => 'lean', + 'Less' => 'less', + 'Lex' => 'lex', + 'LFE' => 'lfe', + 'LilyPond' => 'lilypond', + 'Limbo' => 'limbo', + 'Linker Script' => 'linker-script', + 'Linux Kernel Module' => 'linux-kernel-module', + 'Liquid' => 'liquid', + 'Literate Agda' => 'literate-agda', + 'Literate CoffeeScript' => 'literate-coffeescript', + 'Literate Haskell' => 'literate-haskell', + 'LiveScript' => 'livescript', + 'LLVM' => 'llvm', + 'Logos' => 'logos', + 'Logtalk' => 'logtalk', + 'LOLCODE' => 'lolcode', + 'LookML' => 'lookml', + 'LoomScript' => 'loomscript', + 'LSL' => 'lsl', + 'LTspice Symbol' => 'ltspice-symbol', + 'Lua' => 'lua', + 'M' => 'm', + 'M4' => 'm4', + 'M4Sugar' => 'm4sugar', + 'Makefile' => 'makefile', + 'Mako' => 'mako', + 'Markdown' => 'markdown', + 'Marko' => 'marko', + 'Mask' => 'mask', + 'Mathematica' => 'mathematica', + 'MATLAB' => 'matlab', + 'Maven POM' => 'maven-pom', + 'Max' => 'max', + 'MAXScript' => 'maxscript', + 'mcfunction' => 'mcfunction', + 'MediaWiki' => 'mediawiki', + 'Mercury' => 'mercury', + 'Meson' => 'meson', + 'Metal' => 'metal', + 'Microsoft Developer Studio Project' => 'microsoft-developer-studio-project', + 'MiniD' => 'minid', + 'Mirah' => 'mirah', + 'mIRC Script' => 'mirc-script', + 'MLIR' => 'mlir', + 'Modelica' => 'modelica', + 'Modula-2' => 'modula-2', + 'Modula-3' => 'modula-3', + 'Module Management System' => 'module-management-system', + 'Monkey' => 'monkey', + 'Moocode' => 'moocode', + 'MoonScript' => 'moonscript', + 'Motorola 68K Assembly' => 'motorola-68k-assembly', + 'MQL4' => 'mql4', + 'MQL5' => 'mql5', + 'MTML' => 'mtml', + 'MUF' => 'muf', + 'mupad' => 'mupad', + 'Muse' => 'muse', + 'Myghty' => 'myghty', + 'nanorc' => 'nanorc', + 'NASL' => 'nasl', + 'NCL' => 'ncl', + 'Nearley' => 'nearley', + 'Nemerle' => 'nemerle', + 'nesC' => 'nesc', + 'NetLinx' => 'netlinx', + 'NetLinx+ERB' => 'netlinx+erb', + 'NetLogo' => 'netlogo', + 'NewLisp' => 'newlisp', + 'Nextflow' => 'nextflow', + 'Nginx' => 'nginx', + 'Nim' => 'nim', + 'Ninja' => 'ninja', + 'Nit' => 'nit', + 'Nix' => 'nix', + 'NL' => 'nl', + 'NPM Config' => 'npm-config', + 'NSIS' => 'nsis', + 'Nu' => 'nu', + 'NumPy' => 'numpy', + 'ObjDump' => 'objdump', + 'Object Data Instance Notation' => 'object-data-instance-notation', + 'Objective-C' => 'objective-c', + 'Objective-C++' => 'objective-c++', + 'Objective-J' => 'objective-j', + 'ObjectScript' => 'objectscript', + 'OCaml' => 'ocaml', + 'Odin' => 'odin', + 'Omgrofl' => 'omgrofl', + 'ooc' => 'ooc', + 'Opa' => 'opa', + 'Opal' => 'opal', + 'Open Policy Agent' => 'open-policy-agent', + 'OpenCL' => 'opencl', + 'OpenEdge ABL' => 'openedge-abl', + 'OpenQASM' => 'openqasm', + 'OpenRC runscript' => 'openrc-runscript', + 'OpenSCAD' => 'openscad', + 'OpenStep Property List' => 'openstep-property-list', + 'OpenType Feature File' => 'opentype-feature-file', + 'Org' => 'org', + 'Ox' => 'ox', + 'Oxygene' => 'oxygene', + 'Oz' => 'oz', + 'P4' => 'p4', + 'Pan' => 'pan', + 'Papyrus' => 'papyrus', + 'Parrot' => 'parrot', + 'Parrot Assembly' => 'parrot-assembly', + 'Parrot Internal Representation' => 'parrot-internal-representation', + 'Pascal' => 'pascal', + 'Pawn' => 'pawn', + 'Pep8' => 'pep8', + 'Perl' => 'perl', + 'Pic' => 'pic', + 'Pickle' => 'pickle', + 'PicoLisp' => 'picolisp', + 'PigLatin' => 'piglatin', + 'Pike' => 'pike', + 'PLpgSQL' => 'plpgsql', + 'PLSQL' => 'plsql', + 'Pod' => 'pod', + 'Pod 6' => 'pod-6', + 'PogoScript' => 'pogoscript', + 'Pony' => 'pony', + 'PostCSS' => 'postcss', + 'PostScript' => 'postscript', + 'POV-Ray SDL' => 'pov-ray-sdl', + 'PowerBuilder' => 'powerbuilder', + 'PowerShell' => 'powershell', + 'Prisma' => 'prisma', + 'Processing' => 'processing', + 'Proguard' => 'proguard', + 'Prolog' => 'prolog', + 'Propeller Spin' => 'propeller-spin', + 'Protocol Buffer' => 'protocol-buffer', + 'Public Key' => 'public-key', + 'Pug' => 'pug', + 'Puppet' => 'puppet', + 'Pure Data' => 'pure-data', + 'PureBasic' => 'purebasic', + 'PureScript' => 'purescript', + 'Python console' => 'python-console', + 'Python traceback' => 'python-traceback', + 'q' => 'q', + 'QMake' => 'qmake', + 'QML' => 'qml', + 'Quake' => 'quake', + 'R' => 'r', + 'Racket' => 'racket', + 'Ragel' => 'ragel', + 'Raku' => 'raku', + 'RAML' => 'raml', + 'Rascal' => 'rascal', + 'Raw token data' => 'raw-token-data', + 'RDoc' => 'rdoc', + 'Readline Config' => 'readline-config', + 'REALbasic' => 'realbasic', + 'Reason' => 'reason', + 'Rebol' => 'rebol', + 'Red' => 'red', + 'Redcode' => 'redcode', + 'Regular Expression' => 'regular-expression', + 'Ren\'Py' => 'ren\'py', + 'RenderScript' => 'renderscript', + 'reStructuredText' => 'restructuredtext', + 'REXX' => 'rexx', + 'RHTML' => 'rhtml', + 'Rich Text Format' => 'rich-text-format', + 'Ring' => 'ring', + 'Riot' => 'riot', + 'RMarkdown' => 'rmarkdown', + 'RobotFramework' => 'robotframework', + 'Roff' => 'roff', + 'Roff Manpage' => 'roff-manpage', + 'Rouge' => 'rouge', + 'RPC' => 'rpc', + 'RPM Spec' => 'rpm-spec', + 'Ruby' => 'ruby', + 'RUNOFF' => 'runoff', + 'Rust' => 'rust', + 'Sage' => 'sage', + 'SaltStack' => 'saltstack', + 'SAS' => 'sas', + 'Sass' => 'sass', + 'Scala' => 'scala', + 'Scaml' => 'scaml', + 'Scheme' => 'scheme', + 'Scilab' => 'scilab', + 'SCSS' => 'scss', + 'sed' => 'sed', + 'Self' => 'self', + 'ShaderLab' => 'shaderlab', + 'ShellSession' => 'shellsession', + 'Shen' => 'shen', + 'Slash' => 'slash', + 'Slice' => 'slice', + 'Slim' => 'slim', + 'Smali' => 'smali', + 'Smalltalk' => 'smalltalk', + 'Smarty' => 'smarty', + 'SmPL' => 'smpl', + 'SMT' => 'smt', + 'Solidity' => 'solidity', + 'SourcePawn' => 'sourcepawn', + 'SPARQL' => 'sparql', + 'Spline Font Database' => 'spline-font-database', + 'SQF' => 'sqf', + 'SQL' => 'sql', + 'SQLPL' => 'sqlpl', + 'Squirrel' => 'squirrel', + 'SRecode Template' => 'srecode-template', + 'SSH Config' => 'ssh-config', + 'Stan' => 'stan', + 'Standard ML' => 'standard-ml', + 'Starlark' => 'starlark', + 'Stata' => 'stata', + 'STON' => 'ston', + 'Stylus' => 'stylus', + 'SubRip Text' => 'subrip-text', + 'SugarSS' => 'sugarss', + 'SuperCollider' => 'supercollider', + 'Svelte' => 'svelte', + 'SVG' => 'svg', + 'Swift' => 'swift', + 'SWIG' => 'swig', + 'SystemVerilog' => 'systemverilog', + 'Tcl' => 'tcl', + 'Tcsh' => 'tcsh', + 'Tea' => 'tea', + 'Terra' => 'terra', + 'TeX' => 'tex', + 'Texinfo' => 'texinfo', + 'Text' => 'text', + 'Textile' => 'textile', + 'Thrift' => 'thrift', + 'TI Program' => 'ti-program', + 'TLA' => 'tla', + 'TOML' => 'toml', + 'TSQL' => 'tsql', + 'TSX' => 'tsx', + 'Turing' => 'turing', + 'Turtle' => 'turtle', + 'Twig' => 'twig', + 'TXL' => 'txl', + 'Type Language' => 'type-language', + 'TypeScript' => 'typescript', + 'Unified Parallel C' => 'unified-parallel-c', + 'Unity3D Asset' => 'unity3d-asset', + 'Unix Assembly' => 'unix-assembly', + 'Uno' => 'uno', + 'UnrealScript' => 'unrealscript', + 'UrWeb' => 'urweb', + 'V' => 'v', + 'Vala' => 'vala', + 'VBA' => 'vba', + 'VBScript' => 'vbscript', + 'VCL' => 'vcl', + 'Verilog' => 'verilog', + 'VHDL' => 'vhdl', + 'Vim script' => 'vim-script', + 'Vim Snippet' => 'vim-snippet', + 'Visual Basic .NET' => 'visual-basic-.net', + 'Visual Basic .NET' => 'visual-basic-.net', + 'Volt' => 'volt', + 'Vue' => 'vue', + 'Wavefront Material' => 'wavefront-material', + 'Wavefront Object' => 'wavefront-object', + 'wdl' => 'wdl', + 'Web Ontology Language' => 'web-ontology-language', + 'WebAssembly' => 'webassembly', + 'WebIDL' => 'webidl', + 'WebVTT' => 'webvtt', + 'Wget Config' => 'wget-config', + 'Windows Registry Entries' => 'windows-registry-entries', + 'wisp' => 'wisp', + 'Wollok' => 'wollok', + 'World of Warcraft Addon Data' => 'world-of-warcraft-addon-data', + 'X BitMap' => 'x-bitmap', + 'X Font Directory Index' => 'x-font-directory-index', + 'X PixMap' => 'x-pixmap', + 'X10' => 'x10', + 'xBase' => 'xbase', + 'XC' => 'xc', + 'XCompose' => 'xcompose', + 'XML' => 'xml', + 'XML Property List' => 'xml-property-list', + 'Xojo' => 'xojo', + 'XPages' => 'xpages', + 'XProc' => 'xproc', + 'XQuery' => 'xquery', + 'XS' => 'xs', + 'XSLT' => 'xslt', + 'Xtend' => 'xtend', + 'Yacc' => 'yacc', + 'YAML' => 'yaml', + 'YANG' => 'yang', + 'YARA' => 'yara', + 'YASnippet' => 'yasnippet', + 'ZAP' => 'zap', + 'Zeek' => 'zeek', + 'ZenScript' => 'zenscript', + 'Zephir' => 'zephir', + 'Zig' => 'zig', + 'ZIL' => 'zil', + 'Zimpl' => 'zimpl', + ], + 'defaultValue' => 'All languages' + ] + ], - 'global' => array( - 'date_range' => array( - 'name' => 'Date range', - 'type' => 'list', - 'values' => array( - 'Today' => 'today', - 'Weekly' => 'weekly', - 'Monthly' => 'monthly', - ), - 'defaultValue' => 'today' - ) - ) + 'global' => [ + 'date_range' => [ + 'name' => 'Date range', + 'type' => 'list', + 'values' => [ + 'Today' => 'today', + 'Weekly' => 'weekly', + 'Monthly' => 'monthly', + ], + 'defaultValue' => 'today' + ] + ] - ); + ]; - public function collectData(){ - $params = array('since' => urlencode($this->getInput('date_range'))); - $url = self::URI . '/' . $this->getInput('language') . '?' . http_build_query($params); + public function collectData() + { + $params = ['since' => urlencode($this->getInput('date_range'))]; + $url = self::URI . '/' . $this->getInput('language') . '?' . http_build_query($params); - $html = getSimpleHTMLDOM($url); + $html = getSimpleHTMLDOM($url); - $this->items = array(); - foreach($html->find('.Box-row') as $element) { - $item = array(); + $this->items = []; + foreach ($html->find('.Box-row') as $element) { + $item = []; - // URI - $item['uri'] = self::URI_ITEM . $element->find('h1 a', 0)->href; + // URI + $item['uri'] = self::URI_ITEM . $element->find('h1 a', 0)->href; - // Title - $item['title'] = str_replace(' ', '', trim(strip_tags($element->find('h1 a', 0)->plaintext))); + // Title + $item['title'] = str_replace(' ', '', trim(strip_tags($element->find('h1 a', 0)->plaintext))); - // Description - $description = $element->find('p', 0); - if ($description != null) - $item['content'] = trim(strip_tags($description->innertext)); + // Description + $description = $element->find('p', 0); + if ($description != null) { + $item['content'] = trim(strip_tags($description->innertext)); + } - // Time - $item['timestamp'] = time(); + // Time + $item['timestamp'] = time(); - // TODO: Proxy? - $this->items[] = $item; - } - } + // TODO: Proxy? + $this->items[] = $item; + } + } - public function getName(){ - if (!is_null($this->getInput('language'))) { - $language = array_search($this->getInput('language'), self::PARAMETERS['By language']['language']['values']); - return self::NAME . ': ' . $language; - } + public function getName() + { + if (!is_null($this->getInput('language'))) { + $language = array_search($this->getInput('language'), self::PARAMETERS['By language']['language']['values']); + return self::NAME . ': ' . $language; + } - return parent::getName(); - } + return parent::getName(); + } } diff --git a/bridges/GitlabIssueBridge.php b/bridges/GitlabIssueBridge.php index ce3ab08b..ebcdbb4c 100644 --- a/bridges/GitlabIssueBridge.php +++ b/bridges/GitlabIssueBridge.php @@ -1,205 +1,214 @@ <?php -class GitlabIssueBridge extends BridgeAbstract { - - const MAINTAINER = 'Mynacol'; - const NAME = 'Gitlab Issue/Merge Request'; - const URI = 'https://gitlab.com/'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns comments of an issue/MR of a gitlab project'; - - const PARAMETERS = array( - 'global' => array( - 'h' => array( - 'name' => 'Gitlab instance host name', - 'exampleValue' => 'gitlab.com', - 'defaultValue' => 'gitlab.com', - 'required' => true - ), - 'u' => array( - 'name' => 'User/Organization name', - 'exampleValue' => 'fdroid', - 'required' => true - ), - 'p' => array( - 'name' => 'Project name', - 'exampleValue' => 'fdroidclient', - 'required' => true - ) - - ), - 'Issue comments' => array( - 'i' => array( - 'name' => 'Issue number', - 'type' => 'number', - 'exampleValue' => '2099', - 'required' => true - ) - ), - 'Merge Request comments' => array( - 'i' => array( - 'name' => 'Merge Request number', - 'type' => 'number', - 'exampleValue' => '2099', - 'required' => true - ) - ) - ); - - public function getName(){ - $name = $this->getInput('h') . '/' . $this->getInput('u') . '/' . $this->getInput('p'); - switch ($this->queriedContext) { - case 'Issue comments': - $name .= ' Issue #' . $this->getInput('i'); - break; - case 'Merge Request comments': - $name .= ' MR !' . $this->getInput('i'); - break; - default: - return parent::getName(); - } - return $name; - } - - public function getURI() { - $host = $this->getInput('h') ?? 'gitlab.com'; - $uri = 'https://' . $host . '/' . $this->getInput('u') . '/' - . $this->getInput('p') . '/'; - switch ($this->queriedContext) { - case 'Issue comments': - $uri .= '-/issues'; - break; - case 'Merge Request comments': - $uri .= '-/merge_requests'; - break; - default: - return $uri; - } - $uri .= '/' . $this->getInput('i'); - return $uri; - } - - public function getIcon() { - return 'https://' . $this->getInput('h') . '/favicon.ico'; - } - - public function collectData() { - switch ($this->queriedContext) { - case 'Issue comments': - $this->items[] = $this->parseIssueDescription(); - break; - case 'Merge Request comments': - $this->items[] = $this->parseMergeRequestDescription(); - break; - default: - break; - } - - /* parse issue/MR comments */ - $comments_uri = $this->getURI() . '/discussions.json'; - $comments = getContents($comments_uri); - $comments = json_decode($comments, false); - - foreach ($comments as $value) { - foreach ($value->notes as $comment) { - $item = array(); - $item['uri'] = $comment->noteable_note_url; - $item['uid'] = $item['uri']; - - // TODO fix invalid timestamps (fdroid bot) - $item['timestamp'] = $comment->created_at ?? $comment->updated_at ?? $comment->last_edited_at; - $author = $comment->author ?? $comment->last_edited_by; - $item['author'] = '<img src="' . $author->avatar_url . '" width=24></img> <a href="https://' . - $this->getInput('h') . $author->path . '">' . $author->name . ' @' . $author->username . '</a>'; - - $content = ''; - if ($comment->system) { - $content = $comment->note_html; - if ($comment->type === 'StateNote') { - $content .= ' the issue'; - } elseif ($comment->type === null) { - // e.g. "added 900 commits\n800 from master\n175h4d - commit message\n..." - $content = str_get_html($comment->note_html)->find('p', 0); - } - } else { - // no switch-case to do strict comparison - if ($comment->type === null || $comment->type === 'DiscussionNote') { - $content = 'commented'; - } elseif ($comment->type === 'DiffNote') { - $content = 'commented on a thread'; - } else { - $content = $comment->note_html; - } - } - $item['title'] = $author->name . " $content"; - - $content = $this->fixImgSrc($comment->note_html); - $item['content'] = defaultLinkTo($content, 'https://' . $this->getInput('h') . '/'); - - $this->items[] = $item; - } - } - } - - private function parseIssueDescription() { - $description_uri = $this->getURI() . '.json'; - $description = getContents($description_uri); - $description = json_decode($description, false); - $description_html = getSimpleHtmlDomCached($this->getURI()); - - $item = array(); - $item['uri'] = $this->getURI(); - $item['uid'] = $item['uri']; - - $item['timestamp'] = $description->created_at ?? $description->updated_at; - - $item['author'] = $this->parseAuthor($description_html); - - $item['title'] = $description->title; - $item['content'] = markdownToHtml($description->description); - - return $item; - } - - private function parseMergeRequestDescription() { - $description_uri = $this->getURI() . '/cached_widget.json'; - $description = getContents($description_uri); - $description = json_decode($description, false); - $description_html = getSimpleHtmlDomCached($this->getURI()); - - $item = array(); - $item['uri'] = $this->getURI(); - $item['uid'] = $item['uri']; - - $item['timestamp'] = $description_html->find('.merge-request-details time', 0)->datetime; - - $item['author'] = $this->parseAuthor($description_html); - - $item['title'] = 'Merge Request ' . $description->title; - $item['content'] = markdownToHtml($description->description); - - return $item; - } - - private function fixImgSrc($html) { - if (is_string($html)) { - $html = str_get_html($html); - } - - foreach ($html->find('img') as $img) { - $img->src = $img->getAttribute('data-src'); - } - return $html; - } - - private function parseAuthor($description_html) { - $description_html = $this->fixImgSrc($description_html); - - $authors = $description_html->find('.issuable-meta a.author-link, .merge-request a.author-link'); - $editors = $description_html->find('.edited-text a.author-link'); - $author_str = implode(' ', $authors); - if ($editors) { - $author_str .= ', ' . implode(' ', $editors); - } - return defaultLinkTo($author_str, 'https://' . $this->getInput('h') . '/'); - } + +class GitlabIssueBridge extends BridgeAbstract +{ + const MAINTAINER = 'Mynacol'; + const NAME = 'Gitlab Issue/Merge Request'; + const URI = 'https://gitlab.com/'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns comments of an issue/MR of a gitlab project'; + + const PARAMETERS = [ + 'global' => [ + 'h' => [ + 'name' => 'Gitlab instance host name', + 'exampleValue' => 'gitlab.com', + 'defaultValue' => 'gitlab.com', + 'required' => true + ], + 'u' => [ + 'name' => 'User/Organization name', + 'exampleValue' => 'fdroid', + 'required' => true + ], + 'p' => [ + 'name' => 'Project name', + 'exampleValue' => 'fdroidclient', + 'required' => true + ] + + ], + 'Issue comments' => [ + 'i' => [ + 'name' => 'Issue number', + 'type' => 'number', + 'exampleValue' => '2099', + 'required' => true + ] + ], + 'Merge Request comments' => [ + 'i' => [ + 'name' => 'Merge Request number', + 'type' => 'number', + 'exampleValue' => '2099', + 'required' => true + ] + ] + ]; + + public function getName() + { + $name = $this->getInput('h') . '/' . $this->getInput('u') . '/' . $this->getInput('p'); + switch ($this->queriedContext) { + case 'Issue comments': + $name .= ' Issue #' . $this->getInput('i'); + break; + case 'Merge Request comments': + $name .= ' MR !' . $this->getInput('i'); + break; + default: + return parent::getName(); + } + return $name; + } + + public function getURI() + { + $host = $this->getInput('h') ?? 'gitlab.com'; + $uri = 'https://' . $host . '/' . $this->getInput('u') . '/' + . $this->getInput('p') . '/'; + switch ($this->queriedContext) { + case 'Issue comments': + $uri .= '-/issues'; + break; + case 'Merge Request comments': + $uri .= '-/merge_requests'; + break; + default: + return $uri; + } + $uri .= '/' . $this->getInput('i'); + return $uri; + } + + public function getIcon() + { + return 'https://' . $this->getInput('h') . '/favicon.ico'; + } + + public function collectData() + { + switch ($this->queriedContext) { + case 'Issue comments': + $this->items[] = $this->parseIssueDescription(); + break; + case 'Merge Request comments': + $this->items[] = $this->parseMergeRequestDescription(); + break; + default: + break; + } + + /* parse issue/MR comments */ + $comments_uri = $this->getURI() . '/discussions.json'; + $comments = getContents($comments_uri); + $comments = json_decode($comments, false); + + foreach ($comments as $value) { + foreach ($value->notes as $comment) { + $item = []; + $item['uri'] = $comment->noteable_note_url; + $item['uid'] = $item['uri']; + + // TODO fix invalid timestamps (fdroid bot) + $item['timestamp'] = $comment->created_at ?? $comment->updated_at ?? $comment->last_edited_at; + $author = $comment->author ?? $comment->last_edited_by; + $item['author'] = '<img src="' . $author->avatar_url . '" width=24></img> <a href="https://' . + $this->getInput('h') . $author->path . '">' . $author->name . ' @' . $author->username . '</a>'; + + $content = ''; + if ($comment->system) { + $content = $comment->note_html; + if ($comment->type === 'StateNote') { + $content .= ' the issue'; + } elseif ($comment->type === null) { + // e.g. "added 900 commits\n800 from master\n175h4d - commit message\n..." + $content = str_get_html($comment->note_html)->find('p', 0); + } + } else { + // no switch-case to do strict comparison + if ($comment->type === null || $comment->type === 'DiscussionNote') { + $content = 'commented'; + } elseif ($comment->type === 'DiffNote') { + $content = 'commented on a thread'; + } else { + $content = $comment->note_html; + } + } + $item['title'] = $author->name . " $content"; + + $content = $this->fixImgSrc($comment->note_html); + $item['content'] = defaultLinkTo($content, 'https://' . $this->getInput('h') . '/'); + + $this->items[] = $item; + } + } + } + + private function parseIssueDescription() + { + $description_uri = $this->getURI() . '.json'; + $description = getContents($description_uri); + $description = json_decode($description, false); + $description_html = getSimpleHtmlDomCached($this->getURI()); + + $item = []; + $item['uri'] = $this->getURI(); + $item['uid'] = $item['uri']; + + $item['timestamp'] = $description->created_at ?? $description->updated_at; + + $item['author'] = $this->parseAuthor($description_html); + + $item['title'] = $description->title; + $item['content'] = markdownToHtml($description->description); + + return $item; + } + + private function parseMergeRequestDescription() + { + $description_uri = $this->getURI() . '/cached_widget.json'; + $description = getContents($description_uri); + $description = json_decode($description, false); + $description_html = getSimpleHtmlDomCached($this->getURI()); + + $item = []; + $item['uri'] = $this->getURI(); + $item['uid'] = $item['uri']; + + $item['timestamp'] = $description_html->find('.merge-request-details time', 0)->datetime; + + $item['author'] = $this->parseAuthor($description_html); + + $item['title'] = 'Merge Request ' . $description->title; + $item['content'] = markdownToHtml($description->description); + + return $item; + } + + private function fixImgSrc($html) + { + if (is_string($html)) { + $html = str_get_html($html); + } + + foreach ($html->find('img') as $img) { + $img->src = $img->getAttribute('data-src'); + } + return $html; + } + + private function parseAuthor($description_html) + { + $description_html = $this->fixImgSrc($description_html); + + $authors = $description_html->find('.issuable-meta a.author-link, .merge-request a.author-link'); + $editors = $description_html->find('.edited-text a.author-link'); + $author_str = implode(' ', $authors); + if ($editors) { + $author_str .= ', ' . implode(' ', $editors); + } + return defaultLinkTo($author_str, 'https://' . $this->getInput('h') . '/'); + } } diff --git a/bridges/GizmodoBridge.php b/bridges/GizmodoBridge.php index 7dc3de8f..64e2fc8a 100644 --- a/bridges/GizmodoBridge.php +++ b/bridges/GizmodoBridge.php @@ -1,79 +1,83 @@ <?php -class GizmodoBridge extends FeedExpander { - const MAINTAINER = 'polopollo'; - const NAME = 'Gizmodo'; - const URI = 'https://gizmodo.com'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns the newest posts from Gizmodo.'; - - protected function parseItem($item) { - $item = parent::parseItem($item); - - $html = getSimpleHTMLDOMCached($item['uri']); - - $html = defaultLinkTo($html, $this->getURI()); - $this->stripTags($html); - $this->handleFigureTags($html); - $this->handleIframeTags($html); - - // Get header image - $image = $html->find('meta[property="og:image"]', 0)->content; - - $item['content'] = $html->find('div.js_post-content', 0)->innertext; - - // Get categories - $categories = explode(',', $html->find('meta[name="keywords"]', 0)->content); - $item['categories'] = array_map('trim', $categories); - - $item['enclosures'][] = $html->find('meta[property="og:image"]', 0)->content; - - return $item; - } - - public function collectData() { - $this->collectExpandableDatas(self::URI . '/rss', 20); - } - - private function stripTags($html) { - foreach ($html->find('aside') as $aside) { - $aside->outertext = ''; - } - - foreach ($html->find('div.ad-unit') as $div) { - $div->outertext = ''; - } - - foreach ($html->find('script') as $script) { - $script->outertext = ''; - } - } - - private function handleFigureTags($html) { - foreach ($html->find('figure') as $index => $figure) { - - if (isset($figure->attr['data-id'])) { - $id = $figure->attr['data-id']; - $format = $figure->attr['data-format']; - - } else { - $img = $figure->find('img', 0); - $id = $img->attr['data-chomp-id']; - $format = $img->attr['data-format']; - $figure->find('div.img-permalink-sub-wrapper', 0)->style = ''; - } - - $imageUrl = 'https://i.kinja-img.com/gawker-media/image/upload/' . $id . '.' . $format; - - $figure->find('span', 0)->outertext = <<<EOD +class GizmodoBridge extends FeedExpander +{ + const MAINTAINER = 'polopollo'; + const NAME = 'Gizmodo'; + const URI = 'https://gizmodo.com'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns the newest posts from Gizmodo.'; + + protected function parseItem($item) + { + $item = parent::parseItem($item); + + $html = getSimpleHTMLDOMCached($item['uri']); + + $html = defaultLinkTo($html, $this->getURI()); + $this->stripTags($html); + $this->handleFigureTags($html); + $this->handleIframeTags($html); + + // Get header image + $image = $html->find('meta[property="og:image"]', 0)->content; + + $item['content'] = $html->find('div.js_post-content', 0)->innertext; + + // Get categories + $categories = explode(',', $html->find('meta[name="keywords"]', 0)->content); + $item['categories'] = array_map('trim', $categories); + + $item['enclosures'][] = $html->find('meta[property="og:image"]', 0)->content; + + return $item; + } + + public function collectData() + { + $this->collectExpandableDatas(self::URI . '/rss', 20); + } + + private function stripTags($html) + { + foreach ($html->find('aside') as $aside) { + $aside->outertext = ''; + } + + foreach ($html->find('div.ad-unit') as $div) { + $div->outertext = ''; + } + + foreach ($html->find('script') as $script) { + $script->outertext = ''; + } + } + + private function handleFigureTags($html) + { + foreach ($html->find('figure') as $index => $figure) { + if (isset($figure->attr['data-id'])) { + $id = $figure->attr['data-id']; + $format = $figure->attr['data-format']; + } else { + $img = $figure->find('img', 0); + $id = $img->attr['data-chomp-id']; + $format = $img->attr['data-format']; + $figure->find('div.img-permalink-sub-wrapper', 0)->style = ''; + } + + $imageUrl = 'https://i.kinja-img.com/gawker-media/image/upload/' . $id . '.' . $format; + + $figure->find('span', 0)->outertext = <<<EOD <img src="{$imageUrl}"> EOD; - } - } - - private function handleIframeTags($html) { - foreach($html->find('iframe') as $iframe) { - $iframe->src = urljoin($this->getURI(), $iframe->src); - } - } + } + } + + private function handleIframeTags($html) + { + foreach ($html->find('iframe') as $iframe) { + $iframe->src = urljoin($this->getURI(), $iframe->src); + } + } } diff --git a/bridges/GlassdoorBridge.php b/bridges/GlassdoorBridge.php index 3358c74b..8c53cfa9 100644 --- a/bridges/GlassdoorBridge.php +++ b/bridges/GlassdoorBridge.php @@ -1,187 +1,197 @@ <?php -class GlassdoorBridge extends BridgeAbstract { - - // Contexts - const CONTEXT_BLOG = 'Blogs'; - const CONTEXT_REVIEW = 'Company Reviews'; - const CONTEXT_GLOBAL = 'global'; - - // Global context parameters - const PARAM_LIMIT = 'limit'; - - // Blog context parameters - const PARAM_BLOG_TYPE = 'blog_type'; - const PARAM_BLOG_FULL = 'full_article'; - - const BLOG_TYPE_HOME = 'Home'; - const BLOG_TYPE_COMPANIES_HIRING = 'Companies Hiring'; - const BLOG_TYPE_CAREER_ADVICE = 'Career Advice'; - const BLOG_TYPE_INTERVIEWS = 'Interviews'; - - // Review context parameters - const PARAM_REVIEW_COMPANY = 'company'; - - const MAINTAINER = 'logmanoriginal'; - const NAME = 'Glassdoor Bridge'; - const URI = 'https://www.glassdoor.com/'; - const DESCRIPTION = 'Returns feeds for blog posts and company reviews'; - const CACHE_TIMEOUT = 86400; // 24 hours - - const PARAMETERS = array( - self::CONTEXT_BLOG => array( - self::PARAM_BLOG_TYPE => array( - 'name' => 'Blog type', - 'type' => 'list', - 'title' => 'Select the blog you want to follow', - 'values' => array( - self::BLOG_TYPE_HOME => 'blog/', - self::BLOG_TYPE_COMPANIES_HIRING => 'blog/companies-hiring/', - self::BLOG_TYPE_CAREER_ADVICE => 'blog/career-advice/', - self::BLOG_TYPE_INTERVIEWS => 'blog/interviews/', - ) - ), - self::PARAM_BLOG_FULL => array( - 'name' => 'Full article', - 'type' => 'checkbox', - 'title' => 'Enable to return the full article for each post' - ), - ), - self::CONTEXT_REVIEW => array( - self::PARAM_REVIEW_COMPANY => array( - 'name' => 'Company URL', - 'type' => 'text', - 'required' => true, - 'title' => 'Paste the company review page URL here!', - 'exampleValue' => 'https://www.glassdoor.com/Reviews/GitHub-Reviews-E671945.htm' - ) - ), - self::CONTEXT_GLOBAL => array( - self::PARAM_LIMIT => array( - 'name' => 'Limit', - 'type' => 'number', - 'defaultValue' => -1, - 'title' => 'Specifies the maximum number of items to return (default: All)' - ) - ) - ); - - public function getURI() { - switch($this->queriedContext) { - case self::CONTEXT_BLOG: - return self::URI . $this->getInput(self::PARAM_BLOG_TYPE); - case self::CONTEXT_REVIEW: - return $this->filterCompanyURI($this->getInput(self::PARAM_REVIEW_COMPANY)); - } - - return parent::getURI(); - } - - public function collectData() { - $url = $this->getURI(); - $html = getSimpleHTMLDOM($url); - $html = defaultLinkTo($html, $url); - $limit = $this->getInput(self::PARAM_LIMIT); - - switch($this->queriedContext) { - case self::CONTEXT_BLOG: - $this->collectBlogData($html, $limit); - break; - case self::CONTEXT_REVIEW: - $this->collectReviewData($html, $limit); - break; - } - } - - private function collectBlogData($html, $limit) { - $posts = $html->find('div.post') - or returnServerError('Unable to find blog posts!'); - - foreach($posts as $post) { - $item = []; - - $item['uri'] = $post->find('a', 0)->href; - $item['title'] = $post->find('h3', 0)->plaintext; - $item['content'] = $post->find('p', 0)->plaintext; - $item['author'] = $post->find('p', -2)->plaintext; - $item['timestamp'] = strtotime($post->find('p', -1)->plaintext); - - // TODO: fetch entire blog post content - $this->items[] = $item; - - if ($limit > 0 && count($this->items) >= $limit) { - return; - } - } - } - - private function collectReviewData($html, $limit) { - $reviews = $html->find('#ReviewsFeed li[id^="empReview]') - or returnServerError('Unable to find reviews!'); - - foreach($reviews as $review) { - $item = []; - - $item['uri'] = $review->find('a.reviewLink', 0)->href; - - // Not all reviews have a title - $item['title'] = $review->find('h2', 0)->plaintext ?? 'Glassdoor review'; - - [$date, $author] = explode('-', $review->find('span.authorInfo', 0)->plaintext); - - $item['author'] = trim($author); - - $createdAt = DateTimeImmutable::createFromFormat('F m, Y', trim($date)); - if ($createdAt) { - $item['timestamp'] = $createdAt->getTimestamp(); - } - - $item['content'] = $review->find('.px-std', 2)->text(); - - $this->items[] = $item; - - if($limit > 0 && count($this->items) >= $limit) { - return; - } - } - } - - private function filterCompanyURI($uri) { - /* Make sure the URI is a valid review page. Unfortunately there is no - * simple way to determine if the URI is valid, because of automagic - * redirection and strange naming conventions. - */ - if(!filter_var($uri, - FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) { - returnClientError('The specified URL is invalid!'); - } - - $uri = filter_var($uri, FILTER_SANITIZE_URL); - $path = parse_url($uri, PHP_URL_PATH); - $parts = explode('/', $path); - - $allowed_strings = array( - 'de-DE' => 'Bewertungen', - 'en-AU' => 'Reviews', - 'nl-BE' => 'Reviews', - 'fr-BE' => 'Avis', - 'en-CA' => 'Reviews', - 'fr-CA' => 'Avis', - 'fr-FR' => 'Avis', - 'en-IN' => 'Reviews', - 'en-IE' => 'Reviews', - 'nl-NL' => 'Reviews', - 'de-AT' => 'Bewertungen', - 'de-CH' => 'Bewertungen', - 'fr-CH' => 'Avis', - 'en-GB' => 'Reviews', - 'en' => 'Reviews' - ); - - if(!in_array($parts[1], $allowed_strings)) { - returnClientError('Please specify a URL pointing to the companies review page!'); - } - - return $uri; - } +class GlassdoorBridge extends BridgeAbstract +{ + // Contexts + const CONTEXT_BLOG = 'Blogs'; + const CONTEXT_REVIEW = 'Company Reviews'; + const CONTEXT_GLOBAL = 'global'; + + // Global context parameters + const PARAM_LIMIT = 'limit'; + + // Blog context parameters + const PARAM_BLOG_TYPE = 'blog_type'; + const PARAM_BLOG_FULL = 'full_article'; + + const BLOG_TYPE_HOME = 'Home'; + const BLOG_TYPE_COMPANIES_HIRING = 'Companies Hiring'; + const BLOG_TYPE_CAREER_ADVICE = 'Career Advice'; + const BLOG_TYPE_INTERVIEWS = 'Interviews'; + + // Review context parameters + const PARAM_REVIEW_COMPANY = 'company'; + + const MAINTAINER = 'logmanoriginal'; + const NAME = 'Glassdoor Bridge'; + const URI = 'https://www.glassdoor.com/'; + const DESCRIPTION = 'Returns feeds for blog posts and company reviews'; + const CACHE_TIMEOUT = 86400; // 24 hours + + const PARAMETERS = [ + self::CONTEXT_BLOG => [ + self::PARAM_BLOG_TYPE => [ + 'name' => 'Blog type', + 'type' => 'list', + 'title' => 'Select the blog you want to follow', + 'values' => [ + self::BLOG_TYPE_HOME => 'blog/', + self::BLOG_TYPE_COMPANIES_HIRING => 'blog/companies-hiring/', + self::BLOG_TYPE_CAREER_ADVICE => 'blog/career-advice/', + self::BLOG_TYPE_INTERVIEWS => 'blog/interviews/', + ] + ], + self::PARAM_BLOG_FULL => [ + 'name' => 'Full article', + 'type' => 'checkbox', + 'title' => 'Enable to return the full article for each post' + ], + ], + self::CONTEXT_REVIEW => [ + self::PARAM_REVIEW_COMPANY => [ + 'name' => 'Company URL', + 'type' => 'text', + 'required' => true, + 'title' => 'Paste the company review page URL here!', + 'exampleValue' => 'https://www.glassdoor.com/Reviews/GitHub-Reviews-E671945.htm' + ] + ], + self::CONTEXT_GLOBAL => [ + self::PARAM_LIMIT => [ + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => -1, + 'title' => 'Specifies the maximum number of items to return (default: All)' + ] + ] + ]; + + public function getURI() + { + switch ($this->queriedContext) { + case self::CONTEXT_BLOG: + return self::URI . $this->getInput(self::PARAM_BLOG_TYPE); + case self::CONTEXT_REVIEW: + return $this->filterCompanyURI($this->getInput(self::PARAM_REVIEW_COMPANY)); + } + + return parent::getURI(); + } + + public function collectData() + { + $url = $this->getURI(); + $html = getSimpleHTMLDOM($url); + $html = defaultLinkTo($html, $url); + $limit = $this->getInput(self::PARAM_LIMIT); + + switch ($this->queriedContext) { + case self::CONTEXT_BLOG: + $this->collectBlogData($html, $limit); + break; + case self::CONTEXT_REVIEW: + $this->collectReviewData($html, $limit); + break; + } + } + + private function collectBlogData($html, $limit) + { + $posts = $html->find('div.post') + or returnServerError('Unable to find blog posts!'); + + foreach ($posts as $post) { + $item = []; + + $item['uri'] = $post->find('a', 0)->href; + $item['title'] = $post->find('h3', 0)->plaintext; + $item['content'] = $post->find('p', 0)->plaintext; + $item['author'] = $post->find('p', -2)->plaintext; + $item['timestamp'] = strtotime($post->find('p', -1)->plaintext); + + // TODO: fetch entire blog post content + $this->items[] = $item; + + if ($limit > 0 && count($this->items) >= $limit) { + return; + } + } + } + + private function collectReviewData($html, $limit) + { + $reviews = $html->find('#ReviewsFeed li[id^="empReview]') + or returnServerError('Unable to find reviews!'); + + foreach ($reviews as $review) { + $item = []; + + $item['uri'] = $review->find('a.reviewLink', 0)->href; + + // Not all reviews have a title + $item['title'] = $review->find('h2', 0)->plaintext ?? 'Glassdoor review'; + + [$date, $author] = explode('-', $review->find('span.authorInfo', 0)->plaintext); + + $item['author'] = trim($author); + + $createdAt = DateTimeImmutable::createFromFormat('F m, Y', trim($date)); + if ($createdAt) { + $item['timestamp'] = $createdAt->getTimestamp(); + } + + $item['content'] = $review->find('.px-std', 2)->text(); + + $this->items[] = $item; + + if ($limit > 0 && count($this->items) >= $limit) { + return; + } + } + } + + private function filterCompanyURI($uri) + { + /* Make sure the URI is a valid review page. Unfortunately there is no + * simple way to determine if the URI is valid, because of automagic + * redirection and strange naming conventions. + */ + if ( + !filter_var( + $uri, + FILTER_VALIDATE_URL, + FILTER_FLAG_PATH_REQUIRED + ) + ) { + returnClientError('The specified URL is invalid!'); + } + + $uri = filter_var($uri, FILTER_SANITIZE_URL); + $path = parse_url($uri, PHP_URL_PATH); + $parts = explode('/', $path); + + $allowed_strings = [ + 'de-DE' => 'Bewertungen', + 'en-AU' => 'Reviews', + 'nl-BE' => 'Reviews', + 'fr-BE' => 'Avis', + 'en-CA' => 'Reviews', + 'fr-CA' => 'Avis', + 'fr-FR' => 'Avis', + 'en-IN' => 'Reviews', + 'en-IE' => 'Reviews', + 'nl-NL' => 'Reviews', + 'de-AT' => 'Bewertungen', + 'de-CH' => 'Bewertungen', + 'fr-CH' => 'Avis', + 'en-GB' => 'Reviews', + 'en' => 'Reviews' + ]; + + if (!in_array($parts[1], $allowed_strings)) { + returnClientError('Please specify a URL pointing to the companies review page!'); + } + + return $uri; + } } diff --git a/bridges/GlowficBridge.php b/bridges/GlowficBridge.php index a3a85ef4..b51ead8d 100644 --- a/bridges/GlowficBridge.php +++ b/bridges/GlowficBridge.php @@ -1,90 +1,100 @@ <?php -class GlowficBridge extends BridgeAbstract { - const MAINTAINER = 'l1n'; - const NAME = 'Glowfic Bridge'; - const URI = 'https://www.glowfic.com'; - const CACHE_TIMEOUT = 3600; // 1 hour - const DESCRIPTION = 'Returns the latest replies on a glowfic post.'; - const PARAMETERS = array( - 'global' => array(), - 'Thread' => array( - 'post_id' => array( - 'name' => 'Post ID', - 'title' => 'https://www.glowfic.com/posts/POST ID', - 'required' => true, - 'exampleValue' => '2756', - 'type' => 'number' - ), - 'start_page' => array( - 'name' => 'Start Page', - 'title' => 'To start from an offset page', - 'type' => 'number' - ) - ) - ); - public function collectData() { - $url = $this->getAPIURI(); - $metadata = get_headers( $url . '/replies', true ) or returnClientError('Post did not return reply headers.'); - $metadata['Last-Page'] = ceil( $metadata['Total'] / $metadata['Per-Page'] ); - if(!is_null($this->getInput('start_page')) && - $this->getInput('start_page') < 1 && $metadata['Last-Page'] - $this->getInput('start_page') > 0) { - $first_page = $metadata['Last-Page'] - $this->getInput('start_page'); - } else if(!is_null($this->getInput('start_page')) && $this->getInput('start_page') <= $metadata['Last-Page']) { - $first_page = $this->getInput('start_page'); - } else { - $first_page = 1; - } - for ($page_offset = $first_page; $page_offset <= $metadata['Last-Page']; $page_offset++) { - $jsonContents = getContents($url . '/replies?page=' . $page_offset ) or - returnClientError('Could not retrieve replies for page ' . $page_offset . '.'); - $replies = json_decode($jsonContents); - foreach ($replies as $reply) { - $item = array(); +class GlowficBridge extends BridgeAbstract +{ + const MAINTAINER = 'l1n'; + const NAME = 'Glowfic Bridge'; + const URI = 'https://www.glowfic.com'; + const CACHE_TIMEOUT = 3600; // 1 hour + const DESCRIPTION = 'Returns the latest replies on a glowfic post.'; + const PARAMETERS = [ + 'global' => [], + 'Thread' => [ + 'post_id' => [ + 'name' => 'Post ID', + 'title' => 'https://www.glowfic.com/posts/POST ID', + 'required' => true, + 'exampleValue' => '2756', + 'type' => 'number' + ], + 'start_page' => [ + 'name' => 'Start Page', + 'title' => 'To start from an offset page', + 'type' => 'number' + ] + ] + ]; - $item['content'] = $reply->{'content'}; - $item['uri'] = $this->getURI() . '?page=' . $page_offset . '#reply-' . $reply->{'id'}; - if ($reply->{'icon'}) { - $item['enclosures'] = array($reply->{'icon'}->{'url'}); - } - $item['author'] = $reply->{'character'}->{'screenname'} . ' (' . $reply->{'character'}->{'name'} . ')'; - $item['timestamp'] = date('r', strtotime($reply->{'created_at'})); - $item['title'] = 'Tag by ' . $reply->{'user'}->{'username'} . ' updated at ' . $reply->{'updated_at'}; - $this->items[] = $item; - } - } - } + public function collectData() + { + $url = $this->getAPIURI(); + $metadata = get_headers($url . '/replies', true) or returnClientError('Post did not return reply headers.'); + $metadata['Last-Page'] = ceil($metadata['Total'] / $metadata['Per-Page']); + if ( + !is_null($this->getInput('start_page')) && + $this->getInput('start_page') < 1 && $metadata['Last-Page'] - $this->getInput('start_page') > 0 + ) { + $first_page = $metadata['Last-Page'] - $this->getInput('start_page'); + } elseif (!is_null($this->getInput('start_page')) && $this->getInput('start_page') <= $metadata['Last-Page']) { + $first_page = $this->getInput('start_page'); + } else { + $first_page = 1; + } + for ($page_offset = $first_page; $page_offset <= $metadata['Last-Page']; $page_offset++) { + $jsonContents = getContents($url . '/replies?page=' . $page_offset) or + returnClientError('Could not retrieve replies for page ' . $page_offset . '.'); + $replies = json_decode($jsonContents); + foreach ($replies as $reply) { + $item = []; - private function getAPIURI() { - $url = parent::getURI() . '/api/v1/posts/' . $this->getInput('post_id'); - return $url; - } + $item['content'] = $reply->{'content'}; + $item['uri'] = $this->getURI() . '?page=' . $page_offset . '#reply-' . $reply->{'id'}; + if ($reply->{'icon'}) { + $item['enclosures'] = [$reply->{'icon'}->{'url'}]; + } + $item['author'] = $reply->{'character'}->{'screenname'} . ' (' . $reply->{'character'}->{'name'} . ')'; + $item['timestamp'] = date('r', strtotime($reply->{'created_at'})); + $item['title'] = 'Tag by ' . $reply->{'user'}->{'username'} . ' updated at ' . $reply->{'updated_at'}; + $this->items[] = $item; + } + } + } - public function getURI() { - $url = parent::getURI() . '/posts/' . $this->getInput('post_id'); - return $url; - } + private function getAPIURI() + { + $url = parent::getURI() . '/api/v1/posts/' . $this->getInput('post_id'); + return $url; + } - private function getPost() { - $url = $this->getAPIURI(); - $jsonPost = getContents( $url ) or returnClientError('Could not retrieve post metadata.'); - $post = json_decode($jsonPost); - return $post; - } + public function getURI() + { + $url = parent::getURI() . '/posts/' . $this->getInput('post_id'); + return $url; + } - public function getName(){ - if(!is_null($this->getInput('post_id'))) { - $post = $this->getPost(); - return $post->{'subject'} . ' - ' . parent::getName(); - } - return parent::getName(); - } + private function getPost() + { + $url = $this->getAPIURI(); + $jsonPost = getContents($url) or returnClientError('Could not retrieve post metadata.'); + $post = json_decode($jsonPost); + return $post; + } - public function getDescription(){ - if(!is_null($this->getInput('post_id'))) { - $post = $this->getPost(); - return $post->{'content'}; - } - return parent::getName(); - } + public function getName() + { + if (!is_null($this->getInput('post_id'))) { + $post = $this->getPost(); + return $post->{'subject'} . ' - ' . parent::getName(); + } + return parent::getName(); + } + + public function getDescription() + { + if (!is_null($this->getInput('post_id'))) { + $post = $this->getPost(); + return $post->{'content'}; + } + return parent::getName(); + } } diff --git a/bridges/GoComicsBridge.php b/bridges/GoComicsBridge.php index 9bca83e2..586e2a0d 100644 --- a/bridges/GoComicsBridge.php +++ b/bridges/GoComicsBridge.php @@ -1,60 +1,63 @@ <?php -class GoComicsBridge extends BridgeAbstract { - - const MAINTAINER = 'sky'; - const NAME = 'GoComics Unofficial RSS'; - const URI = 'https://www.gocomics.com/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'The Unofficial GoComics RSS'; - const PARAMETERS = array( array( - 'comicname' => array( - 'name' => 'comicname', - 'type' => 'text', - 'exampleValue' => 'heartofthecity', - 'required' => true - ) - )); - - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); - - //Get info from first page - $author = preg_replace('/By /', '', $html->find('.media-subheading', 0)->plaintext); - - $link = self::URI . $html->find('.gc-deck--cta-0', 0)->find('a', 0)->href; - for($i = 0; $i < 5; $i++) { - - $item = array(); - - $page = getSimpleHTMLDOM($link); - $imagelink = $page->find('.comic.container', 0)->getAttribute('data-image'); - $date = explode('/', $link); - - $item['id'] = $imagelink; - $item['uri'] = $link; - $item['author'] = $author; - $item['title'] = 'GoComics ' . $this->getInput('comicname'); - $item['timestamp'] = DateTime::createFromFormat('Ymd', $date[5] . $date[6] . $date[7])->getTimestamp(); - $item['content'] = '<img src="' . $imagelink . '" />'; - - $link = self::URI . $page->find('.js-previous-comic', 0)->href; - $this->items[] = $item; - } - } - - public function getURI(){ - if(!is_null($this->getInput('comicname'))) { - return self::URI . urlencode($this->getInput('comicname')); - } - - return parent::getURI(); - } - - public function getName(){ - if(!is_null($this->getInput('comicname'))) { - return $this->getInput('comicname') . ' - GoComics'; - } - - return parent::getName(); - } + +class GoComicsBridge extends BridgeAbstract +{ + const MAINTAINER = 'sky'; + const NAME = 'GoComics Unofficial RSS'; + const URI = 'https://www.gocomics.com/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'The Unofficial GoComics RSS'; + const PARAMETERS = [ [ + 'comicname' => [ + 'name' => 'comicname', + 'type' => 'text', + 'exampleValue' => 'heartofthecity', + 'required' => true + ] + ]]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + //Get info from first page + $author = preg_replace('/By /', '', $html->find('.media-subheading', 0)->plaintext); + + $link = self::URI . $html->find('.gc-deck--cta-0', 0)->find('a', 0)->href; + for ($i = 0; $i < 5; $i++) { + $item = []; + + $page = getSimpleHTMLDOM($link); + $imagelink = $page->find('.comic.container', 0)->getAttribute('data-image'); + $date = explode('/', $link); + + $item['id'] = $imagelink; + $item['uri'] = $link; + $item['author'] = $author; + $item['title'] = 'GoComics ' . $this->getInput('comicname'); + $item['timestamp'] = DateTime::createFromFormat('Ymd', $date[5] . $date[6] . $date[7])->getTimestamp(); + $item['content'] = '<img src="' . $imagelink . '" />'; + + $link = self::URI . $page->find('.js-previous-comic', 0)->href; + $this->items[] = $item; + } + } + + public function getURI() + { + if (!is_null($this->getInput('comicname'))) { + return self::URI . urlencode($this->getInput('comicname')); + } + + return parent::getURI(); + } + + public function getName() + { + if (!is_null($this->getInput('comicname'))) { + return $this->getInput('comicname') . ' - GoComics'; + } + + return parent::getName(); + } } diff --git a/bridges/GogsBridge.php b/bridges/GogsBridge.php index c90c8c57..685e5ba2 100644 --- a/bridges/GogsBridge.php +++ b/bridges/GogsBridge.php @@ -1,205 +1,216 @@ <?php -class GogsBridge extends BridgeAbstract { - - const NAME = 'Gogs'; - const URI = 'https://gogs.io'; - const DESCRIPTION = 'Returns the latest issues, commits or releases'; - const MAINTAINER = 'logmanoriginal'; - const CACHE_TIMEOUT = 300; // 5 minutes - - const PARAMETERS = array( - 'global' => array( - 'host' => array( - 'name' => 'Host', - 'exampleValue' => 'https://notabug.org', - 'required' => true, - 'title' => 'Host name with its protocol, without trailing slash', - ), - 'user' => array( - 'name' => 'Username', - 'exampleValue' => 'PDModdingCommunity', - 'required' => true, - 'title' => 'User name as it appears in the URL', - ), - 'project' => array( - 'name' => 'Project name', - 'exampleValue' => 'PD-Loader', - 'required' => true, - 'title' => 'Project name as it appears in the URL', - ), - ), - 'Commits' => array( - 'branch' => array( - 'name' => 'Branch name', - 'defaultValue' => 'master', - 'required' => true, - 'title' => 'Branch name as it appears in the URL', - ), - ), - 'Issues' => array( - 'include_description' => array( - 'name' => 'Include issue description', - 'type' => 'checkbox', - 'title' => 'Activate to include the issue description', - ), - ), - 'Single issue' => array( - 'issue' => array( - 'name' => 'Issue number', - 'type' => 'number', - 'exampleValue' => 100, - 'required' => true, - 'title' => 'Issue number from the issues list', - ), - ), - 'Releases' => array(), - ); - - private $title = ''; - - /** - * Note: detectParamters doesn't make sense for this bridge because there is - * no "single" host for this service. Anyone can host it. - */ - - public function getURI() { - switch($this->queriedContext) { - case 'Commits': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/commits/' . $this->getInput('branch'); - - case 'Issues': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/issues/'; - - case 'Single issue': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/issues/' . $this->getInput('issue'); - - case 'Releases': - return $this->getInput('host') - . '/' . $this->getInput('user') - . '/' . $this->getInput('project') - . '/releases/'; - - default: return parent::getURI(); - } - } - - public function getName() { - switch($this->queriedContext) { - case 'Commits': - case 'Issues': - case 'Releases': return $this->title . ' ' . $this->queriedContext; - case 'Single issue': return $this->title . ' Issue ' . $this->getInput('issue'); - default: return parent::getName(); - } - } - - public function getIcon() { - return 'https://gogs.io/img/favicon.ico'; - } - - public function collectData() { - - $html = getSimpleHTMLDOM($this->getURI()); - - $html = defaultLinkTo($html, $this->getURI()); - - $this->title = $html->find('[property="og:title"]', 0)->content; - - switch($this->queriedContext) { - case 'Commits': - $this->collectCommitsData($html); - break; - case 'Issues': - $this->collectIssuesData($html); - break; - case 'Single issue': - $this->collectSingleIssueData($html); - break; - case 'Releases': - $this->collectReleasesData($html); - break; - } - - } - - protected function collectCommitsData($html) { - $commits = $html->find('#commits-table tbody tr') - or returnServerError('Unable to find commits'); - - foreach($commits as $commit) { - $this->items[] = array( - 'uri' => $commit->find('a.sha', 0)->href, - 'title' => $commit->find('.message span', 0)->plaintext, - 'author' => $commit->find('.author', 0)->plaintext, - 'timestamp' => $commit->find('.time-since', 0)->title, - 'uid' => $commit->find('.sha', 0)->plaintext, - ); - } - } - - protected function collectIssuesData($html) { - $issues = $html->find('.issue.list li') - or returnServerError('Unable to find issues'); - - foreach($issues as $issue) { - $uri = $issue->find('a', 0)->href; - - $item = array( - 'uri' => $uri, - 'title' => $issue->find('.label', 0)->plaintext . ' | ' . $issue->find('a.title', 0)->plaintext, - 'author' => $issue->find('.desc a', 0)->plaintext, - 'timestamp' => $issue->find('.time-since', 0)->title, - 'uid' => $issue->find('.label', 0)->plaintext, - ); - - if($this->getInput('include_description')) { - $issue_html = getSimpleHTMLDOMCached($uri, 3600) - or returnServerError('Unable to load issue description'); - - $issue_html = defaultLinkTo($issue_html, $uri); - - $item['content'] = $issue_html->find('.comment .markdown', 0); - } - - $this->items[] = $item; - } - } - - protected function collectSingleIssueData($html) { - $comments = $html->find('.comments .comment') - or returnServerError('Unable to find comments'); - - foreach($comments as $comment) { - $this->items[] = array( - 'uri' => $comment->find('a[href*="#issue"]', 0)->href, - 'title' => $comment->find('span', 0)->plaintext, - 'author' => $comment->find('.content a', 0)->plaintext, - 'timestamp' => $comment->find('.time-since', 0)->title, - 'content' => $comment->find('.markdown', 0), - ); - } - - $this->items = array_reverse($this->items); - } - - protected function collectReleasesData($html) { - $releases = $html->find('#release-list li') - or returnServerError('Unable to find releases'); - - foreach($releases as $release) { - $this->items[] = array( - 'uri' => $release->find('a', 0)->href, - 'title' => 'Release ' . $release->find('h4', 0)->plaintext, - ); - } - } + +class GogsBridge extends BridgeAbstract +{ + const NAME = 'Gogs'; + const URI = 'https://gogs.io'; + const DESCRIPTION = 'Returns the latest issues, commits or releases'; + const MAINTAINER = 'logmanoriginal'; + const CACHE_TIMEOUT = 300; // 5 minutes + + const PARAMETERS = [ + 'global' => [ + 'host' => [ + 'name' => 'Host', + 'exampleValue' => 'https://notabug.org', + 'required' => true, + 'title' => 'Host name with its protocol, without trailing slash', + ], + 'user' => [ + 'name' => 'Username', + 'exampleValue' => 'PDModdingCommunity', + 'required' => true, + 'title' => 'User name as it appears in the URL', + ], + 'project' => [ + 'name' => 'Project name', + 'exampleValue' => 'PD-Loader', + 'required' => true, + 'title' => 'Project name as it appears in the URL', + ], + ], + 'Commits' => [ + 'branch' => [ + 'name' => 'Branch name', + 'defaultValue' => 'master', + 'required' => true, + 'title' => 'Branch name as it appears in the URL', + ], + ], + 'Issues' => [ + 'include_description' => [ + 'name' => 'Include issue description', + 'type' => 'checkbox', + 'title' => 'Activate to include the issue description', + ], + ], + 'Single issue' => [ + 'issue' => [ + 'name' => 'Issue number', + 'type' => 'number', + 'exampleValue' => 100, + 'required' => true, + 'title' => 'Issue number from the issues list', + ], + ], + 'Releases' => [], + ]; + + private $title = ''; + + /** + * Note: detectParamters doesn't make sense for this bridge because there is + * no "single" host for this service. Anyone can host it. + */ + + public function getURI() + { + switch ($this->queriedContext) { + case 'Commits': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/commits/' . $this->getInput('branch'); + + case 'Issues': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/issues/'; + + case 'Single issue': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/issues/' . $this->getInput('issue'); + + case 'Releases': + return $this->getInput('host') + . '/' . $this->getInput('user') + . '/' . $this->getInput('project') + . '/releases/'; + + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Commits': + case 'Issues': + case 'Releases': + return $this->title . ' ' . $this->queriedContext; + case 'Single issue': + return $this->title . ' Issue ' . $this->getInput('issue'); + default: + return parent::getName(); + } + } + + public function getIcon() + { + return 'https://gogs.io/img/favicon.ico'; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $html = defaultLinkTo($html, $this->getURI()); + + $this->title = $html->find('[property="og:title"]', 0)->content; + + switch ($this->queriedContext) { + case 'Commits': + $this->collectCommitsData($html); + break; + case 'Issues': + $this->collectIssuesData($html); + break; + case 'Single issue': + $this->collectSingleIssueData($html); + break; + case 'Releases': + $this->collectReleasesData($html); + break; + } + } + + protected function collectCommitsData($html) + { + $commits = $html->find('#commits-table tbody tr') + or returnServerError('Unable to find commits'); + + foreach ($commits as $commit) { + $this->items[] = [ + 'uri' => $commit->find('a.sha', 0)->href, + 'title' => $commit->find('.message span', 0)->plaintext, + 'author' => $commit->find('.author', 0)->plaintext, + 'timestamp' => $commit->find('.time-since', 0)->title, + 'uid' => $commit->find('.sha', 0)->plaintext, + ]; + } + } + + protected function collectIssuesData($html) + { + $issues = $html->find('.issue.list li') + or returnServerError('Unable to find issues'); + + foreach ($issues as $issue) { + $uri = $issue->find('a', 0)->href; + + $item = [ + 'uri' => $uri, + 'title' => $issue->find('.label', 0)->plaintext . ' | ' . $issue->find('a.title', 0)->plaintext, + 'author' => $issue->find('.desc a', 0)->plaintext, + 'timestamp' => $issue->find('.time-since', 0)->title, + 'uid' => $issue->find('.label', 0)->plaintext, + ]; + + if ($this->getInput('include_description')) { + $issue_html = getSimpleHTMLDOMCached($uri, 3600) + or returnServerError('Unable to load issue description'); + + $issue_html = defaultLinkTo($issue_html, $uri); + + $item['content'] = $issue_html->find('.comment .markdown', 0); + } + + $this->items[] = $item; + } + } + + protected function collectSingleIssueData($html) + { + $comments = $html->find('.comments .comment') + or returnServerError('Unable to find comments'); + + foreach ($comments as $comment) { + $this->items[] = [ + 'uri' => $comment->find('a[href*="#issue"]', 0)->href, + 'title' => $comment->find('span', 0)->plaintext, + 'author' => $comment->find('.content a', 0)->plaintext, + 'timestamp' => $comment->find('.time-since', 0)->title, + 'content' => $comment->find('.markdown', 0), + ]; + } + + $this->items = array_reverse($this->items); + } + + protected function collectReleasesData($html) + { + $releases = $html->find('#release-list li') + or returnServerError('Unable to find releases'); + + foreach ($releases as $release) { + $this->items[] = [ + 'uri' => $release->find('a', 0)->href, + 'title' => 'Release ' . $release->find('h4', 0)->plaintext, + ]; + } + } } diff --git a/bridges/GolemBridge.php b/bridges/GolemBridge.php index 4bce9e4e..dd9b196e 100644 --- a/bridges/GolemBridge.php +++ b/bridges/GolemBridge.php @@ -1,125 +1,131 @@ <?php -class GolemBridge extends FeedExpander { - const MAINTAINER = 'Mynacol'; - const NAME = 'Golem Bridge'; - const URI = 'https://www.golem.de/'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns the full articles instead of only the intro'; - const PARAMETERS = array(array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'Alle News' - => 'https://rss.golem.de/rss.php?feed=ATOM1.0', - 'Audio/Video' - => 'https://rss.golem.de/rss.php?ms=audio-video&feed=ATOM1.0', - 'Auto' - => 'https://rss.golem.de/rss.php?ms=auto&feed=ATOM1.0', - 'Foto' - => 'https://rss.golem.de/rss.php?ms=foto&feed=ATOM1.0', - 'Games' - => 'https://rss.golem.de/rss.php?ms=games&feed=ATOM1.0', - 'Handy' - => 'https://rss.golem.de/rss.php?ms=handy&feed=ATOM1.0', - 'Internet' - => 'https://rss.golem.de/rss.php?ms=internet&feed=ATOM1.0', - 'Mobil' - => 'https://rss.golem.de/rss.php?ms=mobil&feed=ATOM1.0', - 'Open Source' - => 'https://rss.golem.de/rss.php?ms=open-source&feed=ATOM1.0', - 'Politik/Recht' - => 'https://rss.golem.de/rss.php?ms=politik-recht&feed=ATOM1.0', - 'Security' - => 'https://rss.golem.de/rss.php?ms=security&feed=ATOM1.0', - 'Desktop-Applikationen' - => 'https://rss.golem.de/rss.php?ms=desktop-applikationen&feed=ATOM1.0', - 'Software-Entwicklung' - => 'https://rss.golem.de/rss.php?ms=softwareentwicklung&feed=ATOM1.0', - 'Wirtschaft' - => 'https://rss.golem.de/rss.php?ms=wirtschaft&feed=ATOM1.0', - 'Wissenschaft' - => 'https://rss.golem.de/rss.php?ms=wissenschaft&feed=ATOM1.0' - ) - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Specify number of full articles to return', - 'defaultValue' => 5 - ) - )); - const LIMIT = 5; - const HEADERS = array('Cookie: golem_consent20=simple|220101;'); - - public function collectData() { - $this->collectExpandableDatas( - $this->getInput('category'), - $this->getInput('limit') ?: static::LIMIT - ); - } - - protected function parseItem($item) { - $item = parent::parseItem($item); - $item['content'] = $item['content'] ?? ''; - $uri = $item['uri']; - - while ($uri) { - $articlePage = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT, static::HEADERS); - - // URI without RSS feed reference - $item['uri'] = $articlePage->find('head meta[name="twitter:url"]', 0)->content; - - $author = $articlePage->find('article header .authors .authors__name', 0); - if ($author) { - $item['author'] = $author->innertext; - } - - $item['content'] .= $this->extractContent($articlePage); - - // next page - $nextUri = $articlePage->find('link[rel="next"]', 0); - $uri = $nextUri ? static::URI . $nextUri->href : null; - } - - return $item; - } - - private function extractContent($page) { - $item = ''; - - $article = $page->find('article', 0); - - // delete known bad elements - foreach($article->find('div[id*="adtile"], #job-market, #seminars, - div.gbox_affiliate, div.toc, .embedcontent') as $bad) { - $bad->remove(); - } - // reload html, as remove() is buggy - $article = str_get_html($article->outertext); - - if ($pageHeader = $article->find('header.paged-cluster-header h1', 0)) { - $item .= $pageHeader; - } - - $header = $article->find('header', 0); - foreach($header->find('p, figure') as $element) { - $item .= $element; - } - - $content = $article->find('div.formatted', 0); - - // full image quality - foreach($content->find('img[data-src-full][src*="."]') as $img) { - $img->src = $img->getAttribute('data-src-full'); - } - - foreach($content->find('p, h1, h3, img[src*="."]') as $element) { - $item .= $element; - } - - return $item; - } +class GolemBridge extends FeedExpander +{ + const MAINTAINER = 'Mynacol'; + const NAME = 'Golem Bridge'; + const URI = 'https://www.golem.de/'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns the full articles instead of only the intro'; + const PARAMETERS = [[ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'Alle News' + => 'https://rss.golem.de/rss.php?feed=ATOM1.0', + 'Audio/Video' + => 'https://rss.golem.de/rss.php?ms=audio-video&feed=ATOM1.0', + 'Auto' + => 'https://rss.golem.de/rss.php?ms=auto&feed=ATOM1.0', + 'Foto' + => 'https://rss.golem.de/rss.php?ms=foto&feed=ATOM1.0', + 'Games' + => 'https://rss.golem.de/rss.php?ms=games&feed=ATOM1.0', + 'Handy' + => 'https://rss.golem.de/rss.php?ms=handy&feed=ATOM1.0', + 'Internet' + => 'https://rss.golem.de/rss.php?ms=internet&feed=ATOM1.0', + 'Mobil' + => 'https://rss.golem.de/rss.php?ms=mobil&feed=ATOM1.0', + 'Open Source' + => 'https://rss.golem.de/rss.php?ms=open-source&feed=ATOM1.0', + 'Politik/Recht' + => 'https://rss.golem.de/rss.php?ms=politik-recht&feed=ATOM1.0', + 'Security' + => 'https://rss.golem.de/rss.php?ms=security&feed=ATOM1.0', + 'Desktop-Applikationen' + => 'https://rss.golem.de/rss.php?ms=desktop-applikationen&feed=ATOM1.0', + 'Software-Entwicklung' + => 'https://rss.golem.de/rss.php?ms=softwareentwicklung&feed=ATOM1.0', + 'Wirtschaft' + => 'https://rss.golem.de/rss.php?ms=wirtschaft&feed=ATOM1.0', + 'Wissenschaft' + => 'https://rss.golem.de/rss.php?ms=wissenschaft&feed=ATOM1.0' + ] + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Specify number of full articles to return', + 'defaultValue' => 5 + ] + ]]; + const LIMIT = 5; + const HEADERS = ['Cookie: golem_consent20=simple|220101;']; + + public function collectData() + { + $this->collectExpandableDatas( + $this->getInput('category'), + $this->getInput('limit') ?: static::LIMIT + ); + } + + protected function parseItem($item) + { + $item = parent::parseItem($item); + $item['content'] = $item['content'] ?? ''; + $uri = $item['uri']; + + while ($uri) { + $articlePage = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT, static::HEADERS); + + // URI without RSS feed reference + $item['uri'] = $articlePage->find('head meta[name="twitter:url"]', 0)->content; + + $author = $articlePage->find('article header .authors .authors__name', 0); + if ($author) { + $item['author'] = $author->innertext; + } + + $item['content'] .= $this->extractContent($articlePage); + + // next page + $nextUri = $articlePage->find('link[rel="next"]', 0); + $uri = $nextUri ? static::URI . $nextUri->href : null; + } + + return $item; + } + + private function extractContent($page) + { + $item = ''; + + $article = $page->find('article', 0); + + // delete known bad elements + foreach ( + $article->find('div[id*="adtile"], #job-market, #seminars, + div.gbox_affiliate, div.toc, .embedcontent') as $bad + ) { + $bad->remove(); + } + // reload html, as remove() is buggy + $article = str_get_html($article->outertext); + + if ($pageHeader = $article->find('header.paged-cluster-header h1', 0)) { + $item .= $pageHeader; + } + + $header = $article->find('header', 0); + foreach ($header->find('p, figure') as $element) { + $item .= $element; + } + + $content = $article->find('div.formatted', 0); + + // full image quality + foreach ($content->find('img[data-src-full][src*="."]') as $img) { + $img->src = $img->getAttribute('data-src-full'); + } + + foreach ($content->find('p, h1, h3, img[src*="."]') as $element) { + $item .= $element; + } + + return $item; + } } diff --git a/bridges/GoodreadsBridge.php b/bridges/GoodreadsBridge.php index 4d92dd7f..ae1a865e 100644 --- a/bridges/GoodreadsBridge.php +++ b/bridges/GoodreadsBridge.php @@ -1,95 +1,95 @@ <?php -class GoodreadsBridge extends BridgeAbstract { - - const MAINTAINER = 'captn3m0'; - const NAME = 'Goodreads Bridge'; - const URI = 'https://www.goodreads.com/'; - const CACHE_TIMEOUT = 0; // 30min - const DESCRIPTION = 'Various RSS feeds from Goodreads'; - - const CONTEXT_AUTHOR_BOOKS = 'Books by Author'; - - // Using a specific context because I plan to expand this soon - const PARAMETERS = array( - 'Books by Author' => array( - 'author_url' => array( - 'name' => 'Link to author\'s page on Goodreads', - 'type' => 'text', - 'required' => true, - 'title' => 'Should look somewhat like goodreads.com/author/show/', - 'pattern' => '^(https:\/\/)?(www.)?goodreads\.com\/author\/show\/\d+\..*$', - 'exampleValue' => 'https://www.goodreads.com/author/show/38550.Brandon_Sanderson' - ), - 'published_only' => array( - 'name' => 'Show published books only', - 'type' => 'checkbox', - 'required' => false, - 'title' => 'If left unchecked, this will return unpublished books as well', - 'defaultValue' => 'checked', - ), - ), - ); - - private function collectAuthorBooks($url) { - - $regex = '/goodreads\.com\/author\/show\/(\d+)/'; - - preg_match($regex, $url, $matches); - - $authorId = $matches[1]; - - $authorListUrl = "https://www.goodreads.com/author/list/$authorId?sort=original_publication_year"; - - $html = getSimpleHTMLDOMCached($authorListUrl, self::CACHE_TIMEOUT); - - foreach($html->find('tr[itemtype="http://schema.org/Book"]') as $row) { - $dateSpan = $row->find('.uitext', 0)->plaintext; - $date = null; - - // If book is not yet published, ignore for now - if(preg_match('/published\s+(\d{4})/', $dateSpan, $matches) === 1) { - // Goodreads doesn't give us exact publication date here, only a year - // We are skipping future dates anyway, so this is def published - // but we can't pick a dynamic date either to keep clients from getting - // confused. So we pick a guaranteed date of 1st-Jan instead. - $date = $matches[1] . '-01-01'; - } else if ($this->getInput('published_only') !== 'checked') { - // We can return unpublished books as well - $date = date('Y-01-01'); - } else { - continue; - } - - $row = defaultLinkTo($row, $this->getURI()); - - $item['title'] = $row->find('.bookTitle', 0)->plaintext; - $item['uri'] = $row->find('.bookTitle', 0)->getAttribute('href'); - $item['author'] = $row->find('.authorName', 0)->plaintext; - $item['content'] = '<a href="' - . $row->find('.bookTitle', 0)->getAttribute('href') - . '"><img src="' - . $row->find('.bookCover', 0)->getAttribute('src') - . '"></a>'; - $item['timestamp'] = $date; - $item['enclosures'] = array( - $row->find('.bookCover', 0)->getAttribute('src') - ); - - $this->items[] = $item; // Add item to the list - } - } - - public function collectData() { - - switch ($this->queriedContext) { - case self::CONTEXT_AUTHOR_BOOKS: - $this->collectAuthorBooks($this->getInput('author_url')); - break; - - default: - throw new Exception('Invalid context', 1); - break; - } - } +class GoodreadsBridge extends BridgeAbstract +{ + const MAINTAINER = 'captn3m0'; + const NAME = 'Goodreads Bridge'; + const URI = 'https://www.goodreads.com/'; + const CACHE_TIMEOUT = 0; // 30min + const DESCRIPTION = 'Various RSS feeds from Goodreads'; + + const CONTEXT_AUTHOR_BOOKS = 'Books by Author'; + + // Using a specific context because I plan to expand this soon + const PARAMETERS = [ + 'Books by Author' => [ + 'author_url' => [ + 'name' => 'Link to author\'s page on Goodreads', + 'type' => 'text', + 'required' => true, + 'title' => 'Should look somewhat like goodreads.com/author/show/', + 'pattern' => '^(https:\/\/)?(www.)?goodreads\.com\/author\/show\/\d+\..*$', + 'exampleValue' => 'https://www.goodreads.com/author/show/38550.Brandon_Sanderson' + ], + 'published_only' => [ + 'name' => 'Show published books only', + 'type' => 'checkbox', + 'required' => false, + 'title' => 'If left unchecked, this will return unpublished books as well', + 'defaultValue' => 'checked', + ], + ], + ]; + + private function collectAuthorBooks($url) + { + $regex = '/goodreads\.com\/author\/show\/(\d+)/'; + + preg_match($regex, $url, $matches); + + $authorId = $matches[1]; + + $authorListUrl = "https://www.goodreads.com/author/list/$authorId?sort=original_publication_year"; + + $html = getSimpleHTMLDOMCached($authorListUrl, self::CACHE_TIMEOUT); + + foreach ($html->find('tr[itemtype="http://schema.org/Book"]') as $row) { + $dateSpan = $row->find('.uitext', 0)->plaintext; + $date = null; + + // If book is not yet published, ignore for now + if (preg_match('/published\s+(\d{4})/', $dateSpan, $matches) === 1) { + // Goodreads doesn't give us exact publication date here, only a year + // We are skipping future dates anyway, so this is def published + // but we can't pick a dynamic date either to keep clients from getting + // confused. So we pick a guaranteed date of 1st-Jan instead. + $date = $matches[1] . '-01-01'; + } elseif ($this->getInput('published_only') !== 'checked') { + // We can return unpublished books as well + $date = date('Y-01-01'); + } else { + continue; + } + + $row = defaultLinkTo($row, $this->getURI()); + + $item['title'] = $row->find('.bookTitle', 0)->plaintext; + $item['uri'] = $row->find('.bookTitle', 0)->getAttribute('href'); + $item['author'] = $row->find('.authorName', 0)->plaintext; + $item['content'] = '<a href="' + . $row->find('.bookTitle', 0)->getAttribute('href') + . '"><img src="' + . $row->find('.bookCover', 0)->getAttribute('src') + . '"></a>'; + $item['timestamp'] = $date; + $item['enclosures'] = [ + $row->find('.bookCover', 0)->getAttribute('src') + ]; + + $this->items[] = $item; // Add item to the list + } + } + + public function collectData() + { + switch ($this->queriedContext) { + case self::CONTEXT_AUTHOR_BOOKS: + $this->collectAuthorBooks($this->getInput('author_url')); + break; + + default: + throw new Exception('Invalid context', 1); + break; + } + } } diff --git a/bridges/GoogleGroupsBridge.php b/bridges/GoogleGroupsBridge.php index 03152f5f..5bd7df47 100644 --- a/bridges/GoogleGroupsBridge.php +++ b/bridges/GoogleGroupsBridge.php @@ -1,67 +1,71 @@ <?php -class GoogleGroupsBridge extends XPathAbstract { - const NAME = 'Google Groups Bridge'; - const DESCRIPTION = 'Returns the latest posts on a Google Group'; - const URI = 'https://groups.google.com'; - const PARAMETERS = array( array( - 'group' => array( - 'name' => 'Group id', - 'title' => 'The string that follows /g/ in the URL', - 'exampleValue' => 'governance', - 'required' => true - ), - 'account' => array( - 'name' => 'Account id', - 'title' => 'Some Google groups have an additional id following /a/ in the URL', - 'exampleValue' => 'mozilla.org', - 'required' => false - ) - )); - const CACHE_TIMEOUT = 3600; +class GoogleGroupsBridge extends XPathAbstract +{ + const NAME = 'Google Groups Bridge'; + const DESCRIPTION = 'Returns the latest posts on a Google Group'; + const URI = 'https://groups.google.com'; + const PARAMETERS = [ [ + 'group' => [ + 'name' => 'Group id', + 'title' => 'The string that follows /g/ in the URL', + 'exampleValue' => 'governance', + 'required' => true + ], + 'account' => [ + 'name' => 'Account id', + 'title' => 'Some Google groups have an additional id following /a/ in the URL', + 'exampleValue' => 'mozilla.org', + 'required' => false + ] + ]]; + const CACHE_TIMEOUT = 3600; - const TEST_DETECT_PARAMETERS = array( - 'https://groups.google.com/a/mozilla.org/g/announce' => array( - 'account' => 'mozilla.org', 'group' => 'announce' - ), - 'https://groups.google.com/g/ansible-project' => array( - 'account' => null, 'group' => 'ansible-project' - ), - ); + const TEST_DETECT_PARAMETERS = [ + 'https://groups.google.com/a/mozilla.org/g/announce' => [ + 'account' => 'mozilla.org', 'group' => 'announce' + ], + 'https://groups.google.com/g/ansible-project' => [ + 'account' => null, 'group' => 'ansible-project' + ], + ]; - const XPATH_EXPRESSION_ITEM = '//div[@class="yhgbKd"]'; - const XPATH_EXPRESSION_ITEM_TITLE = './/span[@class="o1DPKc"]'; - const XPATH_EXPRESSION_ITEM_CONTENT = './/span[@class="WzoK"]'; - const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ZLl54"]/@href'; - const XPATH_EXPRESSION_ITEM_AUTHOR = './/span[@class="z0zUgf"][last()]'; - const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/div[@class="tRlaM"]'; - const XPATH_EXPRESSION_ITEM_ENCLOSURES = ''; - const XPATH_EXPRESSION_ITEM_CATEGORIES = ''; - const SETTING_FIX_ENCODING = true; + const XPATH_EXPRESSION_ITEM = '//div[@class="yhgbKd"]'; + const XPATH_EXPRESSION_ITEM_TITLE = './/span[@class="o1DPKc"]'; + const XPATH_EXPRESSION_ITEM_CONTENT = './/span[@class="WzoK"]'; + const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ZLl54"]/@href'; + const XPATH_EXPRESSION_ITEM_AUTHOR = './/span[@class="z0zUgf"][last()]'; + const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/div[@class="tRlaM"]'; + const XPATH_EXPRESSION_ITEM_ENCLOSURES = ''; + const XPATH_EXPRESSION_ITEM_CATEGORIES = ''; + const SETTING_FIX_ENCODING = true; - protected function getSourceUrl() { - $source = self::URI; + protected function getSourceUrl() + { + $source = self::URI; - $account = $this->getInput('account'); - if($account) { - $source = $source . '/a/' . $account; - } - return $source . '/g/' . $this->getInput('group'); - } + $account = $this->getInput('account'); + if ($account) { + $source = $source . '/a/' . $account; + } + return $source . '/g/' . $this->getInput('group'); + } - protected function provideWebsiteContent() { - return defaultLinkTo(getContents($this->getSourceUrl()), self::URI); - } + protected function provideWebsiteContent() + { + return defaultLinkTo(getContents($this->getSourceUrl()), self::URI); + } - const URL_REGEX = '#^https://groups.google.com(?:/a/(?<account>\S+))?(?:/g/(?<group>\S+))#'; + const URL_REGEX = '#^https://groups.google.com(?:/a/(?<account>\S+))?(?:/g/(?<group>\S+))#'; - public function detectParameters($url) { - $params = array(); - if(preg_match(self::URL_REGEX, $url, $matches)) { - $params['group'] = $matches['group']; - $params['account'] = $matches['account']; - return $params; - } - return null; - } + public function detectParameters($url) + { + $params = []; + if (preg_match(self::URL_REGEX, $url, $matches)) { + $params['group'] = $matches['group']; + $params['account'] = $matches['account']; + return $params; + } + return null; + } } diff --git a/bridges/GooglePlayStoreBridge.php b/bridges/GooglePlayStoreBridge.php index ec0c1090..d61be2c8 100644 --- a/bridges/GooglePlayStoreBridge.php +++ b/bridges/GooglePlayStoreBridge.php @@ -1,60 +1,64 @@ <?php -class GooglePlayStoreBridge extends BridgeAbstract { - const NAME = 'Google Play Store'; - const URI = 'https://play.google.com/store/apps'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns the most recent version of an app with its changelog'; - - const TEST_DETECT_PARAMETERS = array( - 'https://play.google.com/store/apps/details?id=com.ichi2.anki' => array( - 'id' => 'com.ichi2.anki' - ) - ); - - const PARAMETERS = array(array( - 'id' => array( - 'name' => 'Application ID', - 'exampleValue' => 'com.ichi2.anki', - 'required' => true - ) - )); - - const INFORMATION_MAP = array( - 'Updated' => 'timestamp', - 'Current Version' => 'title', - 'Offered By' => 'author' - ); - - public function collectData() { - $appuri = static::URI . '/details?id=' . $this->getInput('id'); - $html = getSimpleHTMLDOM($appuri); - - $item = array(); - $item['uri'] = $appuri; - $item['content'] = $html->find('div[itemprop=description]', 1)->innertext; - - // Find other fields from Additional Information section - foreach($html->find('.hAyfc') as $info) { - $index = self::INFORMATION_MAP[$info->first_child()->plaintext] ?? null; - if (is_null($index)) { - continue; - } - $item[$index] = $info->children(1)->plaintext; - } - - $this->items[] = $item; - } - - public function detectParameters($url) { - // Example: https://play.google.com/store/apps/details?id=com.ichi2.anki - - $params = array(); - $regex = '/^(https?:\/\/)?play\.google\.com\/store\/apps\/details\?id=([^\/&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['id'] = urldecode($matches[2]); - return $params; - } - - return null; - } + +class GooglePlayStoreBridge extends BridgeAbstract +{ + const NAME = 'Google Play Store'; + const URI = 'https://play.google.com/store/apps'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns the most recent version of an app with its changelog'; + + const TEST_DETECT_PARAMETERS = [ + 'https://play.google.com/store/apps/details?id=com.ichi2.anki' => [ + 'id' => 'com.ichi2.anki' + ] + ]; + + const PARAMETERS = [[ + 'id' => [ + 'name' => 'Application ID', + 'exampleValue' => 'com.ichi2.anki', + 'required' => true + ] + ]]; + + const INFORMATION_MAP = [ + 'Updated' => 'timestamp', + 'Current Version' => 'title', + 'Offered By' => 'author' + ]; + + public function collectData() + { + $appuri = static::URI . '/details?id=' . $this->getInput('id'); + $html = getSimpleHTMLDOM($appuri); + + $item = []; + $item['uri'] = $appuri; + $item['content'] = $html->find('div[itemprop=description]', 1)->innertext; + + // Find other fields from Additional Information section + foreach ($html->find('.hAyfc') as $info) { + $index = self::INFORMATION_MAP[$info->first_child()->plaintext] ?? null; + if (is_null($index)) { + continue; + } + $item[$index] = $info->children(1)->plaintext; + } + + $this->items[] = $item; + } + + public function detectParameters($url) + { + // Example: https://play.google.com/store/apps/details?id=com.ichi2.anki + + $params = []; + $regex = '/^(https?:\/\/)?play\.google\.com\/store\/apps\/details\?id=([^\/&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['id'] = urldecode($matches[2]); + return $params; + } + + return null; + } } diff --git a/bridges/GoogleSearchBridge.php b/bridges/GoogleSearchBridge.php index 5370804e..406cf2a9 100644 --- a/bridges/GoogleSearchBridge.php +++ b/bridges/GoogleSearchBridge.php @@ -1,102 +1,105 @@ <?php -class GoogleSearchBridge extends BridgeAbstract { +class GoogleSearchBridge extends BridgeAbstract +{ + const MAINTAINER = 'sebsauvage'; + const NAME = 'Google search'; + const URI = 'https://www.google.com/'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns max 100 results from the past year.'; - const MAINTAINER = 'sebsauvage'; - const NAME = 'Google search'; - const URI = 'https://www.google.com/'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns max 100 results from the past year.'; + const PARAMETERS = [[ + 'q' => [ + 'name' => 'keyword', + 'required' => true, + 'exampleValue' => 'rss-bridge', + ], + 'verbatim' => [ + 'name' => 'Verbatim', + 'type' => 'checkbox', + 'title' => 'Use literal keyword(s) without making improvements', + ], + ]]; - const PARAMETERS = array(array( - 'q' => array( - 'name' => 'keyword', - 'required' => true, - 'exampleValue' => 'rss-bridge', - ), - 'verbatim' => array( - 'name' => 'Verbatim', - 'type' => 'checkbox', - 'title' => 'Use literal keyword(s) without making improvements', - ), - )); + public function collectData() + { + $dom = getSimpleHTMLDOM($this->getURI(), ['Accept-language: en-US']); + if (!$dom) { + returnServerError('No results for this query.'); + } + $result = $dom->find('div[id=res]', 0); - public function collectData(){ - $dom = getSimpleHTMLDOM($this->getURI(), ['Accept-language: en-US']); - if (!$dom) { - returnServerError('No results for this query.'); - } - $result = $dom->find('div[id=res]', 0); + if (!$result) { + return; + } - if(!$result) { - return; - } + foreach ($result->find('div[class~=g]') as $element) { + $item = []; - foreach ($result->find('div[class~=g]') as $element) { - $item = []; + $url = $element->find('a[href]', 0)->href; + $item['uri'] = htmlspecialchars_decode($url); + $item['title'] = $element->find('h3', 0)->plaintext; - $url = $element->find('a[href]', 0)->href; - $item['uri'] = htmlspecialchars_decode($url); - $item['title'] = $element->find('h3', 0)->plaintext; + $resultDom = $element->find('div[data-content-feature=1]', 0); + if ($resultDom) { + // Split by — or · + $resultParts = preg_split('/( — | · )/', $resultDom->plaintext); + $resultDate = trim($resultParts[0]); + $resultContent = trim($resultParts[1] ?? ''); + } else { + // Some search results don't have this particular dom identifier + $resultDate = null; + $resultContent = null; + } - $resultDom = $element->find('div[data-content-feature=1]', 0); - if ($resultDom) { - // Split by — or · - $resultParts = preg_split('/( — | · )/', $resultDom->plaintext); - $resultDate = trim($resultParts[0]); - $resultContent = trim($resultParts[1] ?? ''); - } else { - // Some search results don't have this particular dom identifier - $resultDate = null; - $resultContent = null; - } + if ($resultDate) { + try { + $createdAt = new \DateTime($resultDate); + // Set to midnight for consistent datetime + $createdAt->setTime(0, 0); + $item['timestamp'] = $createdAt->format('U'); + } catch (\Exception $e) { + $item['timestamp'] = 0; + } + } else { + $item['timestamp'] = 0; + } - if ($resultDate) { - try { - $createdAt = new \DateTime($resultDate); - // Set to midnight for consistent datetime - $createdAt->setTime(0, 0); - $item['timestamp'] = $createdAt->format('U'); - } catch (\Exception $e) { - $item['timestamp'] = 0; - } - } else { - $item['timestamp'] = 0; - } + if ($resultContent) { + $item['content'] = $resultContent; + } - if ($resultContent) { - $item['content'] = $resultContent; - } + $this->items[] = $item; + } + // Sort by descending date + usort($this->items, function ($a, $b) { + return $b['timestamp'] <=> $a['timestamp']; + }); + } - $this->items[] = $item; - } - // Sort by descending date - usort($this->items, function($a, $b) { - return $b['timestamp'] <=> $a['timestamp']; - }); - } + public function getURI() + { + if ($this->getInput('q')) { + $queryParameters = [ + 'q' => $this->getInput('q'), + 'hl' => 'en', + 'num' => '100', // get 100 results + 'complete' => '0', + // in past year, sort by date, optionally verbatim + 'tbs' => 'qdr:y,sbd:1' . ($this->getInput('verbatim') ? ',li:1' : ''), + ]; + return sprintf('https://www.google.com/search?%s', http_build_query($queryParameters)); + } - public function getURI() { - if ($this->getInput('q')) { - $queryParameters = [ - 'q' => $this->getInput('q'), - 'hl' => 'en', - 'num' => '100', // get 100 results - 'complete' => '0', - // in past year, sort by date, optionally verbatim - 'tbs' => 'qdr:y,sbd:1' . ($this->getInput('verbatim') ? ',li:1' : ''), - ]; - return sprintf('https://www.google.com/search?%s', http_build_query($queryParameters)); - } + return parent::getURI(); + } - return parent::getURI(); - } + public function getName() + { + if (!is_null($this->getInput('q'))) { + return $this->getInput('q') . ' - Google search'; + } - public function getName(){ - if(!is_null($this->getInput('q'))) { - return $this->getInput('q') . ' - Google search'; - } - - return parent::getName(); - } + return parent::getName(); + } } diff --git a/bridges/GrandComicsDatabaseBridge.php b/bridges/GrandComicsDatabaseBridge.php index b4440870..7b83f84b 100644 --- a/bridges/GrandComicsDatabaseBridge.php +++ b/bridges/GrandComicsDatabaseBridge.php @@ -1,61 +1,61 @@ <?php -class GrandComicsDatabaseBridge extends BridgeAbstract { - - const MAINTAINER = 'corenting'; - const NAME = 'Grand Comics Database Bridge'; - const URI = 'https://www.comics.org/'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Returns the latest comics added to a series timeline'; - const PARAMETERS = array( array( - 'series' => array( - 'name' => 'Series id (from the timeline URL)', - 'required' => true, - 'exampleValue' => '63051', - ), - )); - - public function collectData(){ - - $url = self::URI . 'series/' . $this->getInput('series') . '/details/timeline/'; - $html = getSimpleHTMLDOM($url); - - $table = $html->find('table', 0); - $list = array_reverse($table->find('[class^=row_even]')); - $seriesName = $html->find('span[id=series_name]', 0)->innertext; - - // Get row headers - $rowHeaders = $table->find('th'); - foreach($list as $article) { - - // Skip empty rows - $emptyRow = $article->find('td.empty_month'); - if (count($emptyRow) != 0) { - continue; - } - - $rows = $article->find('td'); - $key_date = $rows[0]->innertext; - - // Get URL too - $uri = 'https://www.comics.org' . $article->find('a')[0]->href; - - // Build content - $content = ''; - for($i = 0; $i < count($rowHeaders); $i++) { - $headerItem = $rowHeaders[$i]->innertext; - $rowItem = $rows[$i]->innertext; - $content = $content . $headerItem . ': ' . $rowItem . '<br/>'; - } - - // Build final item - $content = str_replace('href="/', 'href="' . static::URI, $content); - $item = array(); - $item['title'] = $seriesName . ' - ' . $key_date; - $item['timestamp'] = strtotime($key_date); - $item['content'] = str_get_html($content); - $item['uri'] = $uri; - - $this->items[] = $item; - } - } + +class GrandComicsDatabaseBridge extends BridgeAbstract +{ + const MAINTAINER = 'corenting'; + const NAME = 'Grand Comics Database Bridge'; + const URI = 'https://www.comics.org/'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Returns the latest comics added to a series timeline'; + const PARAMETERS = [ [ + 'series' => [ + 'name' => 'Series id (from the timeline URL)', + 'required' => true, + 'exampleValue' => '63051', + ], + ]]; + + public function collectData() + { + $url = self::URI . 'series/' . $this->getInput('series') . '/details/timeline/'; + $html = getSimpleHTMLDOM($url); + + $table = $html->find('table', 0); + $list = array_reverse($table->find('[class^=row_even]')); + $seriesName = $html->find('span[id=series_name]', 0)->innertext; + + // Get row headers + $rowHeaders = $table->find('th'); + foreach ($list as $article) { + // Skip empty rows + $emptyRow = $article->find('td.empty_month'); + if (count($emptyRow) != 0) { + continue; + } + + $rows = $article->find('td'); + $key_date = $rows[0]->innertext; + + // Get URL too + $uri = 'https://www.comics.org' . $article->find('a')[0]->href; + + // Build content + $content = ''; + for ($i = 0; $i < count($rowHeaders); $i++) { + $headerItem = $rowHeaders[$i]->innertext; + $rowItem = $rows[$i]->innertext; + $content = $content . $headerItem . ': ' . $rowItem . '<br/>'; + } + + // Build final item + $content = str_replace('href="/', 'href="' . static::URI, $content); + $item = []; + $item['title'] = $seriesName . ' - ' . $key_date; + $item['timestamp'] = strtotime($key_date); + $item['content'] = str_get_html($content); + $item['uri'] = $uri; + + $this->items[] = $item; + } + } } diff --git a/bridges/GroupBundNaturschutzBridge.php b/bridges/GroupBundNaturschutzBridge.php index d6c5cf11..2aa78578 100644 --- a/bridges/GroupBundNaturschutzBridge.php +++ b/bridges/GroupBundNaturschutzBridge.php @@ -2,106 +2,107 @@ class GroupBundNaturschutzBridge extends XPathAbstract { - const NAME = 'BUND Naturschutz in Bayern e.V. - Kreisgruppen'; - const URI = 'https://www.bund-naturschutz.de/ueber-uns/organisation/kreisgruppen-ortsgruppen'; - const DESCRIPTION = 'Returns the latest news from specified BUND Naturschutz in Bayern e.V. local group (Germany)'; - const MAINTAINER = 'dweipert'; + const NAME = 'BUND Naturschutz in Bayern e.V. - Kreisgruppen'; + const URI = 'https://www.bund-naturschutz.de/ueber-uns/organisation/kreisgruppen-ortsgruppen'; + const DESCRIPTION = 'Returns the latest news from specified BUND Naturschutz in Bayern e.V. local group (Germany)'; + const MAINTAINER = 'dweipert'; - const PARAMETERS = array( - array( - 'group' => array( - 'name' => 'Group', - 'type' => 'list', - 'values' => array( - // 'Aichach-Friedberg' => 'bn-aic.de', # non-uniform page - 'Altötting' => 'altoetting', - 'Amberg-Sulzbach' => 'amberg-sulzbach', - 'Ansbach' => 'ansbach', - 'Aschaffenburg' => 'aschaffenburg', - 'Augsburg' => 'augsburg', - 'Bad Kissingen' => 'bad-kissingen', - 'Bad Tölz' => 'bad-toelz', - 'Bamberg' => 'bamberg', - 'Bayreuth' => 'bayreuth', # single entry # different layout - 'Berchtesgadener Land' => 'berchtesgadener-land', - 'Cham' => 'cham', - // 'Coburg' => 'coburg', # no real entries # different layout - 'Dachau' => 'dachau', - 'Deggendorf' => 'Deggendorf', - 'Dillingen' => 'dillingen', - 'Dingolfing-Landau' => 'dingolfing-landau', - 'Donau-Ries' => 'donauries', - 'Ebersberg' => 'ebersberg', - 'Eichstätt' => 'eichstaett', # single entry since 2020 - 'Erding' => 'erding', - 'Erlangen' => 'erlangen', - 'Forchheim' => 'forchheim', - 'Freising' => 'freising', - 'Freyung-Grafenau' => 'freyung-grafenau', - 'Fürstenfeldbruck' => 'fuerstenfeldbruck', - 'Fürth-Land' => 'fuerth-land', - 'Fürth-Stadt' => 'fuerth', - 'Garmisch-Partenkirchen' => 'garmisch-partenkirchen', - 'Günzburg' => 'guenzburg', - 'Hassberge' => 'hassberge', - 'Höchstadt-Herzogenaurach' => 'hoechstadt-herzogenaurach', - // 'Hof' => 'kreisgruppehof.bund-naturschutz.com', # non-uniform page - 'Ingolstadt' => 'ingolstadt', - 'Kelheim' => 'kelheim', - 'Kempten' => 'kempten', - 'Kitzingen' => 'kitzingen', - 'Kronach' => 'kronach', - 'Kulmbach' => 'kulmbach', - 'Landsberg' => 'landsberg', - 'Landshut' => 'landshut', - 'Lichtenfeld' => 'lichtenfels', - 'Lindau' => 'lindau', - 'Main-Spessart' => 'main-spessart', - 'Memmingen-Unterallgäu' => 'memmingen-unterallgaeu', - 'Miesbach' => 'miesbach', - 'Miltenberg' => 'miltenberg', - 'Mühldorf am Inn' => 'muehldorf', - // 'München' => 'bn-muenchen.de', # non-uniform page - 'Neu-Ulm' => 'neu-ulm', - 'Neuburg-Schrobenhausen' => 'neuburg-schrobenhausen', - 'Neumarkt' => 'neumarkt', - 'Neustadt/Aisch-Bad Windsheim' => 'neustadt-aisch', - 'Neustadt/Waldnaab-Weiden' => 'neustadt-weiden', - 'Nürnberg Stadt' => 'nuernberg-stadt', - 'Nürnberger Land' => 'nuernberger-land', - 'Ostallgäu-Kaufbeuren' => 'Ostallgäu-Kaufbeuren', - 'Passau' => 'passau', - 'Pfaffenhofen/Ilm' => 'pfaffenhofen', - 'Regen' => 'regen', - 'Regensburg' => 'regensburg', - 'Rhön-Grabfeld' => 'rhoen-grabfeld', - 'Rosenheim' => 'rosenheim', - 'Roth' => 'roth', - 'Rottal-Inn' => 'rottal-inn', - 'Schwabach' => 'schwabach', - 'Schwandorf' => 'schwandorf', - 'Schweinfurt' => 'schweinfurt', - 'Starnberg' => 'starnberg', - 'Straubing-Bogen' => 'straubing', - 'Tirschenreuth' => 'tirschenreuth', - 'Traunstein' => 'traunstein', - 'Weilheim-Schongau' => 'weilheim-schongau', - 'Weißenburg-Gunzenhausen' => 'weissenburg-gunzenhausen', - 'Wunsiedel' => 'wunsiedel', - 'Würzburg' => 'wuerzburg', - ), - ), - ), - ); + const PARAMETERS = [ + [ + 'group' => [ + 'name' => 'Group', + 'type' => 'list', + 'values' => [ + // 'Aichach-Friedberg' => 'bn-aic.de', # non-uniform page + 'Altötting' => 'altoetting', + 'Amberg-Sulzbach' => 'amberg-sulzbach', + 'Ansbach' => 'ansbach', + 'Aschaffenburg' => 'aschaffenburg', + 'Augsburg' => 'augsburg', + 'Bad Kissingen' => 'bad-kissingen', + 'Bad Tölz' => 'bad-toelz', + 'Bamberg' => 'bamberg', + 'Bayreuth' => 'bayreuth', # single entry # different layout + 'Berchtesgadener Land' => 'berchtesgadener-land', + 'Cham' => 'cham', + // 'Coburg' => 'coburg', # no real entries # different layout + 'Dachau' => 'dachau', + 'Deggendorf' => 'Deggendorf', + 'Dillingen' => 'dillingen', + 'Dingolfing-Landau' => 'dingolfing-landau', + 'Donau-Ries' => 'donauries', + 'Ebersberg' => 'ebersberg', + 'Eichstätt' => 'eichstaett', # single entry since 2020 + 'Erding' => 'erding', + 'Erlangen' => 'erlangen', + 'Forchheim' => 'forchheim', + 'Freising' => 'freising', + 'Freyung-Grafenau' => 'freyung-grafenau', + 'Fürstenfeldbruck' => 'fuerstenfeldbruck', + 'Fürth-Land' => 'fuerth-land', + 'Fürth-Stadt' => 'fuerth', + 'Garmisch-Partenkirchen' => 'garmisch-partenkirchen', + 'Günzburg' => 'guenzburg', + 'Hassberge' => 'hassberge', + 'Höchstadt-Herzogenaurach' => 'hoechstadt-herzogenaurach', + // 'Hof' => 'kreisgruppehof.bund-naturschutz.com', # non-uniform page + 'Ingolstadt' => 'ingolstadt', + 'Kelheim' => 'kelheim', + 'Kempten' => 'kempten', + 'Kitzingen' => 'kitzingen', + 'Kronach' => 'kronach', + 'Kulmbach' => 'kulmbach', + 'Landsberg' => 'landsberg', + 'Landshut' => 'landshut', + 'Lichtenfeld' => 'lichtenfels', + 'Lindau' => 'lindau', + 'Main-Spessart' => 'main-spessart', + 'Memmingen-Unterallgäu' => 'memmingen-unterallgaeu', + 'Miesbach' => 'miesbach', + 'Miltenberg' => 'miltenberg', + 'Mühldorf am Inn' => 'muehldorf', + // 'München' => 'bn-muenchen.de', # non-uniform page + 'Neu-Ulm' => 'neu-ulm', + 'Neuburg-Schrobenhausen' => 'neuburg-schrobenhausen', + 'Neumarkt' => 'neumarkt', + 'Neustadt/Aisch-Bad Windsheim' => 'neustadt-aisch', + 'Neustadt/Waldnaab-Weiden' => 'neustadt-weiden', + 'Nürnberg Stadt' => 'nuernberg-stadt', + 'Nürnberger Land' => 'nuernberger-land', + 'Ostallgäu-Kaufbeuren' => 'Ostallgäu-Kaufbeuren', + 'Passau' => 'passau', + 'Pfaffenhofen/Ilm' => 'pfaffenhofen', + 'Regen' => 'regen', + 'Regensburg' => 'regensburg', + 'Rhön-Grabfeld' => 'rhoen-grabfeld', + 'Rosenheim' => 'rosenheim', + 'Roth' => 'roth', + 'Rottal-Inn' => 'rottal-inn', + 'Schwabach' => 'schwabach', + 'Schwandorf' => 'schwandorf', + 'Schweinfurt' => 'schweinfurt', + 'Starnberg' => 'starnberg', + 'Straubing-Bogen' => 'straubing', + 'Tirschenreuth' => 'tirschenreuth', + 'Traunstein' => 'traunstein', + 'Weilheim-Schongau' => 'weilheim-schongau', + 'Weißenburg-Gunzenhausen' => 'weissenburg-gunzenhausen', + 'Wunsiedel' => 'wunsiedel', + 'Würzburg' => 'wuerzburg', + ], + ], + ], + ]; - const XPATH_EXPRESSION_ITEM = '//div[@itemtype="http://schema.org/Article"]'; - const XPATH_EXPRESSION_ITEM_TITLE = './/*[@itemprop="headline"]'; - const XPATH_EXPRESSION_ITEM_CONTENT = './/*[@itemprop="description"]/text()'; - const XPATH_EXPRESSION_ITEM_URI = './/a/@href'; - const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/*[@itemprop="datePublished"]/@datetime'; - const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@src'; + const XPATH_EXPRESSION_ITEM = '//div[@itemtype="http://schema.org/Article"]'; + const XPATH_EXPRESSION_ITEM_TITLE = './/*[@itemprop="headline"]'; + const XPATH_EXPRESSION_ITEM_CONTENT = './/*[@itemprop="description"]/text()'; + const XPATH_EXPRESSION_ITEM_URI = './/a/@href'; + const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/*[@itemprop="datePublished"]/@datetime'; + const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@src'; - protected function getSourceUrl() { - return 'https://' . $this->getInput('group') . '.bund-naturschutz.de/aktuelles'; - } + protected function getSourceUrl() + { + return 'https://' . $this->getInput('group') . '.bund-naturschutz.de/aktuelles'; + } } diff --git a/bridges/HDWallpapersBridge.php b/bridges/HDWallpapersBridge.php index c2dc5c20..ca5e251b 100644 --- a/bridges/HDWallpapersBridge.php +++ b/bridges/HDWallpapersBridge.php @@ -1,86 +1,91 @@ <?php -class HDWallpapersBridge extends BridgeAbstract { - const MAINTAINER = 'nel50n'; - const NAME = 'HD Wallpapers Bridge'; - const URI = 'https://www.hdwallpapers.in/'; - const CACHE_TIMEOUT = 43200; //12h - const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers'; - const PARAMETERS = array( array( - 'c' => array( - 'name' => 'category', - 'required' => true, - 'defaultValue' => 'latest_wallpapers' - ), - 'm' => array( - 'name' => 'max number of wallpapers' - ), - 'r' => array( - 'name' => 'resolution', - 'required' => true, - 'defaultValue' => 'HD', - 'title' => 'e.g=HD OR 1920x1200 OR 1680x1050' - ) - )); +class HDWallpapersBridge extends BridgeAbstract +{ + const MAINTAINER = 'nel50n'; + const NAME = 'HD Wallpapers Bridge'; + const URI = 'https://www.hdwallpapers.in/'; + const CACHE_TIMEOUT = 43200; //12h + const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers'; - public function collectData(){ - $category = $this->getInput('c'); - if(strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) { - $category .= '-desktop-wallpapers'; - } + const PARAMETERS = [ [ + 'c' => [ + 'name' => 'category', + 'required' => true, + 'defaultValue' => 'latest_wallpapers' + ], + 'm' => [ + 'name' => 'max number of wallpapers' + ], + 'r' => [ + 'name' => 'resolution', + 'required' => true, + 'defaultValue' => 'HD', + 'title' => 'e.g=HD OR 1920x1200 OR 1680x1050' + ] + ]]; - $num = 0; - $max = $this->getInput('m') ?: 14; - $lastpage = 1; + public function collectData() + { + $category = $this->getInput('c'); + if (strrpos($category, 'wallpapers') !== strlen($category) - strlen('wallpapers')) { + $category .= '-desktop-wallpapers'; + } - for($page = 1; $page <= $lastpage; $page++) { - $link = self::URI . $category . '/page/' . $page; - $html = getSimpleHTMLDOM($link); + $num = 0; + $max = $this->getInput('m') ?: 14; + $lastpage = 1; - if($page === 1) { - preg_match('/page\/(\d+)$/', $html->find('.pagination a', -2)->href, $matches); - $lastpage = min($matches[1], ceil($max / 14)); - } + for ($page = 1; $page <= $lastpage; $page++) { + $link = self::URI . $category . '/page/' . $page; + $html = getSimpleHTMLDOM($link); - $html = defaultLinkTo($html, self::URI); + if ($page === 1) { + preg_match('/page\/(\d+)$/', $html->find('.pagination a', -2)->href, $matches); + $lastpage = min($matches[1], ceil($max / 14)); + } - foreach($html->find('.wallpapers .wall a') as $element) { - $thumbnail = $element->find('img', 0); + $html = defaultLinkTo($html, self::URI); - $search = array(self::URI, 'wallpapers.html'); - $replace = array(self::URI . 'download/', $this->getInput('r') . '.jpg'); + foreach ($html->find('.wallpapers .wall a') as $element) { + $thumbnail = $element->find('img', 0); - $item = array(); - $item['uri'] = str_replace($search, $replace, $element->href); + $search = [self::URI, 'wallpapers.html']; + $replace = [self::URI . 'download/', $this->getInput('r') . '.jpg']; - $item['timestamp'] = time(); - $item['title'] = $element->find('em1', 0)->text(); - $item['content'] = $item['title'] - . '<br><a href="' - . $item['uri'] - . '"><img src="' - . $thumbnail->src - . '" /></a>'; + $item = []; + $item['uri'] = str_replace($search, $replace, $element->href); - $item['enclosures'] = array($item['uri']); - $this->items[] = $item; + $item['timestamp'] = time(); + $item['title'] = $element->find('em1', 0)->text(); + $item['content'] = $item['title'] + . '<br><a href="' + . $item['uri'] + . '"><img src="' + . $thumbnail->src + . '" /></a>'; - $num++; - if ($num >= $max) - break 2; - } - } - } + $item['enclosures'] = [$item['uri']]; + $this->items[] = $item; - public function getName(){ - if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) { - return 'HDWallpapers - ' - . str_replace(array('__', '_'), array(' & ', ' '), $this->getInput('c')) - . ' [' - . $this->getInput('r') - . ']'; - } + $num++; + if ($num >= $max) { + break 2; + } + } + } + } - return parent::getName(); - } + public function getName() + { + if (!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) { + return 'HDWallpapers - ' + . str_replace(['__', '_'], [' & ', ' '], $this->getInput('c')) + . ' [' + . $this->getInput('r') + . ']'; + } + + return parent::getName(); + } } diff --git a/bridges/HackerNewsUserThreadsBridge.php b/bridges/HackerNewsUserThreadsBridge.php index 1b8410dd..fee96b61 100644 --- a/bridges/HackerNewsUserThreadsBridge.php +++ b/bridges/HackerNewsUserThreadsBridge.php @@ -1,48 +1,50 @@ <?php -class HackerNewsUserThreadsBridge extends BridgeAbstract { - const MAINTAINER = 'rakoo'; - const NAME = 'Hacker News User Threads'; - const URI = 'https://news.ycombinator.com'; - const CACHE_TIMEOUT = 7200; // 2 hours - const DESCRIPTION = 'Hacker News threads for a user (at https://news.ycombinator.com/threads?id=xxx)'; - const PARAMETERS = array( array( - 'user' => array( - 'name' => 'User', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'nixcraft', - 'title' => 'User whose threads you want to see' - ) - )); +class HackerNewsUserThreadsBridge extends BridgeAbstract +{ + const MAINTAINER = 'rakoo'; + const NAME = 'Hacker News User Threads'; + const URI = 'https://news.ycombinator.com'; + const CACHE_TIMEOUT = 7200; // 2 hours + const DESCRIPTION = 'Hacker News threads for a user (at https://news.ycombinator.com/threads?id=xxx)'; + const PARAMETERS = [ [ + 'user' => [ + 'name' => 'User', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'nixcraft', + 'title' => 'User whose threads you want to see' + ] + ]]; - public function collectData(){ - $url = 'https://news.ycombinator.com/threads?id=' . $this->getInput('user'); - $html = getSimpleHTMLDOM($url); - Debug::log('queried ' . $url); - Debug::log('found ' . $html); + public function collectData() + { + $url = 'https://news.ycombinator.com/threads?id=' . $this->getInput('user'); + $html = getSimpleHTMLDOM($url); + Debug::log('queried ' . $url); + Debug::log('found ' . $html); - $item = array(); - $articles = $html->find('tr[class*="comtr"]'); - $story = ''; + $item = []; + $articles = $html->find('tr[class*="comtr"]'); + $story = ''; - foreach ($articles as $element) { - $id = $element->getAttribute('id'); - $item['uri'] = 'https://news.ycombinator.com/item?id=' . $id; + foreach ($articles as $element) { + $id = $element->getAttribute('id'); + $item['uri'] = 'https://news.ycombinator.com/item?id=' . $id; - $author = $element->find('span[class*="comhead"]', 0)->find('a[class="hnuser"]', 0)->innertext; - $newstory = $element->find('span[class*="comhead"]', 0)->find('span[class="onstory"]', 0); - if (count($newstory->find('a')) > 0) { - $story = $newstory->find('a', 0)->innertext; - } + $author = $element->find('span[class*="comhead"]', 0)->find('a[class="hnuser"]', 0)->innertext; + $newstory = $element->find('span[class*="comhead"]', 0)->find('span[class="onstory"]', 0); + if (count($newstory->find('a')) > 0) { + $story = $newstory->find('a', 0)->innertext; + } - $title = $author . ' | on ' . $story; - $item['author'] = $author; - $item['title'] = $title; - $item['timestamp'] = $element->find('span[class*="age"]', 0)->find('a', 0)->innertext; - $item['content'] = $element->find('span[class*="commtext"]', 0)->innertext; + $title = $author . ' | on ' . $story; + $item['author'] = $author; + $item['title'] = $title; + $item['timestamp'] = $element->find('span[class*="age"]', 0)->find('a', 0)->innertext; + $item['content'] = $element->find('span[class*="commtext"]', 0)->innertext; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/HardwareInfoBridge.php b/bridges/HardwareInfoBridge.php index ae79e0fd..e295984c 100644 --- a/bridges/HardwareInfoBridge.php +++ b/bridges/HardwareInfoBridge.php @@ -1,66 +1,67 @@ <?php + class HardwareInfoBridge extends FeedExpander { - const NAME = 'Hardware Info Bridge'; - const URI = 'https://nl.hardware.info/'; - const DESCRIPTION = 'Tech news from hardware.info (Dutch)'; - const MAINTAINER = 't0stiman'; - - public function collectData() - { - $this->collectExpandableDatas('https://nl.hardware.info/updates/all.rss', 20); - } - - protected function parseItem($feedItem) - { - $item = parent::parseItem($feedItem); + const NAME = 'Hardware Info Bridge'; + const URI = 'https://nl.hardware.info/'; + const DESCRIPTION = 'Tech news from hardware.info (Dutch)'; + const MAINTAINER = 't0stiman'; - //get full article - $articlePage = getSimpleHTMLDOMCached($feedItem->link); + public function collectData() + { + $this->collectExpandableDatas('https://nl.hardware.info/updates/all.rss', 20); + } - $article = $articlePage->find('div.article__content', 0); + protected function parseItem($feedItem) + { + $item = parent::parseItem($feedItem); - //everything under the social bar is not part of the article, remove it - $reachedEndOfArticle = false; + //get full article + $articlePage = getSimpleHTMLDOMCached($feedItem->link); - foreach($article->find('*') as $child) { + $article = $articlePage->find('div.article__content', 0); - if(!$reachedEndOfArticle && isset($child->attr['class']) - && $child->attr['class'] == 'article__content__social-bar') { - $reachedEndOfArticle = true; - } + //everything under the social bar is not part of the article, remove it + $reachedEndOfArticle = false; - if($reachedEndOfArticle) { - $child->outertext = ''; - } - } + foreach ($article->find('*') as $child) { + if ( + !$reachedEndOfArticle && isset($child->attr['class']) + && $child->attr['class'] == 'article__content__social-bar' + ) { + $reachedEndOfArticle = true; + } - //get rid of some more elements we don't need - $to_remove_selectors = array( - 'script', - 'div.incontent', - 'div.article__content__social-bar', - 'div#revealNewsTip', - 'div.article__previous_next' - ); + if ($reachedEndOfArticle) { + $child->outertext = ''; + } + } - foreach($to_remove_selectors as $selector) { - foreach($article->find($selector) as $found) { - $found->outertext = ''; - } - } + //get rid of some more elements we don't need + $to_remove_selectors = [ + 'script', + 'div.incontent', + 'div.article__content__social-bar', + 'div#revealNewsTip', + 'div.article__previous_next' + ]; - // convert iframes to links. meant for embedded YouTube videos. - foreach($article->find('iframe') as $found) { + foreach ($to_remove_selectors as $selector) { + foreach ($article->find($selector) as $found) { + $found->outertext = ''; + } + } - $iframeUrl = $found->getAttribute('src'); + // convert iframes to links. meant for embedded YouTube videos. + foreach ($article->find('iframe') as $found) { + $iframeUrl = $found->getAttribute('src'); - if ($iframeUrl) { - $found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>'; - } - } + if ($iframeUrl) { + $found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>'; + } + } - $item['content'] = $article; - return $item; - } + $item['content'] = $article; + return $item; + } } diff --git a/bridges/HashnodeBridge.php b/bridges/HashnodeBridge.php index 159510fb..ccfea547 100644 --- a/bridges/HashnodeBridge.php +++ b/bridges/HashnodeBridge.php @@ -1,46 +1,48 @@ <?php -class HashnodeBridge extends BridgeAbstract { +class HashnodeBridge extends BridgeAbstract +{ + const MAINTAINER = 'liamka'; + const NAME = 'Hashnode'; + const URI = 'https://hashnode.com'; + const CACHE_TIMEOUT = 3600; // 1hr + const DESCRIPTION = 'See trending or latest posts in Hashnode community.'; + const LATEST_POSTS = 'https://hashnode.com/api/stories/recent?page='; - const MAINTAINER = 'liamka'; - const NAME = 'Hashnode'; - const URI = 'https://hashnode.com'; - const CACHE_TIMEOUT = 3600; // 1hr - const DESCRIPTION = 'See trending or latest posts in Hashnode community.'; - const LATEST_POSTS = 'https://hashnode.com/api/stories/recent?page='; + public function collectData() + { + $this->items = []; + for ($i = 0; $i < 5; $i++) { + $url = self::LATEST_POSTS . $i; + $content = getContents($url); + $array = json_decode($content, true); - public function collectData(){ - $this->items = []; - for ($i = 0; $i < 5; $i++) { - $url = self::LATEST_POSTS . $i; - $content = getContents($url); - $array = json_decode($content, true); + if ($array['posts'] != null) { + foreach ($array['posts'] as $post) { + $item = []; + $item['title'] = $post['title']; + $item['content'] = nl2br(htmlspecialchars($post['brief'])); + $item['timestamp'] = $post['dateAdded']; + if ($post['partOfPublication'] === true) { + $item['uri'] = sprintf( + 'https://%s.hashnode.dev/%s', + $post['publication']['username'], + $post['slug'] + ); + } else { + $item['uri'] = sprintf('https://hashnode.com/post/%s', $post['slug']); + } + if (!isset($item['uri'])) { + continue; + } + $this->items[] = $item; + } + } + } + } - if($array['posts'] != null) { - foreach($array['posts'] as $post) { - $item = []; - $item['title'] = $post['title']; - $item['content'] = nl2br(htmlspecialchars($post['brief'])); - $item['timestamp'] = $post['dateAdded']; - if($post['partOfPublication'] === true) { - $item['uri'] = sprintf( - 'https://%s.hashnode.dev/%s', - $post['publication']['username'], - $post['slug'] - ); - } else { - $item['uri'] = sprintf('https://hashnode.com/post/%s', $post['slug']); - } - if(!isset($item['uri'])) { - continue; - } - $this->items[] = $item; - } - } - } - } - - public function getName(){ - return self::NAME . ': Recent posts'; - } + public function getName() + { + return self::NAME . ': Recent posts'; + } } diff --git a/bridges/HaveIBeenPwnedBridge.php b/bridges/HaveIBeenPwnedBridge.php index da1e3938..e8a5e4f9 100644 --- a/bridges/HaveIBeenPwnedBridge.php +++ b/bridges/HaveIBeenPwnedBridge.php @@ -1,4 +1,5 @@ <?php + /** * Uses the API as documented here: * https://haveibeenpwned.com/API/v3#AllBreaches @@ -6,149 +7,148 @@ * Gets the latest breaches by the date of the breach or when it was added to * HIBP. * */ -class HaveIBeenPwnedBridge extends BridgeAbstract { - const NAME = 'Have I Been Pwned (HIBP) Bridge'; - const URI = 'https://haveibeenpwned.com'; - const DESCRIPTION = 'Returns list of Pwned websites'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(array( - 'order' => array( - 'name' => 'Order by', - 'type' => 'list', - 'values' => array( - 'Breach date' => 'breachDate', - 'Date added to HIBP' => 'dateAdded', - ), - 'defaultValue' => 'dateAdded', - ), - 'item_limit' => array( - 'name' => 'Limit number of returned items', - 'type' => 'number', - 'required' => true, - 'defaultValue' => 20, - ) - )); - const API_URI = 'https://haveibeenpwned.com/api/v3'; - - const CACHE_TIMEOUT = 3600; - - private $breaches = array(); - - public function collectData() { - - $data = json_decode(getContents(self::API_URI . '/breaches'), true); - - foreach($data as $breach) { - $item = array(); - - $pwnCount = number_format($breach['PwnCount']); - $item['title'] = $breach['Title'] . ' - ' - . $pwnCount . ' breached accounts'; - $item['dateAdded'] = $breach['AddedDate']; - $item['breachDate'] = $breach['BreachDate']; - $item['uri'] = self::URI . '/PwnedWebsites#' . $breach['Name']; - - $item['content'] = '<p>' . $breach['Description'] . '</p>'; - $item['content'] .= '<p>' . $this->breachType($breach) . '</p>'; - - $breachDate = date('j F Y', strtotime($breach['BreachDate'])); - $addedDate = date('j F Y', strtotime($breach['AddedDate'])); - $compData = implode(', ', $breach['DataClasses']); - - $item['content'] .= <<<EOD +class HaveIBeenPwnedBridge extends BridgeAbstract +{ + const NAME = 'Have I Been Pwned (HIBP) Bridge'; + const URI = 'https://haveibeenpwned.com'; + const DESCRIPTION = 'Returns list of Pwned websites'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [[ + 'order' => [ + 'name' => 'Order by', + 'type' => 'list', + 'values' => [ + 'Breach date' => 'breachDate', + 'Date added to HIBP' => 'dateAdded', + ], + 'defaultValue' => 'dateAdded', + ], + 'item_limit' => [ + 'name' => 'Limit number of returned items', + 'type' => 'number', + 'required' => true, + 'defaultValue' => 20, + ] + ]]; + const API_URI = 'https://haveibeenpwned.com/api/v3'; + + const CACHE_TIMEOUT = 3600; + + private $breaches = []; + + public function collectData() + { + $data = json_decode(getContents(self::API_URI . '/breaches'), true); + + foreach ($data as $breach) { + $item = []; + + $pwnCount = number_format($breach['PwnCount']); + $item['title'] = $breach['Title'] . ' - ' + . $pwnCount . ' breached accounts'; + $item['dateAdded'] = $breach['AddedDate']; + $item['breachDate'] = $breach['BreachDate']; + $item['uri'] = self::URI . '/PwnedWebsites#' . $breach['Name']; + + $item['content'] = '<p>' . $breach['Description'] . '</p>'; + $item['content'] .= '<p>' . $this->breachType($breach) . '</p>'; + + $breachDate = date('j F Y', strtotime($breach['BreachDate'])); + $addedDate = date('j F Y', strtotime($breach['AddedDate'])); + $compData = implode(', ', $breach['DataClasses']); + + $item['content'] .= <<<EOD <p> <strong>Breach date:</strong> {$breachDate}<br> <strong>Date added to HIBP:</strong> {$addedDate}<br> <strong>Compromised accounts:</strong> {$pwnCount}<br> <strong>Compromised data:</strong> {$compData}<br> EOD; - $item['uid'] = $breach['Name']; - $this->breaches[] = $item; - } - - $this->orderBreaches(); - $this->createItems(); - } - - private const BREACH_TYPES = array( - 'IsVerified' => array( - false => 'Unverified breach, may be sourced from elsewhere' - ), - 'IsFabricated' => array( - true => 'Fabricated breach, likely not legitimate' - ), - 'IsSensitive' => array( - true => 'Sensitive breach, not publicly searchable' - ), - 'IsRetired' => array( - true => 'Retired breach, removed from system' - ), - 'IsSpamList' => array( - true => 'Spam list, used for spam marketing' - ), - 'IsMalware' => array( - true => 'Malware breach' - ), - ); - - /** - * Extract data breach type(s) - */ - private function breachType($breach) { - - $content = ''; - - foreach (self::BREACH_TYPES as $type => $message) { - if (isset($message[$breach[$type]])) { - $content .= $message[$breach[$type]] . '.<br>'; - } - } - - return $content; - - } - - /** - * Order Breaches by date added or date breached - */ - private function orderBreaches() { - - $sortBy = $this->getInput('order'); - $sort = array(); - - foreach ($this->breaches as $key => $item) { - $sort[$key] = $item[$sortBy]; - } - - array_multisort($sort, SORT_DESC, $this->breaches); - - } - - /** - * Create items from breaches array - */ - private function createItems() { - - $limit = $this->getInput('item_limit'); - - if ($limit < 1) { - $limit = 20; - } - - foreach ($this->breaches as $breach) { - $item = array(); - - $item['title'] = $breach['title']; - $item['timestamp'] = $breach[$this->getInput('order')]; - $item['uri'] = $breach['uri']; - $item['content'] = $breach['content']; - $item['uid'] = $breach['uid']; - - $this->items[] = $item; - - if (count($this->items) >= $limit) { - break; - } - } - } + $item['uid'] = $breach['Name']; + $this->breaches[] = $item; + } + + $this->orderBreaches(); + $this->createItems(); + } + + private const BREACH_TYPES = [ + 'IsVerified' => [ + false => 'Unverified breach, may be sourced from elsewhere' + ], + 'IsFabricated' => [ + true => 'Fabricated breach, likely not legitimate' + ], + 'IsSensitive' => [ + true => 'Sensitive breach, not publicly searchable' + ], + 'IsRetired' => [ + true => 'Retired breach, removed from system' + ], + 'IsSpamList' => [ + true => 'Spam list, used for spam marketing' + ], + 'IsMalware' => [ + true => 'Malware breach' + ], + ]; + + /** + * Extract data breach type(s) + */ + private function breachType($breach) + { + $content = ''; + + foreach (self::BREACH_TYPES as $type => $message) { + if (isset($message[$breach[$type]])) { + $content .= $message[$breach[$type]] . '.<br>'; + } + } + + return $content; + } + + /** + * Order Breaches by date added or date breached + */ + private function orderBreaches() + { + $sortBy = $this->getInput('order'); + $sort = []; + + foreach ($this->breaches as $key => $item) { + $sort[$key] = $item[$sortBy]; + } + + array_multisort($sort, SORT_DESC, $this->breaches); + } + + /** + * Create items from breaches array + */ + private function createItems() + { + $limit = $this->getInput('item_limit'); + + if ($limit < 1) { + $limit = 20; + } + + foreach ($this->breaches as $breach) { + $item = []; + + $item['title'] = $breach['title']; + $item['timestamp'] = $breach[$this->getInput('order')]; + $item['uri'] = $breach['uri']; + $item['content'] = $breach['content']; + $item['uid'] = $breach['uid']; + + $this->items[] = $item; + + if (count($this->items) >= $limit) { + break; + } + } + } } diff --git a/bridges/HeiseBridge.php b/bridges/HeiseBridge.php index b73e5124..5f5092e3 100644 --- a/bridges/HeiseBridge.php +++ b/bridges/HeiseBridge.php @@ -1,79 +1,87 @@ <?php -class HeiseBridge extends FeedExpander { - const MAINTAINER = 'Dreckiger-Dan'; - const NAME = 'Heise Online Bridge'; - const URI = 'https://heise.de/'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns the full articles instead of only the intro'; - const PARAMETERS = array(array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'Alle News' - => 'https://www.heise.de/newsticker/heise-atom.xml', - 'Top-News' - => 'https://www.heise.de/newsticker/heise-top-atom.xml', - 'Internet-Störungen' - => 'https://www.heise.de/netze/netzwerk-tools/imonitor-internet-stoerungen/feed/aktuelle-meldungen/', - 'Alle News von heise Developer' - => 'https://www.heise.de/developer/rss/news-atom.xml' - ) - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Specify number of full articles to return', - 'defaultValue' => 5 - ) - )); - const LIMIT = 5; +class HeiseBridge extends FeedExpander +{ + const MAINTAINER = 'Dreckiger-Dan'; + const NAME = 'Heise Online Bridge'; + const URI = 'https://heise.de/'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns the full articles instead of only the intro'; + const PARAMETERS = [[ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'Alle News' + => 'https://www.heise.de/newsticker/heise-atom.xml', + 'Top-News' + => 'https://www.heise.de/newsticker/heise-top-atom.xml', + 'Internet-Störungen' + => 'https://www.heise.de/netze/netzwerk-tools/imonitor-internet-stoerungen/feed/aktuelle-meldungen/', + 'Alle News von heise Developer' + => 'https://www.heise.de/developer/rss/news-atom.xml' + ] + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Specify number of full articles to return', + 'defaultValue' => 5 + ] + ]]; + const LIMIT = 5; - public function collectData() { - $this->collectExpandableDatas( - $this->getInput('category'), - $this->getInput('limit') ?: static::LIMIT - ); - } + public function collectData() + { + $this->collectExpandableDatas( + $this->getInput('category'), + $this->getInput('limit') ?: static::LIMIT + ); + } - protected function parseItem($feedItem) { - $item = parent::parseItem($feedItem); - $item['uri'] = explode('?', $item['uri'])[0] . '?seite=all'; + protected function parseItem($feedItem) + { + $item = parent::parseItem($feedItem); + $item['uri'] = explode('?', $item['uri'])[0] . '?seite=all'; - if (strpos($item['uri'], 'https://www.heise.de') !== 0) { - return $item; - } + if (strpos($item['uri'], 'https://www.heise.de') !== 0) { + return $item; + } - $article = getSimpleHTMLDOMCached($item['uri']); + $article = getSimpleHTMLDOMCached($item['uri']); - if ($article) { - $article = defaultLinkTo($article, $item['uri']); - $item = $this->addArticleToItem($item, $article); - } + if ($article) { + $article = defaultLinkTo($article, $item['uri']); + $item = $this->addArticleToItem($item, $article); + } - return $item; - } + return $item; + } - private function addArticleToItem($item, $article) { - $authors = $article->find('.a-creator__names', 0)->find('.a-creator__name'); - if ($authors) - $item['author'] = implode(', ', array_map(function ($e) { return $e->plaintext; }, $authors )); + private function addArticleToItem($item, $article) + { + $authors = $article->find('.a-creator__names', 0)->find('.a-creator__name'); + if ($authors) { + $item['author'] = implode(', ', array_map(function ($e) { + return $e->plaintext; + }, $authors)); + } - $content = $article->find('div[class*="article-content"]', 0); + $content = $article->find('div[class*="article-content"]', 0); - if ($content == null) - $content = $article->find('#article_content', 0); + if ($content == null) { + $content = $article->find('#article_content', 0); + } - foreach($content->find('p, h3, ul, table, pre, img') as $element) { - $item['content'] .= $element; - } + foreach ($content->find('p, h3, ul, table, pre, img') as $element) { + $item['content'] .= $element; + } - foreach($content->find('img') as $img) { - $item['enclosures'][] = $img->src; - } + foreach ($content->find('img') as $img) { + $item['enclosures'][] = $img->src; + } - return $item; - } + return $item; + } } diff --git a/bridges/HotUKDealsBridge.php b/bridges/HotUKDealsBridge.php index 45cc6637..7328ca04 100644 --- a/bridges/HotUKDealsBridge.php +++ b/bridges/HotUKDealsBridge.php @@ -1,3336 +1,3335 @@ <?php -class HotUKDealsBridge extends PepperBridgeAbstract { +class HotUKDealsBridge extends PepperBridgeAbstract +{ + const NAME = 'HotUKDeals bridge'; + const URI = 'https://www.hotukdeals.com/'; + const DESCRIPTION = 'Return the HotUKDeals search result using keywords'; + const MAINTAINER = 'sysadminstory'; + const PARAMETERS = [ + 'Search by keyword(s))' => [ + 'q' => [ + 'name' => 'Keyword(s)', + 'type' => 'text', + 'exampleValue' => 'lamp', + 'required' => true + ], + 'hide_expired' => [ + 'name' => 'Hide expired deals', + 'type' => 'checkbox', + ], + 'hide_local' => [ + 'name' => 'Hide local deals', + 'type' => 'checkbox', + 'title' => 'Hide deals in physical store', + ], + 'priceFrom' => [ + 'name' => 'Minimal Price', + 'type' => 'text', + 'title' => 'Minmal Price in Pounds', + 'required' => false + ], + 'priceTo' => [ + 'name' => 'Maximum Price', + 'type' => 'text', + 'title' => 'Maximum Price in Pounds', + 'required' => false + ], + ], - const NAME = 'HotUKDeals bridge'; - const URI = 'https://www.hotukdeals.com/'; - const DESCRIPTION = 'Return the HotUKDeals search result using keywords'; - const MAINTAINER = 'sysadminstory'; - const PARAMETERS = array( - 'Search by keyword(s))' => array ( - 'q' => array( - 'name' => 'Keyword(s)', - 'type' => 'text', - 'exampleValue' => 'lamp', - 'required' => true - ), - 'hide_expired' => array( - 'name' => 'Hide expired deals', - 'type' => 'checkbox', - ), - 'hide_local' => array( - 'name' => 'Hide local deals', - 'type' => 'checkbox', - 'title' => 'Hide deals in physical store', - ), - 'priceFrom' => array( - 'name' => 'Minimal Price', - 'type' => 'text', - 'title' => 'Minmal Price in Pounds', - 'required' => false - ), - 'priceTo' => array( - 'name' => 'Maximum Price', - 'type' => 'text', - 'title' => 'Maximum Price in Pounds', - 'required' => false - ), - ), + 'Deals per group' => [ + 'group' => [ + 'name' => 'Group', + 'type' => 'list', + 'title' => 'Group whose deals must be displayed', + 'values' => [ + '3D Blu-ray' => '3d-bluray', + '3D Printer' => '3d-printer', + '3D TV' => '3d-tv', + '4K Blu-ray' => '4k-bluray', + '4K Monitor' => '4k-monitor', + '4K TV' => '4k-tv', + '5G Phones' => '5g-phones', + '7 Up' => '7up', + '8K TV' => '8k-tv', + '32 inch TV' => '32-inch-tv', + '40 inch TV' => '40-inch-tv', + '55 inch TV' => '55-inch-tv', + '65 inch TV' => '65-inch-tv', + '75 inch TV' => '75-inch-tv', + '144Hz Monitor' => '144hz', + 'A4 Paper' => 'a4-paper', + 'AAA Battery' => 'aaa', + 'AA Battery' => 'aa', + 'Abercrombie' => 'abercrombie', + 'Aberlour' => 'aberlour', + 'Accommodation' => 'accomodation', + 'Accurist' => 'accurist', + 'Ace Combat 7: Skies Unknown' => 'ace-combat-7', + 'Acer' => 'acer', + 'Acer Aspire' => 'acer-aspire', + 'Acer Laptop' => 'acer-laptop', + 'Acer PC Monitor' => 'acer-pc-monitor', + 'Acer Predator' => 'acer-predator', + 'Action Camera' => 'action-camera', + 'Action Figure & Playsets' => 'playsets', + 'Activewear' => 'sports-clothes', + 'Activia' => 'activia', + 'adidas' => 'adidas', + 'adidas Continental' => 'continental', + 'Adidas Gazelle' => 'gazelle', + 'Adidas Originals' => 'adidas-originals', + 'Adidas Samba' => 'samba', + 'Adidas Stan Smith' => 'stan-smith', + 'Adidas Superstar' => 'adidas-superstar', + 'Adidas Trainers' => 'adidas-shoes', + 'Adidas Ultraboost' => 'adidas-ultraboost', + 'Adidas ZX Flux' => 'adidas-zx-flux', + 'Adobe' => 'adobe', + 'Adobe Lightroom' => 'lightroom', + 'Adobe Photoshop' => 'photoshop', + 'Adult Products' => 'adult', + 'Advent Calendar' => 'advent-calendar', + 'Adventure Time' => 'adventure-time', + 'AEG' => 'aeg', + 'Aftershave' => 'aftershave', + 'Age Of Empires' => 'age-of-empires', + 'Air Bed' => 'air-bed', + 'Air Conditioner' => 'air-con', + 'Airer' => 'airer', + 'Airfix' => 'airfix', + 'Air Fryer' => 'air-fryer', + 'Airline' => 'airline', + 'Airport' => 'airport', + 'Airport Parking' => 'airport-parking', + 'Air Purifier' => 'air-purifier', + 'AirTag' => 'airtag', + 'Air Treatment' => 'air-treatment', + 'AKG' => 'akg', + 'Alarm Clock' => 'alarm-clock', + 'Alarm System' => 'alarm-system', + 'Alcatel' => 'alcatel', + 'Alcohol' => 'alcohol', + 'Alesis' => 'alesis', + 'Alien: Isolation' => 'alien-isolation', + 'Alienware' => 'alienware', + 'All-in-One PC' => 'all-in-one-pc', + 'All-in-One Printer' => 'all-in-one-printer', + 'Alloy Wheel' => 'alloy-wheels', + 'All Saints' => 'all-saints', + 'Almonds' => 'almonds', + 'Alpro' => 'alpro', + 'Alton Towers' => 'alton-towers', + 'Amazfit' => 'xiaomi-amazfit', + 'Amazfit Bip' => 'xiaomi-amazfit-bip', + 'Amazfit GTS' => 'amazfit-gts', + 'Amazfit Verge' => 'amazfit-verge', + 'Amazfit Verge Lite' => 'amazfit-verge-lite', + 'Amazfit Watch' => 'amazfit-watch', + 'Amazon Add On Item' => 'add-on-item', + 'Amazon Business' => 'amazon-business', + 'Amazon Echo' => 'amazon-echo', + 'Amazon Echo Dot' => 'amazon-echo-dot', + 'Amazon Echo Plus' => 'amazon-echo-plus', + 'Amazon Echo Show' => 'amazon-echo-show', + 'Amazon Echo Show 5' => 'echo-show-5', + 'Amazon Echo Show 8' => 'amazon-echo-show-8', + 'Amazon Echo Spot' => 'amazon-echo-spot', + 'Amazon Fire 7' => 'amazon-fire-7', + 'Amazon Fire HD 8' => 'amazon-fire-hd-7', + 'Amazon Fire HD 10 Tablet' => 'amazon-fire-hd-10', + 'Amazon Fire Tablet' => 'amazon-tablet', + 'Amazon Fire TV Cube' => 'fire-tv-cube', + 'Amazon Fire TV Stick' => 'amazon-fire-stick', + 'Amazon Pantry' => 'amazon-pantry', + 'Amazon Prime' => 'amazon-prime', + 'Amazon Prime Video' => 'amazon-video', + 'Amazon Warehouse' => 'amazon-warehouse', + 'AMD' => 'amd', + 'AMD Radeon' => 'radeon', + 'AMD Ryzen' => 'amd-ryzen', + 'AMD Ryzen 5 5600X' => 'amd-ryzen-5-5600x', + 'AMD Ryzen 7 5800X' => 'amd-ryzen-7-5800x', + 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x', + 'AMD Ryzen 9 5950X' => 'amd-ryzen-9-5950x', + 'Amex' => 'amex', + 'Amiibo' => 'amiibo', + 'Amplifier' => 'amplifier', + 'Anchor Butter' => 'anchor-butter', + 'Andrex' => 'andrex', + 'Android Apps' => 'android-app', + 'Android Smartphone' => 'android-smartphone', + 'Android Tablet' => 'android-tablet', + 'Angelcare' => 'angelcare', + 'Angle Grinder' => 'grinder', + 'Anglepoise' => 'anglepoise', + 'Angry Birds' => 'angry-birds', + 'Animal Crossing' => 'animal-crossing', + 'Anime' => 'anime', + 'Anker' => 'anker', + 'Ankle Boots' => 'ankle-boots', + 'Anno 1800' => 'anno-1800', + 'Anthem' => 'anthem', + 'Antibacterial Hand Gel' => 'hand-gel', + 'Antibacterial Wipes' => 'cleaning-wipes', + 'Antivirus' => 'antivirus', + 'Antler' => 'antler', + 'AOC' => 'aoc', + 'Apex Legends' => 'apex-legends', + 'A Plague Tale: Innocence' => 'a-plague-tale-innocence', + 'App' => 'app', + 'Apple' => 'apple', + 'Apple AirPods' => 'apple-airpods', + 'Apple Airpods 2' => 'airpods-2', + 'Apple Airpods Max' => 'airpods-max', + 'Apple Airpods Pro' => 'airpods-pro', + 'Apple EarPods' => 'earpods', + 'Apple Headphones' => 'apple-headphones', + 'Apple HomePod' => 'apple-homepod', + 'Apple HomePod mini' => 'apple-homepod-mini', + 'Apple Keyboard' => 'apple-keyboard', + 'Apple Pencil' => 'apple-pencil', + 'Apple TV' => 'apple-tv', + 'Apple TV 4K' => 'apple-tv-4k', + 'Apple Watch' => 'apple-watch', + 'Apple Watch 3' => 'apple-watch-3', + 'Apple Watch 4' => 'apple-watch-4', + 'Apple Watch 5' => 'apple-watch-5', + 'Apple Watch 6' => 'apple-watch-6', + 'Apple Watch SE' => 'apple-watch-se', + 'Apron' => 'apron', + 'Aquadoodle' => 'aquadoodle', + 'Aqua Optima' => 'aqua-optima', + 'Aquarium' => 'aquarium', + 'Aramis' => 'aramis', + 'Argan Oil' => 'argan-oil', + 'Ariel' => 'ariel', + 'Ark' => 'ark', + 'Armani' => 'armani', + 'Armchair' => 'armchair', + 'Armed Forces Discount' => 'armed-forces', + 'Arsenal F. C.' => 'arsenal', + 'Arts and Crafts' => 'craft', + 'Asics' => 'asics', + 'Ask' => 'ask', + 'ASRock' => 'asrock', + 'Assassin's Creed' => 'assassins-creed', + 'Assassin's Creed: Odyssey' => 'assassins-creed-odyssey', + 'Assassin's Creed: Origins' => 'assassins-creed-origins', + 'Assassin's Creed: Unity' => 'assassins-creed-unity', + 'Assassin's Creed: Valhalla' => 'assasins-creed-valhalla', + 'Astral Chain' => 'astral-chain', + 'ASTRO Gaming' => 'astro-gaming', + 'Astro Gaming A40' => 'astro-gaming-a40', + 'Astro Gaming A50' => 'astro-gaming-a50', + 'Asus' => 'asus', + 'ASUS Laptop' => 'asus-laptop', + 'ASUS Monitor' => 'asus-monitor', + 'ASUS ROG' => 'asus-rog', + 'Asus ROG Phone' => 'asus-rog-phone', + 'Asus ROG Phone 2' => 'asus-rog-phone-2', + 'ASUS Router' => 'asus-router', + 'Asus Smartphone' => 'asus-smartphone', + 'ASUS Vivobook' => 'asus-vivobook', + 'ASUS Zenbook' => 'zenbook', + 'Asus ZenFone 6' => 'asus-zenfone-6', + 'Atari' => 'atari', + 'Audi' => 'audi', + 'Audio & Hi-Fi' => 'audio', + 'Audio Accessories' => 'audio-accessories', + 'Audiobook' => 'audiobook', + 'Audio Technica' => 'audio-technica', + 'Aukey' => 'aukey', + 'Aussie' => 'aussie', + 'Autoglym' => 'autoglym', + 'Aveeno' => 'aveeno', + 'Avengers' => 'avengers', + 'AVG' => 'avg', + 'Aviva' => 'aviva', + 'Avon' => 'avon', + 'AV Receiver' => 'av-receiver', + 'Axe' => 'axe', + 'Baby Annabell' => 'baby-annabell', + 'Baby Bath' => 'baby-bath', + 'Baby Born' => 'baby-born', + 'Baby Bottle' => 'baby-bottles', + 'Baby Bouncer' => 'bouncer', + 'Baby Carrier' => 'baby-carrier', + 'Baby Clothes' => 'baby-clothes', + 'Baby Food' => 'baby-food', + 'Baby Gym' => 'baby-gym', + 'Baby Jogger' => 'baby-jogger', + 'Babyliss' => 'babyliss', + 'Baby Monitor' => 'baby-monitor', + 'Baby Shoes' => 'baby-shoes', + 'Baby Swing' => 'baby-swing', + 'Baby Walker' => 'baby-walker', + 'Baby Wipes' => 'wipes', + 'Bacardi' => 'bacardi', + 'Backpack' => 'backpack', + 'Back to the Future' => 'back-to-the-future', + 'Bacon' => 'bacon', + 'Badminton' => 'badminton', + 'Bag' => 'bag', + 'Bagless Vacuum Cleaner' => 'bagless-vacuum-cleaner', + 'Bahco' => 'bahco', + 'Baileys' => 'baileys', + 'Baked Beans' => 'baked-beans', + 'Bakery Products' => 'bakery-products', + 'Baking' => 'baking', + 'Ball Pit' => 'ball-pit', + 'Ballpoint Pen' => 'pen', + 'Band of Brothers' => 'band-of-brothers', + 'Bang & Olufsen' => 'bang-olufsen', + 'Bank' => 'bank', + 'Bank Account' => 'bank-account', + 'Banks & Credit Cards' => 'bank-credit-card', + 'Barbell' => 'barbell', + 'Barbie' => 'barbie', + 'Barbour' => 'barbour', + 'Barclaycard' => 'barclaycard', + 'Barclays' => 'barclays', + 'Barebones PC' => 'barebones', + 'bareMinerals' => 'bareminerals', + 'Barry M' => 'barry-m', + 'Bar Stools' => 'bar-stools', + 'Base Layer' => 'base-layer', + 'Basket' => 'basket', + 'Basketball' => 'basketball', + 'Basmati Rice' => 'basmati-rice', + 'Bath Mat' => 'bath-mat', + 'Bathroom Accessories' => 'bathroom', + 'Bathroom Cabinet' => 'bathroom-cabinet', + 'Bathroom Scale' => 'bathroom-scales', + 'Bathroom Tap' => 'tap', + 'Batman' => 'batman', + 'Battery' => 'battery', + 'Battleborn' => 'battleborn', + 'Battlefield' => 'battlefield', + 'Battlefield 1' => 'battlefield-1', + 'Battlefield 4' => 'battlefield-4', + 'Battlefield 5' => 'battlefield-5', + 'Battlestar Galactica' => 'battlestar-galactica', + 'Baylis & Harding' => 'baylis-and-harding', + 'Bayonetta' => 'bayonetta', + 'Bayonetta 2' => 'bayonetta-2', + 'Baywatch' => 'baywatch', + 'BB-8' => 'bb-8', + 'BBC' => 'bbc', + 'BBQ Food' => 'bbq', + 'BBQs and Grills' => 'grill', + 'Bean Bag' => 'bean-bag', + 'Beanie Hat' => 'beanie-hat', + 'Bean to Cup Machine' => 'bean-to-cup', + 'Beard Trimmer' => 'beard-trimmer', + 'Beats by Dre' => 'beats-by-dre', + 'Beats Solo 3' => 'beats-solo-3', + 'Beats Studio 3' => 'beats-studio-3', + 'Beauty' => 'beauty-care', + 'Beauty and the Beast' => 'beauty-and-the-beast', + 'Becks' => 'becks', + 'Bed' => 'bed', + 'Bedding' => 'bedding', + 'Bedding & Linens' => 'bedding-linens', + 'Bed Frame' => 'bed-frame', + 'Bedroom' => 'bedroom-furniture', + 'Beef' => 'beef', + 'Beer' => 'beer', + 'Beer Advent Calendar' => 'beer-advent-calendar', + 'Beko' => 'beko', + 'Belkin' => 'belkin', + 'Belstaff' => 'belstaff', + 'Belt' => 'belt', + 'BelVita' => 'belvita', + 'Ben & Jerry's' => 'ben-jerrys', + 'Benefit Cosmetics' => 'benefit-cosmetics', + 'BenQ' => 'benq', + 'BenQ Monitor' => 'benq-monitor', + 'Ben Sherman' => 'ben-sherman', + 'BeoPlay Headphones' => 'beoplay-headphones', + 'Beoplay Speakers' => 'beoplay', + 'Berghaus' => 'berghaus', + 'Bestway' => 'bestway', + 'Betting' => 'betting', + 'Beyerdynamic' => 'beyerdynamic', + 'Bic' => 'bic', + 'Bike' => 'bike', + 'Bike Accessories' => 'bike-accessories', + 'Bike Brake' => 'brakes', + 'Bike Computer' => 'bike-computer', + 'Bike Helmet' => 'bicycle-helmet', + 'Bike Inner Tube' => 'inner-tube', + 'Bike Lights' => 'bike-lights', + 'Bike Lock' => 'bike-lock', + 'Bike Parts' => 'bike-parts', + 'Bike Pump' => 'bike-pump', + 'Biker Equipment' => 'biker-equipment', + 'Bike Saddle' => 'saddle', + 'Biking & Urban Sports' => 'biking-urban-sports', + 'Bikini' => 'bikini', + 'Billabong' => 'billabong', + 'Bin' => 'bin', + 'Binatone' => 'binatone', + 'Bingo' => 'bingo', + 'Binoculars' => 'binoculars', + 'Bio Oil' => 'bio-oil', + 'Bioshock' => 'bioshock', + 'Birds Eye' => 'birds-eye', + 'Birkenstock' => 'birkenstock', + 'Biscuits' => 'biscuits', + 'Bissell' => 'bissell', + 'Bistro Set' => 'bistro-set', + 'Bitdefender' => 'bitdefender', + 'Black & Decker' => 'black-decker', + 'Blackberry Smartphone' => 'blackberry', + 'Blanket' => 'blanket', + 'Blaupunkt' => 'blaupunkt', + 'Blazer' => 'blazer', + 'Bleach' => 'bleach', + 'Blended Malt' => 'malt', + 'Blender' => 'blender', + 'Blinds' => 'blinds', + 'Blink XT2 Smart Security Camera' => 'blink-xt2', + 'Blizzard' => 'blizzard', + 'Blood & Truth' => 'blood-and-truth', + 'Bloodborne' => 'bloodborne', + 'Blood Pressure Monitor' => 'blood-pressure', + 'Blu-ray' => 'blu-ray', + 'Blu-ray Player' => 'blu-ray-player', + 'Bluetooth Headphones' => 'bluetooth-headphones', + 'Bluetooth Speaker' => 'bluetooth-speaker', + 'BMW' => 'bmw', + 'BMW Mini Cooper' => 'mini-cooper', + 'BMX' => 'bmx', + 'Board Game' => 'board-game', + 'Boardman' => 'boardman', + 'Boat Shoes' => 'boat-shoes', + 'Bodum' => 'bodum', + 'Bogof' => 'bogof', + 'Boiler' => 'boiler', + 'Bold' => 'bold', + 'Bombay Sapphire' => 'bombay-sapphire', + 'Bomber Jacket' => 'bomber-jacket', + 'Bonne Maman' => 'bonne-maman', + 'Bonsai' => 'bonsai', + 'Book' => 'book', + 'Bookcase' => 'bookcase', + 'Books & Magazines' => 'books-magazines', + 'Booster Seat' => 'booster-seat', + 'Boots' => 'boots', + 'Borderlands' => 'borderlands', + 'Borderlands 3' => 'borderlands-3', + 'Bosch' => 'bosch', + 'Bosch Dishwasher' => 'bosch-dishwasher', + 'Bosch Drill' => 'bosch-drill', + 'Bosch Fridge' => 'bosch-fridge', + 'Bosch Rotak' => 'rotak', + 'Bosch Washing Machine' => 'bosch-washing-machine', + 'Bose' => 'bose', + 'Bose Headphones' => 'bose-headphones', + 'Bose Noise Cancelling Headphones 700' => 'bose-headphones-700', + 'Bose QuietComfort' => 'bose-quietcomfort', + 'Bose QuietComfort 35 II' => 'bose-quietcomfort-35-ii', + 'Bose SoundLink' => 'bose-soundlink', + 'Bose SoundLink Around-Ear II' => 'bose-soundlink-2', + 'Bose SoundTouch' => 'bose-soundtouch', + 'BOSS' => 'hugo-boss', + 'Boss Bottled' => 'boss-bottled', + 'Bouncy Castle' => 'bouncy-castle', + 'Bourbon' => 'bourbon', + 'Bourjois' => 'bourjois', + 'Bowers & Wilkins' => 'bowers-wilkins', + 'Bowling' => 'bowling', + 'Bowmore' => 'bowmore', + 'Boxers' => 'boxers', + 'Boxing' => 'boxing', + 'Boxing Gloves' => 'boxing-gloves', + 'Boy's Clothes' => 'clothes-for-boys', + 'Bra' => 'bra', + 'Brabantia' => 'brabantia', + 'Bracelet' => 'bracelet', + 'Brands' => 'brand', + 'Brandy' => 'brandy', + 'Branston' => 'branston', + 'Branston Beans' => 'branston-beans', + 'Braun' => 'braun', + 'Braun Series 3' => 'braun-series-3', + 'Braun Series 5' => 'braun-series-5', + 'Braun Series 7' => 'braun-series-7', + 'Braun Series 9' => 'braun-series-9', + 'Braun Shaver' => 'braun-shaver', + 'Bread' => 'bread', + 'Breadmaker' => 'breadmaker', + 'Breakdown Cover' => 'breakdown', + 'Breaking Bad' => 'breaking-bad', + 'Breast Pump' => 'breast-pump', + 'Breville' => 'breville', + 'Breville Blend Active' => 'blendactive', + 'Brewdog' => 'brewdog', + 'Bridge Camera' => 'bridge-camera', + 'Briefcase' => 'briefcase', + 'Brita' => 'brita', + 'Britax' => 'britax', + 'British Airways' => 'british-airways', + 'Broadband' => 'broadband', + 'Broadband & Phone Contracts' => 'broadband-phone-service', + 'Brogues' => 'brogues', + 'Brother' => 'brother', + 'Brother Printer' => 'brother-printer', + 'Brownie' => 'brownie', + 'BT' => 'bt', + 'BT Sport' => 'bt-sport', + 'Budweiser' => 'budweiser', + 'Buffalo' => 'buffalo', + 'Bugaboo' => 'bugaboo', + 'Buggy' => 'buggy', + 'Build-A-Bear' => 'build-a-bear', + 'Bulb' => 'bulbs', + 'Bulletstorm' => 'bulletstorm', + 'Bulmers' => 'bulmers', + 'Bulova' => 'bulova', + 'Burberry' => 'burberry', + 'Burger' => 'burger', + 'Burnout Paradise' => 'burnout-paradise', + 'Burt's Bees' => 'burts-bees', + 'Bus and Coach Ticket' => 'bus', + 'Bush' => 'bush', + 'Bushmills' => 'bushmills', + 'Butter' => 'butter', + 'Buying From Abroad' => 'buying-from-abroad', + 'Bvlgari' => 'bvlgari', + 'Cabin Case' => 'cabin-case', + 'Cabinet' => 'cabinet', + 'Cable Reel' => 'cable-reel', + 'Cables' => 'cables', + 'Cadbury's' => 'cadbury', + 'Café Rouge' => 'cafe-rouge', + 'Cafetière' => 'cafetiere', + 'Caffè Nero' => 'cafe-nero', + 'Cake' => 'cake', + 'Calculator' => 'calculator', + 'Calendar' => 'calendar', + 'Call of Duty' => 'call-of-duty', + 'Call of Duty: Black Ops' => 'black-ops', + 'Call of Duty: Black Ops 3' => 'black-ops-3', + 'Call of Duty: Black Ops 4' => 'black-ops-4', + 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war', + 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare', + 'Call of Duty: Modern Warfare' => 'modern-warfare', + 'Call of Duty: WW2' => 'call-of-duty-ww2', + 'Calpol' => 'calpol', + 'Calvin Klein' => 'calvin-klein', + 'Camcorder' => 'camcorder', + 'Camelbak' => 'camelbak', + 'Camera' => 'camera', + 'Camera Accessories' => 'camera-accessories', + 'Camera Bag' => 'camera-bag', + 'Camera Lens' => 'lens', + 'Camping' => 'camping', + 'Campingaz' => 'campingaz', + 'Candle' => 'candle', + 'Cannondale' => 'cannondale', + 'Canon' => 'canon', + 'Canon Camera' => 'canon-camera', + 'Canon EOS' => 'canon-eos', + 'Canon Lens' => 'canon-lens', + 'Canon Pixma' => 'canon-pixma', + 'Canon PowerShot' => 'canon-powershot', + 'Canon PowerShot SX430 IS' => 'canon-powershot-sx430-is', + 'Canon Printer' => 'canon-printer', + 'Canterbury' => 'canterbury', + 'Canton' => 'canton', + 'Canvas Print' => 'canvas-print', + 'Cap' => 'cap', + 'Capsule Machine' => 'capsule-machine', + 'Captain America' => 'captain-america', + 'Captain Morgan' => 'captain-morgan', + 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker', + 'Car' => 'car', + 'Car & Motorcycle' => 'car-motorcycle', + 'Car Accessories' => 'car-accessories', + 'Caravan' => 'caravan', + 'Car Battery' => 'car-battery', + 'Carbon Monoxide Detector' => 'carbon-monoxide', + 'Car Care' => 'car-care', + 'Car Charger' => 'car-charger', + 'Cardhu' => 'cardhu', + 'Cardigan' => 'cardigan', + 'Card Reader' => 'card-reader', + 'Carex' => 'carex', + 'Carhartt' => 'carhartt', + 'Car Hire' => 'car-hire', + 'Car Insurance' => 'car-insurance', + 'Car Leasing' => 'car-lease', + 'Carling' => 'carling', + 'Car Lock' => 'lock', + 'Carlsberg' => 'carlsberg', + 'Car Mats' => 'car-mats', + 'Carolina Herrera' => 'carolina-herrera', + 'Car Parts' => 'car-parts', + 'Carpet' => 'carpet', + 'Carpet Cleaner' => 'carpet-cleaner', + 'CarPlan' => 'carplan', + 'Car Polish' => 'car-polish', + 'Carrera Bikes' => 'carrera', + 'Car Seat' => 'car-seat', + 'Car Service' => 'car-service', + 'Car Stereo' => 'car-stereo', + 'Car Wash' => 'car-wash', + 'Car Wax' => 'car-wax', + 'Casio' => 'casio', + 'Casio Eco-Drive' => 'eco-drive', + 'Casio Edifice' => 'edifice', + 'Casio G-Shock' => 'g-shock', + 'Casserole' => 'casserole', + 'Cast Iron Pots and Pans' => 'cast-iron', + 'Castrol' => 'castrol', + 'Caterpillar' => 'caterpillar', + 'Cat Flap' => 'cat-flap', + 'Cat Food' => 'cat-food', + 'Cath Kidston' => 'cath-kidston', + 'Cat Supplies' => 'cat-supplies', + 'CCTV' => 'cctv', + 'CD' => 'cd', + 'CD Player' => 'cd-player', + 'Ceiling Light' => 'ceiling-light', + 'Celebrations' => 'celebrations', + 'Cereal' => 'cereal', + 'Cetirizine' => 'cetirizine', + 'Chad Valley' => 'chad-valley', + 'Chainsaw' => 'chainsaw', + 'Champagne' => 'champagne', + 'Champneys' => 'champneys', + 'Chanel' => 'chanel', + 'Chanel Coco Mademoiselle' => 'coco-mademoiselle', + 'Changing Bag' => 'changing-bag', + 'Channel 4' => 'channel-4', + 'Charger' => 'charger', + 'Cheese' => 'cheese', + 'Chelsea Boots' => 'chelsea-boots', + 'Chelsea F. C.' => 'chelsea', + 'Chess' => 'chess', + 'Chessington' => 'chessington', + 'Chest Freezer' => 'chest-freezer', + 'Chest of Drawers' => 'chest-of-drawers', + 'Chicco' => 'chicco', + 'Chicken' => 'chicken', + 'Childcare' => 'baby', + 'Children's Books' => 'childrens-books', + 'Chino' => 'chino', + 'Chisel' => 'chisel', + 'Chloe' => 'chloe', + 'Chocolate' => 'chocolate', + 'Chocolate Advent Calendar' => 'chocolate-advent-calendar', + 'Chopper' => 'chopper', + 'Chopping Board' => 'chopping-board', + 'Christmas Card' => 'christmas-card', + 'Christmas Decoration' => 'christmas-decorations', + 'Christmas Gift' => 'christmas-gifts', + 'Christmas Jumper' => 'christmas-jumper', + 'Christmas Lights' => 'christmas-lights', + 'Christmas Stocking Fillers' => 'christmas-stocking-fillers', + 'Christmas Toys' => 'christmas-toys', + 'Christmas Tree' => 'christmas-tree', + 'Chromebook' => 'chromebook', + 'Chromecast' => 'chromecast', + 'Chromecast Ultra' => 'chromecast-ultra', + 'Chromecast with Google TV' => 'chromecast-google-tv', + 'Chronograph' => 'chronograph', + 'Chupa Chups' => 'chupa-chups', + 'Chuwi' => 'chuwi', + 'Cider' => 'cider', + 'Cinema' => 'cinema', + 'Cineworld' => 'cineworld', + 'Circular Saw' => 'circular-saw', + 'Circulon' => 'circulon', + 'Ciroc' => 'ciroc', + 'Cities Skylines' => 'cities-skylines', + 'Citizen' => 'citizen', + 'Citroen' => 'citroen', + 'City Break' => 'city-breaks', + 'Civilization' => 'civilization', + 'Clarins' => 'clarins', + 'Clarks' => 'clarks', + 'Clearance' => 'clearance', + 'Climbing' => 'climbing', + 'Climbing Frame' => 'climbing-frame', + 'Clinique' => 'clinique', + 'Clothes' => 'clothes', + 'Cloud Service' => 'cloud', + 'Clutch Bag' => 'clutch', + 'Coat' => 'coat', + 'Coca Cola' => 'coke', + 'Cocktail' => 'cocktail', + 'Coconut Oil' => 'coconut', + 'Coffee' => 'coffee', + 'Coffee Beans' => 'coffee-beans', + 'Coffee Machine' => 'coffee-machine', + 'Coffee Pods' => 'coffee-pods', + 'Coffee Table' => 'coffee-table', + 'Cognac' => 'cognac', + 'Cola' => 'cola', + 'Coleman' => 'coleman', + 'Colgate' => 'colgate', + 'Combi Drill' => 'combi', + 'Comfort' => 'comfort', + 'Comic' => 'comic', + 'Command & Conquer' => 'command-and-conquer', + 'Compact Camera' => 'compact-camera', + 'Compact Flash' => 'compact-flash', + 'Competitions' => 'competitions', + 'Compost' => 'compost', + 'Compressor' => 'compressor', + 'Computer Accessories' => 'computer-accessories', + 'Computers & Tablets' => 'computers', + 'Concert' => 'concert', + 'Condé Nast' => 'conde-nast', + 'Conditioner' => 'conditioner', + 'Condom' => 'condom', + 'Connectors' => 'connectors', + 'Contact Lenses' => 'contact-lenses', + 'Contents Insurance' => 'contents-insurance', + 'Controller' => 'controller', + 'Converse' => 'converse', + 'Converse Chuck Taylor' => 'chuck-taylor', + 'Cooker' => 'cooker', + 'Cooking Oil' => 'cooking-oil', + 'Cookware' => 'cooking', + 'Cookware Set' => 'cookware-set', + 'Cookworks' => 'cookworks', + 'Cool Box' => 'cool-box', + 'Coors Light' => 'coors-light', + 'Cordless Drill' => 'cordless-drill', + 'Cordless Phone' => 'cordless-phone', + 'Cornetto' => 'cornetto', + 'Corona Beer' => 'corona', + 'Corsair' => 'corsair', + 'Cosatto' => 'cosatto', + 'Costa Coffee' => 'costa-coffee', + 'Costume' => 'costume', + 'Cot' => 'cot', + 'Counter Strike' => 'counter-strike', + 'Courses and Training' => 'education', + 'Cow & Gate' => 'cow-and-gate', + 'Cozy Coupe' => 'cozy-coupe', + 'CPU' => 'cpu', + 'CPU Cooler' => 'cpu-cooler', + 'Craghoppers' => 'craghoppers', + 'Crash Bandicoot' => 'crash-bandicoot', + 'Crash Team Racing Nitro-Fueled' => 'crash-team-racing-nitro-fueled', + 'Crayola' => 'crayola', + 'Creatine' => 'creatine', + 'Credit Card' => 'credit-card', + 'Creme Egg' => 'creme-egg', + 'Cricket' => 'cricket', + 'Crisps' => 'crisps', + 'Crocs' => 'crocs', + 'Cross Trainer' => 'cross-trainer', + 'Crown Paint' => 'crown', + 'Crucial' => 'crucial', + 'Cruelty Free Makeup' => 'cruelty-free-makeup', + 'Cruises' => 'cruise', + 'Cube Bikes' => 'cube', + 'Cubot' => 'cubot', + 'Cufflinks' => 'cufflinks', + 'Culture & Leisure' => 'entertainment', + 'Cuphead' => 'cuphead', + 'Cuprinol' => 'cuprinol', + 'Curling Wand' => 'curling-wand', + 'Curtain' => 'curtain', + 'Cushelle' => 'cushelle', + 'Cushion' => 'cushion', + 'Cutlery' => 'cutlery', + 'CyberLink' => 'cyberlink', + 'Cyberpunk 2077' => 'cyberpunk-2077', + 'Cybex' => 'cybex', + 'Cycling' => 'cycling', + 'Cycling Jacket' => 'cycling-jacket', + 'D-Link' => 'd-link', + 'DAB Radio' => 'dab-radio', + 'Dacia' => 'dacia', + 'Daily Mail' => 'daily-mail', + 'Dairy Milk' => 'dairy-milk', + 'Darksiders' => 'darksiders', + 'Dark Souls' => 'dark-souls', + 'Dark Souls 3' => 'dark-souls-3', + 'Dartboard' => 'dartboard', + 'Darts' => 'darts', + 'Dash Cam' => 'dash-cam', + 'Data Storage' => 'storage', + 'Davidoff' => 'davidoff', + 'Days Gone' => 'days-gone', + 'Days Out' => 'days-out', + 'Daz' => 'daz', + 'DC Comic' => 'dc', + 'DDR3' => 'ddr3', + 'DDR4' => 'ddr4', + 'Dead Island' => 'dead-island', + 'Dead or Alive 6' => 'dead-or-alive-6', + 'Deadpool' => 'deadpool', + 'Dead Rising' => 'dead-rising', + 'Death Stranding' => 'death-stranding', + 'Deezer' => 'deezer', + 'Dehumidifier' => 'dehumidifier', + 'Dell' => 'dell', + 'Dell Laptop' => 'dell-laptop', + 'Dell Monitor' => 'dell-monitor', + 'Dell XPS' => 'xps', + 'Delonghi' => 'delonghi', + 'Demon's Souls' => 'demon-souls', + 'Denby' => 'denby', + 'Denon' => 'denon', + 'Deodorant' => 'deodorant', + 'Desk' => 'desk', + 'Desperados Beer' => 'desperados', + 'Despicable Me' => 'despicable-me', + 'Destiny' => 'destiny', + 'Destiny 2' => 'destiny-2', + 'Detergent' => 'detergent', + 'Detroit: Become Human' => 'detroit-become-human', + 'Dettol' => 'dettol', + 'Deus Ex' => 'deus-ex', + 'Deus Ex: Mankind Divided' => 'deus-ex-mankind-divided', + 'Development Boards' => 'development-boards', + 'Devil May Cry 5' => 'devil-may-cry-5', + 'DeWalt' => 'dewalt', + 'DFDS' => 'dfds', + 'Diablo 3' => 'diablo-3', + 'Diary' => 'diary', + 'Dickies' => 'dickies', + 'Diesel' => 'diesel', + 'Diet' => 'diet', + 'Diggerland' => 'diggerland', + 'Digihome' => 'digihome', + 'Digimon' => 'digimon', + 'Digital Camera' => 'digital-camera', + 'Digital Watch' => 'digital-watch', + 'Dildo' => 'dildo', + 'Dimplex' => 'dimplex', + 'Dining Room' => 'dining-room', + 'Dining Room Chair' => 'chair', + 'Dining Set' => 'dining-set', + 'Dining Table' => 'dining-table', + 'Dinner Plate' => 'plates', + 'Dinner Set' => 'dinner-set', + 'Dinosaur' => 'dinosaur', + 'Dior' => 'dior', + 'Dior Sauvage' => 'dior-sauvage', + 'Dirt' => 'dirt', + 'Dirt 4' => 'dirt-4', + 'DIRT 5' => 'dirt-5', + 'Dirt Rally 2.0' => 'dirt-rally-2', + 'Disaronno' => 'disaronno', + 'Discord Nitro' => 'discord-nitro', + 'Disgaea' => 'disgaea', + 'Dishonored' => 'dishonored', + 'Dishonored 2' => 'dishonored-2', + 'Dishwasher' => 'dishwasher', + 'Dishwasher Tablets' => 'dishwasher-tablets', + 'Disinfectants' => 'disinfectants', + 'Disney' => 'disney', + 'Disney's Cars' => 'disney-cars', + 'Disney's Frozen' => 'disney-frozen', + 'Disney+' => 'disney-plus', + 'Disney Infinity' => 'disney-infinity', + 'Disneyland' => 'disneyland', + 'Disney Princess' => 'disney-princess', + 'Disney Tsum Tsum' => 'tsum-tsum', + 'Disney World' => 'disney-world', + 'Divan' => 'divan', + 'DIY' => 'diy', + 'DJ Equipment' => 'dj', + 'DJI Phantom' => 'dji-phantom', + 'DKNY' => 'dkny', + 'Doctor Who' => 'doctor-who', + 'Dog Bed' => 'dog-bed', + 'Dog Food' => 'dog-food', + 'Dog Supplies' => 'dog', + 'Dolce & Gabbana' => 'dolce', + 'Dolce Gusto' => 'dolce-gusto', + 'Dolce Gusto Coffee Machine' => 'dolce-gusto-coffee-machine', + 'Doll' => 'doll', + 'Dolls House' => 'dolls-house', + 'Domain Service' => 'domain', + 'Doogee' => 'doogee', + 'Doom' => 'doom', + 'Door' => 'door', + 'Doorbell' => 'doorbell', + 'Door Handles' => 'door-handles', + 'Doormat' => 'doormat', + 'Doritos' => 'doritos', + 'Dove' => 'dove', + 'Down Jacket' => 'down-jacket', + 'Downton Abbey' => 'downton-abbey', + 'Dr. Martens' => 'dr-martens', + 'Dragon Age' => 'dragon-age', + 'Dragon Ball' => 'dragon-ball', + 'Dragon Ball: FighterZ' => 'dragon-ball-fighterz', + 'Dragon Quest' => 'dragon-quest', + 'Dragon Quest Builders' => 'dragon-quest-builders', + 'Dragon Quest Builders 2' => 'dragon-quest-builders-2', + 'Dragon Quest XI: Echoes of an Elusive Age' => 'dragon-quest-xi', + 'Draper' => 'draper', + 'Drayton Manor' => 'drayton-manor', + 'Dreame T20' => 'dreame-t20', + 'Dreame V9' => 'dreame-v9', + 'Dreame V9P' => 'dreame-v9p', + 'Dreame V10' => 'dreame-v10', + 'Dreame V11' => 'dreame-v11', + 'Dreame Vacuum Cleaner' => 'xiaomi-vacuum-cleaner', + 'Dremel' => 'dremel', + 'Dress' => 'dress', + 'Dressing Gown' => 'dressing-gown', + 'Drill' => 'drill', + 'Drill Driver' => 'driver', + 'Drinks' => 'drinks', + 'Driveclub' => 'driveclub', + 'Driving Lessons' => 'driving-lessons', + 'Drone' => 'drone', + 'Dryer' => 'dryer', + 'DSLR Camera' => 'dslr', + 'Dual Fuel Cooker' => 'dual-fuel', + 'Dualit' => 'dualit', + 'Dual Sim' => 'sim', + 'Dulux' => 'dulux', + 'Duracell' => 'duracell', + 'Durex' => 'durex', + 'Duvet' => 'duvet', + 'DVD' => 'dvd', + 'DVD Player' => 'dvd-player', + 'Dying Light' => 'dying-light', + 'Dymo' => 'dymo', + 'Dyson' => 'dyson', + 'Dyson Supersonic' => 'dyson-supersonic', + 'Dyson V6' => 'dyson-v6', + 'Dyson V7' => 'dyson-v7', + 'Dyson V8' => 'dyson-v8', + 'Dyson V10' => 'dyson-v10', + 'Dyson V11' => 'dyson-v11', + 'Dyson Vacuum Cleaner' => 'dyson-vacuum-cleaner', + 'e-Reader' => 'ereader', + 'EA' => 'ea', + 'EA Access' => 'ea-access', + 'Earphones' => 'earphones', + 'Earrings' => 'earrings', + 'EA Sports' => 'ea-sports', + 'EA Sports UFC' => 'ufc', + 'Easter Eggs' => 'egg', + 'Eastpak' => 'eastpak', + 'eBook' => 'ebook', + 'Ecovacs' => 'ecovacs', + 'Ecover' => 'ecover', + 'Educational Toys' => 'educational-toys', + 'EE' => 'ee', + 'eFootball PES 2021' => 'pes-2021', + 'ELC Happyland' => 'happyland', + 'Electrical Accessories' => 'electrical-accessories', + 'Electric Bike' => 'electric-bike', + 'Electric Blanket' => 'electric-blanket', + 'Electric Cooker' => 'electric-cooker', + 'Electric Fires' => 'electric-fire', + 'Electric Scooter' => 'electric-scooter', + 'Electric Shower' => 'electric-shower', + 'Electric Toothbrush' => 'electric-toothbrush', + 'Electronic Accessories' => 'electronics-accessories', + 'Electronics' => 'electronics', + 'Elemis' => 'elemis', + 'Elephone' => 'elephone', + 'Elgato' => 'elgato', + 'Elite Dangerous' => 'elite-dangerous', + 'Elizabeth Arden' => 'elizabeth-arden', + 'Emirates' => 'emirates', + 'Endura' => 'endura', + 'Eneloop' => 'eneloop', + 'Energizer' => 'energizer', + 'Energy' => 'energy', + 'Energy, Heating & Gas' => 'energy-heating-gas', + 'Energy Drinks' => 'energy-drinks', + 'Engine Oil' => 'engine-oil', + 'Epilator' => 'epilator', + 'Epson' => 'epson', + 'Epson Printer' => 'epson-printer', + 'Espresso' => 'espresso', + 'Espresso Machine' => 'espresso-machine', + 'Esprit' => 'esprit', + 'Estée Lauder' => 'estee-lauder', + 'Ethernet' => 'ethernet', + 'Etnies' => 'etnies', + 'Eurostar Ticket' => 'eurostar', + 'Eurotunnel' => 'eurotunnel', + 'Everton F. C.' => 'everton', + 'EVGA' => 'evga', + 'Evian' => 'evian', + 'Exercise Equipment' => 'exercise-equipment', + 'Exercise Weights' => 'weight', + 'Extension Lead' => 'extension-lead', + 'External Hard Drive' => 'external-hard-drive', + 'F1' => 'formula-one', + 'F1 2017' => 'f1-2017', + 'F1 2018' => 'f1-2018', + 'F1 2019' => 'f1-2019', + 'F1 2020' => 'f1-2020', + 'Fabric Conditioner' => 'fabric-conditioner', + 'Face Cream' => 'face-cream', + 'Face Mask' => 'face-mask', + 'Fairy' => 'fairy', + 'Fairy Light' => 'fairy-light', + 'Fallout' => 'fallout', + 'Fallout 4' => 'fallout-4', + 'Fallout 76' => 'fallout-76', + 'Family & Kids' => 'kids', + 'Family Break' => 'family-break', + 'Family Guy' => 'family-guy', + 'Famous Grouse' => 'famous-grouse', + 'Fancy Dress' => 'fancy-dress', + 'Fans' => 'fan', + 'Fanta' => 'fanta', + 'Far Cry' => 'far-cry', + 'Far Cry 4' => 'far-cry-4', + 'Far Cry 5' => 'far-cry-5', + 'Far Cry New Dawn' => 'far-cry-new-dawn', + 'Far Cry Primal' => 'far-cry-primal', + 'Farming Simulator' => 'farming-simulator', + 'Fashion & Accessories' => 'fashion', + 'Fashion Accessories' => 'fashion-accessories', + 'Fashion for Men' => 'mens-clothing', + 'Fashion for Women' => 'womens-clothes', + 'Fast and Furious' => 'fast-and-furious', + 'Father's Day' => 'fathers-day', + 'FatMax' => 'fatmax', + 'FC Barcelona' => 'fc-barcelona', + 'Felix' => 'felix', + 'Fence' => 'fence', + 'Fender Guitar' => 'fender', + 'Ferrero Rocher' => 'ferrero-rocher', + 'Ferry' => 'ferry', + 'Festival' => 'festival', + 'Fever Thermometer' => 'thermometer', + 'Fiat' => 'fiat', + 'Fidget Spinner' => 'spinner', + 'FIFA' => 'fifa', + 'FIFA 17' => 'fifa-17', + 'FIFA 18' => 'fifa-18', + 'FIFA 19' => 'fifa-19', + 'FIFA 20' => 'fifa-20', + 'FIFA 21' => 'fifa-21', + 'FightStick' => 'fightstick', + 'Figures' => 'figures', + 'Fila Trainers' => 'fila-trainers', + 'Filing Cabinet' => 'filing-cabinet', + 'Final Fantasy' => 'final-fantasy', + 'Final Fantasy 15' => 'final-fantasy-15', + 'Finance & Insurance' => 'personal-finance', + 'Finish' => 'finish', + 'Finlux' => 'finlux', + 'Fiorelli' => 'fiorelli', + 'Fire Emblem' => 'fire-emblem', + 'Fire Pit' => 'fire-pit', + 'Fireplace' => 'fireplace', + 'Firewall: Zero Hour' => 'firewall-zero-hour', + 'First Aid' => 'first-aid', + 'Fish & Seafood' => 'fish-and-seafood', + 'Fish and Aquatic Pet Supplies' => 'fish', + 'Fisher Price' => 'fisher-price', + 'Fisher Price Imaginext' => 'imaginext', + 'Fisher Price Jumperoo' => 'jumperoo', + 'Fisher Price Little People' => 'little-people', + 'Fishing' => 'fishing', + 'Fiskars' => 'fiskars', + 'Fitbit' => 'fitbit', + 'Fitbit Alta' => 'fitbit-alta', + 'Fitbit Blaze' => 'fitbit-blaze', + 'Fitbit Charge 2' => 'fitbit-charge-2', + 'Fitbit Inspire' => 'fitbit-inspire', + 'Fitbit Versa' => 'fitbit-versa', + 'Fitness & Running' => 'fitness', + 'Fitness App' => 'fitness-app', + 'Fitness Tracker' => 'fitness-tracker', + 'Flamingo Land' => 'flamingo-land', + 'Flea Treatment' => 'flea', + 'Fleece Clothing' => 'fleece', + 'Flights' => 'flight', + 'Flip Flops' => 'flip-flops', + 'Floodlight' => 'floodlight', + 'Flooring' => 'flooring', + 'Flowers' => 'flowers', + 'Flymo' => 'flymo', + 'FM Transmitter' => 'fm-transmitter', + 'Food' => 'food', + 'Food Containers' => 'food-containers', + 'Food Processor' => 'food-processor', + 'Food Server' => 'food-server', + 'Football' => 'football', + 'Football Boots' => 'football-boots', + 'Football Manager' => 'football-manager', + 'Football Matches' => 'football-matches', + 'Football Shirt' => 'football-shirt', + 'Foot Pump' => 'foot-pump', + 'Ford' => 'ford', + 'For Honor' => 'for-honor', + 'Fortnite' => 'fortnite', + 'Fortnite: Darkfire' => 'fortnite-darkfire', + 'Forza' => 'forza', + 'Forza 7' => 'forza-7', + 'Forza Horizon' => 'forza-horizon', + 'Forza Horizon 3' => 'forza-horizon-3', + 'Forza Horizon 4' => 'forza-horizon-4', + 'Forza Motorsport' => 'forza-motorsport', + 'Foscam' => 'foscam', + 'Fossil' => 'fossil', + 'Foster's' => 'fosters', + 'Foundation' => 'foundation', + 'Fountain Pen' => 'fountain-pen', + 'Fred Perry' => 'fred-perry', + 'Freesat' => 'freesat', + 'Freeview' => 'freeview', + 'Freezer' => 'freezer', + 'Fridge' => 'fridge', + 'Fridge Freezer' => 'fridge-freezer', + 'Frontline' => 'frontline', + 'Frozen Food' => 'frozen', + 'Fruit' => 'fruit', + 'Fruit and Vegetables' => 'fruit-and-vegetable', + 'Fruit of the Loom' => 'fruit-of-the-loom', + 'Fryer' => 'fryer', + 'Frying Pan' => 'frying-pan', + 'Fujifilm' => 'fuji', + 'Fujitsu' => 'fujitsu', + 'Funko Pop' => 'funko-pop', + 'Furby' => 'furby', + 'Furniture' => 'furniture', + 'G-Star' => 'g-star', + 'G-Sync Monitor' => 'g-sync', + 'Gaggia' => 'gaggia', + 'Gambling' => 'gambling', + 'Game App' => 'game-app', + 'Game of Thrones' => 'game-of-thrones', + 'Games & Board Games' => 'board-games', + 'Games Consoles' => 'console', + 'Gaming' => 'gaming', + 'Gaming Accessories' => 'gaming-accessories', + 'Gaming Chair' => 'gaming-chair', + 'Gaming Headset' => 'gaming-headset', + 'Gaming Keyboard' => 'gaming-keyboard', + 'Gaming Laptop' => 'gaming-laptop', + 'Gaming Monitor' => 'gaming-monitor', + 'Gaming Mouse' => 'gaming-mouse', + 'Gaming PC' => 'gaming-pc', + 'Gant' => 'gant', + 'Garage' => 'garage', + 'Garage & Service' => 'garage-service', + 'Garden' => 'garden', + 'Garden & Do It Yourself' => 'garden-diy', + 'Garden Furniture' => 'garden-furniture', + 'Gardening' => 'gardening', + 'Garden Storage' => 'garden-storage', + 'Garden Table' => 'table', + 'Garmin' => 'garmin', + 'Garmin Fenix' => 'garmin-fenix', + 'Garmin Fenix 6' => 'garmin-fenix-6', + 'Garmin Fenix 6 Pro' => 'garmin-fenix-6-pro', + 'Garmin Forerunner' => 'garmin-forerunner', + 'Garmin Vivoactive' => 'garmin-vivoactive', + 'Garmin Watch' => 'garmin-watch', + 'Garnier' => 'garnier', + 'Gas' => 'gas', + 'Gas Canister' => 'butane', + 'Gas Cooker' => 'gas-cooker', + 'Gatwick' => 'gatwick', + 'Gazebo' => 'gazebo', + 'GBK' => 'gbk', + 'Gears 5' => 'gears-5', + 'Gears of War' => 'gears-of-war', + 'Gears of War 4' => 'gears-of-war-4', + 'George Foreman' => 'george-foreman', + 'Geox' => 'geox', + 'GHD' => 'ghd', + 'Ghostbusters' => 'ghostbusters', + 'Ghostbusters: The Video Game Remastered' => 'ghostbusters-the-video-game', + 'Ghost of Tsushima' => 'ghost-of-tsushima', + 'Gibson Guitar' => 'gibson', + 'giffgaff' => 'giffgaff', + 'Gift Card' => 'gift-card', + 'Gift Hamper' => 'hamper', + 'Gifts' => 'gifts', + 'Gift Set' => 'gift-set', + 'GIGABYTE' => 'gigabyte', + 'Gigaset' => 'gigaset', + 'Gilet' => 'gilet', + 'Gillette Fusion' => 'fusion', + 'Gillette Mach3' => 'mach-3', + 'Gillette Razor' => 'gillette', + 'Gimbal' => 'gimbal', + 'Gin' => 'gin', + 'Girl's Clothes' => 'girls-clothes', + 'Glasses' => 'glasses', + 'Glassware' => 'glassware', + 'Glenfiddich' => 'glenfiddich', + 'Glenlivet' => 'glenlivet', + 'Glenmorangie' => 'glenmorangie', + 'Gloves' => 'gloves', + 'Glue' => 'glue', + 'Glue Gun' => 'glue-gun', + 'Gluten-Free' => 'gluten-free', + 'God of War' => 'god-of-war', + 'Go Kart' => 'go-kart', + 'Golf' => 'golf', + 'Golf Balls' => 'golf-balls', + 'Golf Clubs' => 'golf-clubs', + 'Goodfellas' => 'goodfellas', + 'Goodmans' => 'goodmans', + 'Goodyear' => 'goodyear', + 'Google' => 'google', + 'Google Home' => 'google-home', + 'Google Home Max' => 'google-home-max', + 'Google Home Mini' => 'google-home-mini', + 'Google Nest' => 'nest', + 'Google Nest Audio' => 'google-nest-audio', + 'Google Nest Hub' => 'google-home-hub', + 'Google Nest Mini' => 'nest-mini', + 'Google Nest Protect' => 'google-nest-protect', + 'Google Nexus' => 'nexus', + 'Google Pixel' => 'google-pixel', + 'Google Pixel 2' => 'google-pixel-2', + 'Google Pixel 2 XL' => 'google-pixel-2-xl', + 'Google Pixel 3' => 'google-pixel-3', + 'Google Pixel 3 XL' => 'google-pixel-3-xl', + 'Google Pixel 3a' => 'google-pixel-3a', + 'Google Pixel 3a XL' => 'google-pixel-3a-xl', + 'Google Pixel 4' => 'google-pixel-4', + 'Google Pixel 4 XL' => 'google-pixel-4-xl', + 'Google Pixel 4a' => 'google-pixel-4a', + 'Google Pixel 4a 5G' => 'google-pixel-4a-5g', + 'Google Pixel 5' => 'google-pixel-5', + 'Google Pixelbook' => 'google-pixelbook', + 'Google Pixel XL' => 'google-pixel-xl', + 'Google Smartphone' => 'google-smartphone', + 'Google Stadia' => 'google-stadia', + 'GoPro' => 'gopro', + 'GoPro HERO 6' => 'gopro-hero-6', + 'GoPro HERO 7' => 'gopro-hero-7', + 'GoPro HERO 8' => 'gopro-hero-8', + 'GoPro HERO 9' => 'gopro-hero-9', + 'Gore-Tex Clothing and Shoes' => 'gore-tex', + 'Graco' => 'graco', + 'Grand National' => 'grand-national', + 'Gran Turismo' => 'gran-turismo', + 'Gran Turismo Sport' => 'gran-turismo-sport', + 'Graphics Card' => 'graphics-card', + 'Gravity Rush' => 'gravity-rush', + 'Graze' => 'graze', + 'GreedFall' => 'greedfall', + 'Greenhouse' => 'greenhouse', + 'Greeting Cards and Wrapping Paper' => 'wrapping-paper-and-cards', + 'Greggs' => 'greggs', + 'Grey Goose' => 'grey-goose', + 'Griffin Technology' => 'griffin', + 'GroBag' => 'grobag', + 'Groceries' => 'groceries', + 'Gruffalo' => 'gruffalo', + 'Grundig' => 'grundig', + 'GTA' => 'gta', + 'GTA V' => 'gta-v', + 'GTX 970' => 'gtx-970', + 'GTX 980' => 'gtx-980', + 'GTX 1060' => 'gtx-1060', + 'GTX 1070' => 'gtx-1070', + 'GTX 1080' => 'gtx-1080', + 'GTX 1080 Ti' => 'gtx-1080-ti', + 'GTX 1660' => 'gtx-1660', + 'GTX 1660 Ti' => 'gtx-1660-ti', + 'Guardians of the Galaxy' => 'guardians-of-the-galaxy', + 'Gucci' => 'gucci', + 'Guinness' => 'guinness', + 'Guitar' => 'guitar', + 'Guitar Amp' => 'guitar-amp', + 'Guitar Hero' => 'guitar-hero', + 'Gulliver's' => 'gullivers', + 'Gym' => 'gym', + 'Gym Membership' => 'gym-membership', + 'H1Z1' => 'h1z1', + 'Häagen Dazs' => 'haagen-dazs', + 'Habitat' => 'habitat', + 'Hacksaw' => 'hacksaw', + 'Hair Brush' => 'hair-brush', + 'Hair Care' => 'hair', + 'Hair Clipper' => 'hair-clipper', + 'Hair Colour' => 'hair-colour', + 'Haircut' => 'haircut', + 'Hair Dryer' => 'hair-dryer', + 'Hair Dye' => 'hair-dye', + 'Hair Removal Devices' => 'hair-removal-devices', + 'Halifax' => 'halifax', + 'Hall' => 'hall', + 'Halloween' => 'halloween', + 'Halo' => 'halo', + 'Halo 5' => 'halo-5', + 'Ham' => 'ham', + 'Hammer' => 'hammer', + 'Hammer Drill' => 'hammer-drill', + 'Hammock' => 'hammock', + 'Handbag' => 'handbag', + 'Hand Blender' => 'hand-blender', + 'Hand Cream' => 'hand-cream', + 'Hand Mixer' => 'hand-mixer', + 'Hand Tools' => 'hand-tools', + 'Handwash' => 'handwash', + 'Hard Drive' => 'hard-drive', + 'Haribo' => 'haribo', + 'Harman Kardon' => 'harman-kardon', + 'Harry Potter' => 'harry-potter', + 'Hasbro' => 'hasbro', + 'Hat' => 'hat', + 'Hatchimals' => 'hatchimals', + 'Hats & Caps' => 'hats-caps', + 'Hauck' => 'hauck', + 'Hayfever Remedies' => 'hayfever', + 'Headboard' => 'headboard', + 'Headphones' => 'headphones', + 'Headset' => 'headset', + 'Health & Beauty' => 'beauty', + 'Healthcare' => 'health-care', + 'Heart Rate Monitor' => 'heart-rate-monitor', + 'Heater' => 'heater', + 'Heating' => 'heating', + 'Heating Appliances' => 'heating-appliances', + 'Hedge Trimmer' => 'hedge-trimmer', + 'Heineken' => 'heineken', + 'Heinz' => 'heinz', + 'Heinz Beanz' => 'heinz-baked-beans', + 'Hello Kitty' => 'hello-kitty', + 'Hello Neighbour' => 'hello-neighbour', + 'Helly Hansen' => 'helly-hansen', + 'Henry Hoover' => 'henry-hoover', + 'Hermes' => 'hermes', + 'High5' => 'high-5', + 'Highchair' => 'highchair', + 'Hiking' => 'hiking', + 'Hilton' => 'hilton', + 'Hisense' => 'hisense', + 'Hisense TVs' => 'hisense-tv', + 'Hitachi' => 'hitachi', + 'Hitman' => 'hitman', + 'Hive' => 'hive', + 'Hive Active Heating' => 'hive-active-heating', + 'Hob' => 'hob', + 'Hobbit' => 'hobbit', + 'Hockey' => 'hockey', + 'Holiday Inn' => 'holiday-inn', + 'Holiday Park' => 'holiday-parks', + 'Holidays and Trips' => 'holidays-and-trips', + 'Hollow Knight' => 'hollow-knight', + 'Home & Living' => 'home', + 'Home Accessories' => 'home-accessories', + 'Home Appliances' => 'home-appliances', + 'Home Care' => 'home-care', + 'Home Cinema' => 'home-cinema', + 'HoMedics' => 'homedics', + 'Homefront' => 'homefront', + 'Home Networking' => 'network', + 'Homeplug' => 'homeplug', + 'Home Security' => 'home-security', + 'Homeware' => 'homeware', + 'Honda' => 'honda', + 'Honey' => 'honey', + 'Honeywell' => 'honeywell', + 'Honor 6X' => 'honor-6x', + 'Honor 7' => 'honor-7', + 'Honor 8S' => 'honor-8s', + 'Honor 8X' => 'honor-8x', + 'Honor 8X Max' => 'honor-8x-max', + 'Honor 9' => 'honor-9', + 'Honor 9X' => 'honor-9x', + 'Honor 10' => 'honor-10', + 'Honor Band 5' => 'honor-band-5', + 'Honor Play' => 'honor-play', + 'Honor Smartphone' => 'honor', + 'Honor View 20' => 'honor-view-20', + 'Hoodie' => 'hoodie', + 'Hoover' => 'hoover', + 'Hori' => 'hori', + 'Horizon: Zero Dawn' => 'horizon-zero-dawn', + 'Hornby' => 'hornby', + 'Horse Races' => 'horse-races', + 'Hose' => 'hose', + 'HOTAS' => 'hotas', + 'Hotel' => 'hotel', + 'Hotpoint' => 'hotpoint', + 'Hotspot' => 'hotspot', + 'Hot Tub' => 'hot-tub', + 'Hot Water Bottle' => 'hot-water-bottle', + 'Hot Wheels' => 'hot-wheels', + 'Hozelock' => 'hozelock', + 'HP' => 'hp', + 'HP Envy' => 'hp-envy', + 'HP Laptop' => 'hp-laptop', + 'HP Omen' => 'hp-omen', + 'HP Printer' => 'hp-printer', + 'HTC' => 'htc', + 'HTC 10' => 'htc-10', + 'HTC Desire' => 'htc-desire', + 'HTC One' => 'htc-one', + 'HTC Smartphone' => 'htc-smartphone', + 'HTC U11' => 'htc-u11', + 'HTC Vive' => 'htc-vive', + 'Huawei' => 'huawei', + 'Huawei Freebuds 3' => 'huawei-freebuds-3', + 'Huawei Headphones' => 'huawei-headphones', + 'Huawei Mate 20' => 'huawei-mate-20', + 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro', + 'Huawei Mate 30' => 'huawei-mate-30', + 'Huawei Mate 30 Lite' => 'huawei-mate-30-lite', + 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro', + 'Huawei Matebook' => 'huawei-matebook', + 'Huawei MediaPad M3' => 'huawei-mediapad-m3', + 'Huawei MediaPad M5' => 'huawei-mediapad-m5', + 'Huawei MediaPad T3' => 'huawei-mediapad-t3', + 'Huawei MediaPad T5' => 'huawei-mediapad-t5', + 'Huawei P9' => 'huawei-p9', + 'Huawei P10' => 'huawei-p10', + 'Huawei P20' => 'huawei-p20', + 'Huawei P20 Lite' => 'huawei-p20-lite', + 'Huawei P20 Pro' => 'huawei-p20-pro', + 'Huawei P30' => 'huawei-p30', + 'Huawei P30 Lite' => 'huawei-p30-lite', + 'Huawei P30 Pro' => 'huawei-p30-pro', + 'Huawei P40' => 'huawei-p40', + 'Huawei P40 Lite' => 'huawei-p40-lite', + 'Huawei P40 Pro' => 'huawei-p40-pro', + 'Huawei P Smart' => 'huawei-p-smart', + 'Huawei Smartphone' => 'huawei-smartphone', + 'Huawei Smartwatch' => 'huawei-smartwatch', + 'Huawei Tablet' => 'huawei-tablet', + 'Huawei Watch 2' => 'huawei-watch-2', + 'Huawei Watch GT' => 'huawei-watch-gt', + 'Huawei Watch GT2' => 'huawei-watch-gt2', + 'Huawei Watch GT 2 Pro' => 'huawei-watch-gt-2-pro', + 'Huawei Y7' => 'huawei-y7', + 'Huggies' => 'huggies', + 'Hulk' => 'hulk', + 'Humax' => 'humax', + 'Humidifier' => 'humidifier', + 'Hunter' => 'hunter', + 'HyperX' => 'hyperx', + 'Hyrule Warriors' => 'hyrule-warriors', + 'Hyundai' => 'hyundai', + 'IAMS' => 'iams', + 'iCandy' => 'icandy', + 'Ice-Watch' => 'ice-watch', + 'Ice Cream' => 'ice-cream', + 'Ice Cream Maker' => 'ice-cream-maker', + 'iMac' => 'apple-imac', + 'iMac 2021' => 'imac-2021', + 'Impact Driver' => 'impact-driver', + 'Indesit' => 'indesit', + 'Inflatable Boats' => 'boat', + 'Inflatable Toys' => 'inflatable', + 'Injustice' => 'injustice', + 'Injustice 2' => 'injustice-2', + 'Ink Cartridge' => 'ink', + 'Inkjet Printer' => 'inkjet-printer', + 'Innocent' => 'innocent', + 'Instant Cameras' => 'instant-cameras', + 'Instant Ink' => 'instant-ink', + 'Instax Mini 9' => 'instax-mini-9', + 'Insulation' => 'insulation', + 'Insurance' => 'insurance', + 'Intel' => 'intel', + 'Intel Atom' => 'atom', + 'Intel i3' => 'i3', + 'Intel i5' => 'i5', + 'Intel i7' => 'i7', + 'Intel i9' => 'intel-i9', + 'Internet' => 'internet', + 'Internet Security' => 'internet-security', + 'In the Night Garden' => 'in-the-night-garden', + 'Intimate Care' => 'intimate-care', + 'Introduce Yourself' => 'introduce-yourself', + 'iOS Apps' => 'ios-apps', + 'iPad' => 'ipad', + 'iPad 2019' => 'ipad-2019', + 'iPad 2020' => 'ipad-2020', + 'iPad Air' => 'ipad-air', + 'iPad Air 2019' => 'ipad-air-2019', + 'iPad Air 2020' => 'ipad-air-2020', + 'iPad Case' => 'ipad-case', + 'iPad mini' => 'ipad-mini', + 'iPad Pro' => 'ipad-pro', + 'iPad Pro 11' => 'ipad-pro-11', + 'iPad Pro 12.9' => 'ipad-pro-12-9', + 'iPad Pro 2020' => 'ipad-pro-2020', + 'iPad Pro 2021' => 'ipad-pro-2021', + 'IP Camera' => 'ip-camera', + 'iPhone' => 'iphone', + 'iPhone 5s' => 'iphone-5s', + 'iPhone 6' => 'iphone-6', + 'iPhone 6 Plus' => 'iphone-6-plus', + 'iPhone 6s' => 'iphone-6s', + 'iPhone 6s Plus' => 'iphone-6s-plus', + 'iPhone 7' => 'iphone-7', + 'iPhone 7 Plus' => 'iphone-7-plus', + 'iPhone 8' => 'iphone-8', + 'iPhone 8 Plus' => 'iphone-8-plus', + 'iPhone 11' => 'iphone-11', + 'iPhone 11 Pro' => 'iphone-11-pro', + 'iPhone 11 Pro Max' => 'iphone-11-pro-max', + 'iPhone 12' => 'iphone-12', + 'iPhone 12 mini' => 'iphone-12-mini', + 'iPhone 12 Pro' => 'iphone-12-pro', + 'iPhone 12 Pro Max' => 'iphone-12-pro-max', + 'iPhone Accessories' => 'iphone-accessories', + 'iPhone Case' => 'iphone-case', + 'iPhone SE' => 'iphone-se', + 'iPhone X' => 'iphone-x', + 'iPhone Xr' => 'iphone-xr', + 'iPhone Xs' => 'iphone-xs', + 'iPhone Xs Max' => 'iphone-xs-max', + 'iPod' => 'ipod', + 'iPod Nano' => 'ipod-nano', + 'iPod Shuffle' => 'ipod-shuffle', + 'iPod Touch' => 'ipod-touch', + 'Irish Whiskey' => 'irish-whisky', + 'Irn Bru' => 'irn-bru', + 'iRobot' => 'irobot', + 'Iron' => 'iron', + 'Ironing' => 'ironing', + 'Ironing Board' => 'ironing-board', + 'Iron Man' => 'iron-man', + 'Issey Miyake' => 'issey-miyake', + 'ITV' => 'itv', + 'Jabra' => 'jabra', + 'Jabra Elite 85h' => 'jabra-elite-85h', + 'Jabra Elite Active 65t' => 'jabra-elite-active-65t', + 'Jabra Elite Active 75t' => 'jabra-elite-active-75t', + 'Jabra Headphones' => 'jabra-headphones', + 'Jack & Jones' => 'jack-and-jones', + 'Jack Daniel's' => 'jack-daniels', + 'Jacket' => 'jacket', + 'Jack Wills' => 'jack-wills', + 'Jack Wolfskin' => 'jack-wolfskin', + 'Jaffa Cakes' => 'jaffa-cakes', + 'Jägermeister' => 'jagermeister', + 'Jameson' => 'jameson', + 'Jamie Oliver' => 'jamie-oliver', + 'Jaybird' => 'jaybird', + 'JBL' => 'jbl', + 'JBL Flip' => 'jbl-flip', + 'JBL GO' => 'jbl-go', + 'JBL Headphones' => 'jbl-headphones', + 'JBL Link' => 'jbl-link', + 'JBL Live' => 'jbl-live', + 'JBL Tune' => 'jbl-tune', + 'JCB' => 'jcb', + 'Jean Paul Gaultier' => 'jean-paul-gautier', + 'Jean Paul Gaultier Le Male' => 'le-male', + 'Jeans' => 'jeans', + 'Jelly Belly' => 'jelly-belly', + 'Jewellery' => 'jewellery', + 'Jigsaw' => 'jigsaw', + 'Jim Beam' => 'jim-beam', + 'Jimmy Choo' => 'jimmy-choo', + 'JML' => 'jml', + 'Jogging Bottoms' => 'jogging-bottoms', + 'Johnnie Walker' => 'johnnie-walker', + 'Johnson's' => 'johnsons', + 'John West' => 'john-west', + 'John Wick' => 'john-wick', + 'JoJo Siwa' => 'jojo', + 'Joop' => 'joop', + 'Joseph Joseph' => 'joseph-joseph', + 'Joules' => 'joules', + 'Juice' => 'juice', + 'Juicer' => 'juicer', + 'Jumper' => 'jumper', + 'Jurassic World' => 'jurassic-world', + 'Jura Whisky' => 'jura', + 'Just Cause' => 'just-cause', + 'Just Cause 3' => 'just-cause-3', + 'Just Cause 4' => 'just-cause-4', + 'Just Dance' => 'just-dance', + 'JVC' => 'jvc', + 'K-Swiss' => 'k-swiss', + 'Karcher' => 'karcher', + 'Karcher Window Vacuum' => 'karcher-window-cleaner', + 'Karen Millen' => 'karen-millen', + 'Karrimor' => 'karrimor', + 'Kaspersky' => 'kaspersky', + 'Kayak' => 'kayak', + 'Keg' => 'keg', + 'Kellogg's' => 'kelloggs', + 'Kellogg's Cornflakes' => 'cornflakes', + 'Kellogg's Crunchy Nut' => 'crunchy-nut', + 'Kenco' => 'kenco', + 'Kenwood' => 'kenwood', + 'Kenwood kMix' => 'kmix', + 'Kenzo' => 'kenzo', + 'Ketchup' => 'ketchup', + 'Keter' => 'keter', + 'Kettle' => 'kettle', + 'Kettlebell' => 'kettlebell', + 'Keyboard' => 'keyboard', + 'KIA' => 'kia', + 'Kickers' => 'kickers', + 'Kid's Bike' => 'kids-bike', + 'Kid's Clothes' => 'kids-clothes', + 'Kid's Room' => 'kids-rooms', + 'Kid's Shoes' => 'kids-shoes', + 'Kidizoom' => 'kidizoom', + 'Killzone' => 'killzone', + 'Kilner' => 'kilner', + 'Kinder' => 'kinder', + 'Kindle' => 'kindle', + 'Kindle Book' => 'kindle-book', + 'Kindle Fire' => 'kindle-fire', + 'Kindle Oasis' => 'kindle-oasis', + 'Kindle Paperwhite' => 'kindle-paperwhite', + 'Kingdom Come: Deliverance' => 'kingdom-come-deliverance', + 'Kingdom Hearts' => 'kingdom-hearts', + 'Kingdom Hearts 3' => 'kingdom-hearts-3', + 'Kingdom Hearts: The Story So Far' => 'kingdom-hearts-the-story-so-far', + 'King Kong' => 'king-kong', + 'King Size Bed' => 'king-size', + 'Kingsmill' => 'kingsmill', + 'Kingston' => 'kingston', + 'Kitchen' => 'kitchen', + 'KitchenAid' => 'kitchenaid', + 'Kitchen Appliances' => 'kitchen-appliances', + 'Kitchen Knife' => 'knife', + 'Kitchen Roll' => 'kitchen-roll', + 'Kitchen Scale' => 'kitchen-scales', + 'Kitchen Tap' => 'kitchen-tap', + 'Kitchen Utensils' => 'kitchen-utensils', + 'Kite' => 'kite', + 'KitSound' => 'kitsound', + 'Knickers' => 'knickers', + 'Kobo' => 'kobo', + 'Kodak' => 'kodak', + 'Kodi' => 'kodi', + 'Kohinoor' => 'kohinoor', + 'Kopparberg' => 'kopparberg', + 'Kraken' => 'kraken', + 'Krispy Kreme' => 'krispy-kreme', + 'Krups' => 'krups', + 'KTC' => 'ktc', + 'Kurt Geiger' => 'kurt-geiger', + 'L'Occitane' => 'loccitane', + 'L.O.L. Surprise!' => 'lol-surprise', + 'Lacoste' => 'lacoste', + 'Ladder' => 'ladder', + 'Lamaze' => 'lamaze', + 'Lamb' => 'lamb', + 'Laminate' => 'laminate', + 'Laminator' => 'laminator', + 'Lamp' => 'lamp', + 'Lancôme' => 'lancome', + 'Landmann' => 'landmann', + 'Lantern' => 'lantern', + 'Laphroaig' => 'laphroaig', + 'Laptop' => 'laptop', + 'Laptop Accessories' => 'laptop-accessories', + 'Laptop Case' => 'laptop-case', + 'Laptop Sleeve' => 'laptop-sleeve', + 'Laser Printer' => 'laser-printer', + 'Last Minute' => 'last-minute', + 'Laundry Basket' => 'laundry-basket', + 'Laura Ashley' => 'laura-ashley', + 'Lavazza' => 'lavazza', + 'Lavender' => 'lavender', + 'Lawnmower' => 'lawnmower', + 'Lay-Z-Spa' => 'lay-z-spa', + 'LeapFrog' => 'leapfrog', + 'Le Creuset' => 'le-creuset', + 'LED Bulb' => 'led-bulbs', + 'LED Light' => 'led-light', + 'LED Strip Lights' => 'led-strip-lights', + 'LED TV' => 'led-tv', + 'Lee Stafford' => 'lee-stafford', + 'Leffe' => 'leffe', + 'Leggings' => 'leggings', + 'Lego' => 'lego', + 'Lego Advent Calendar' => 'lego-advent-calendar', + 'Lego Architecture' => 'lego-architecture', + 'Lego Art' => 'lego-art', + 'Lego Batman' => 'lego-batman', + 'Lego BrickHeadz' => 'lego-brickheadz', + 'Lego City' => 'lego-city', + 'Lego Classic' => 'lego-classic', + 'Lego Creator' => 'lego-creator', + 'Lego Dimensions' => 'lego-dimensions', + 'Lego Disney' => 'lego-disney', + 'Lego Dots' => 'lego-dots', + 'Lego Duplo' => 'lego-duplo', + 'Lego Friends' => 'lego-friends', + 'LEGO Harry Potter' => 'lego-harry-potter', + 'Lego Hidden Side' => 'lego-hidden-side', + 'Legoland' => 'legoland', + 'Lego Marvel' => 'lego-marvel', + 'Lego Mindstorms' => 'lego-mindstorms', + 'Lego Nexo Knights' => 'lego-nexo-knights', + 'Lego Ninjago' => 'lego-ninjago', + 'Lego Porsche' => 'lego-porsche', + 'Lego Simpsons' => 'lego-simpsons', + 'Lego Speed Champions' => 'lego-speed-champions', + 'Lego Star Wars' => 'lego-star-wars', + 'Lego Star Wars Millennium Falcon' => 'lego-star-wars-millennium-falcon', + 'Lego Super Mario' => 'lego-mario', + 'Lego Technic' => 'lego-technic', + 'Lego VIDIYO' => 'lego-vidiyo', + 'Lemonade' => 'lemonade', + 'Lenor' => 'lenor', + 'Lenovo' => 'lenovo', + 'Lenovo IdeaPad' => 'lenovo-ideapad', + 'Lenovo Laptop' => 'lenovo-laptop', + 'Lenovo Tablet' => 'lenovo-tablet', + 'Lenovo Thinkpad' => 'thinkpad', + 'Lenovo Yoga Laptop' => 'lenovo-yoga-laptop', + 'Lenovo Yoga Tablet' => 'lenovo-yoga', + 'Les Paul' => 'les-paul', + 'Levi's' => 'levi', + 'Lexar' => 'lexar', + 'LG' => 'lg', + 'LG G3' => 'lg-g3', + 'LG G5' => 'lg-g5', + 'LG G6' => 'lg-g6', + 'LG G7' => 'lg-g7', + 'LG G8S ThinQ' => 'lg-g8s-thinq', + 'LG OLED TV' => 'lg-oled-tv', + 'LG Smartphone' => 'lg-smartphone', + 'LG TV' => 'lg-tv', + 'LG V30' => 'lg-v30', + 'LG V40 ThinQ' => 'lg-v40-thinq', + 'Life Insurance' => 'life-insurance', + 'Life is Strange' => 'life-is-strange', + 'Light Box' => 'light-box', + 'Lighting' => 'lighting', + 'Lightning Cable' => 'lightning-cable', + 'Lightsaber' => 'lightsaber', + 'Lindor' => 'lindor', + 'Lindt' => 'lindt', + 'Lingerie' => 'lingerie', + 'Linksys' => 'linksys', + 'Linx' => 'linx', + 'Lion King' => 'lion-king', + 'Lipstick' => 'lipstick', + 'Lipsy' => 'lipsy', + 'Little Tikes' => 'little-tikes', + 'Liverpool F. C.' => 'liverpool-fc', + 'Living Room' => 'living-room', + 'Local Traffic' => 'local-traffic', + 'Lodge' => 'lodge', + 'Loft' => 'loft', + 'Logitech' => 'logitech', + 'Logitech G430' => 'logitech-g430', + 'Logitech G703' => 'logitech-g703', + 'Logitech G903' => 'logitech-g903', + 'Logitech Harmony' => 'harmony', + 'Logitech Keyboard' => 'logitech-keyboard', + 'Logitech Mouse' => 'logitech-mouse', + 'Logitech MX Master' => 'logitech-mx-master', + 'Logitech MX Master 2S' => 'logitech-mx-master-2s', + 'London Eye' => 'london-eye', + 'London Zoo' => 'london-zoo', + 'Longleat' => 'longleat', + 'Long Sleeve' => 'long-sleeve', + 'Lord of the Rings' => 'lord-of-the-rings', + 'Lottery' => 'lottery', + 'Lounger' => 'lounger', + 'Lowepro' => 'lowepro', + 'Lucozade' => 'lucozade', + 'Luigi' => 'luigi', + 'Luigi's Mansion' => 'luigis-manison', + 'Luigi's Mansion 3' => 'luigis-mansion-3', + 'Lunch Bag' => 'lunch-bag', + 'Lunch Box' => 'lunch-box', + 'Lurpak' => 'lurpak', + 'Luton' => 'luton', + 'Lyle & Scott' => 'lyle-and-scott', + 'Lynx' => 'lynx', + 'M.2 SSD' => 'm2-ssd', + 'MacBook' => 'macbook', + 'MacBook Air' => 'macbook-air', + 'MacBook Pro' => 'macbook-pro', + 'MacBook Pro 13' => 'macbook-pro-13', + 'MacBook Pro 15' => 'macbook-pro-15', + 'MacBook Pro 16' => 'macbook-pro-16', + 'Maclaren' => 'maclaren', + 'Mac mini' => 'mac-mini', + 'Madame Tussauds' => 'madame-tussauds', + 'Mad Catz' => 'madcatz', + 'Madden NFL' => 'madden', + 'Madden NFL 20' => 'madden-nfl-20', + 'Mad Max' => 'mad-max', + 'Mafia 3' => 'mafia-3', + 'Magazine' => 'magazine', + 'Magimix' => 'magimix', + 'Magners' => 'magners', + 'Magnum' => 'magnum', + 'Make Up' => 'make-up', + 'Makeup Advent Calendar' => 'makeup-advent-calendar', + 'Make Up Brush' => 'make-up-brush', + 'Makita' => 'makita', + 'Makita Drill' => 'makita-drill', + 'Malibu' => 'malibu', + 'Maltesers' => 'maltesers', + 'MAM' => 'mam', + 'Mamas & Papas' => 'mamas-and-papas', + 'Manchester United' => 'manchester-united', + 'Manfrotto' => 'manfrotto', + 'Manga' => 'manga', + 'Manuka Honey' => 'manuka-honey', + 'Marantz' => 'marantz', + 'Marc Jacobs' => 'marc-jacobs', + 'Marc Jacobs Daisy' => 'daisy', + 'Mario & Sonic at the Olympic Games: Tokyo 2020' => 'mario-and-sonic-tokyo-2020', + 'Mario + Rabbids Kingdom Battle' => 'mario-rabbids-kingdom-battle', + 'Mario Kart' => 'mario-kart', + 'Mario Kart 8' => 'mario-kart-8', + 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe', + 'Marmite' => 'marmite', + 'Mars' => 'mars', + 'Marshall' => 'marshall', + 'Marshall Headphones' => 'marshall-headphones', + 'Marvel' => 'marvel', + 'Marvel's Spider-Man (PS4)' => 'spider-man-2018', + 'Marvel's Spider-Man: Miles Morales' => 'spiderman-miles-morales', + 'Mascara' => 'mascara', + 'Massage' => 'massage', + 'Mass Effect' => 'mass-effect', + 'Mass Effect: Andromeda' => 'mass-effect-andromeda', + 'Mastercard' => 'mastercard', + 'Masterplug' => 'masterplug', + 'Maternity & Pregnancy' => 'maternity', + 'Mattress' => 'mattress', + 'Mattress Protector' => 'mattress-protector', + 'Mattress Topper' => 'mattress-topper', + 'Mavic' => 'mavic', + 'Max Factor' => 'max-factor', + 'Maxi Cosi' => 'maxi-cosi', + 'Maximuscle' => 'maximuscle', + 'Maxtor' => 'maxtor', + 'Maybelline' => 'maybelline', + 'Mayo' => 'mayo', + 'Mazda' => 'mazda', + 'McAfee' => 'mcafee', + 'Meat & Sausages' => 'meat', + 'Meccano' => 'meccano', + 'Mechanical Keyboard' => 'mechanical-keyboard', + 'Medal of Honor' => 'medal-of-honor', + 'Medela' => 'medela', + 'Media Player' => 'media-player', + 'Medievil' => 'medievil', + 'Medion' => 'medion', + 'Mega Bloks' => 'mega-bloks', + 'Megathread' => 'megathread', + 'Melissa & Doug' => 'melissa', + 'Memory Cards' => 'memory-cards', + 'Memory Foam Mattress' => 'memory-foam', + 'Men's Boots' => 'mens-boots', + 'Men's Fragrance' => 'mens-fragrance', + 'Men's Shoes' => 'mens-shoes', + 'Men's Suit' => 'suit', + 'Mercedes' => 'mercedes', + 'Meridian' => 'meridian', + 'Merlin' => 'merlin', + 'Merrell' => 'merrell', + 'Messenger Bag' => 'messenger-bag', + 'Metal Gear Solid' => 'metal-gear-solid', + 'Metro Exodus' => 'metro-exodus', + 'Metroid' => 'metroid', + 'Metro Series' => 'metro-series', + 'Michael Kors' => 'michael-kors', + 'Michelin' => 'michelin', + 'Microphone' => 'microphone', + 'Micro SD Card' => 'micro-sd', + 'Micro SDHC' => 'micro-sdhc', + 'Micro SDXC' => 'micro-sdxc', + 'Microserver' => 'microserver', + 'Microsoft' => 'microsoft', + 'Microsoft Flight Simulator' => 'microsoft-flight-simulator', + 'Microsoft Office' => 'microsoft-office', + 'Microsoft Points' => 'microsoft-points', + 'Microsoft Software' => 'microsoft-software', + 'Microsoft Surface Book' => 'surface-book', + 'Microsoft Surface Laptop' => 'surface', + 'Microsoft Surface Pro 6' => 'surface-pro-6', + 'Microsoft Surface Pro 7' => 'surface-pro-7', + 'Microsoft Surface Tablet' => 'microsoft-surface-tablet', + 'Microwave' => 'microwave', + 'Middle Earth' => 'middle-earth', + 'Middle Earth: Shadow of Mordor' => 'shadow-of-mordor', + 'Middle Earth: Shadow of War' => 'middle-earth-shadow-of-war', + 'Miele' => 'miele', + 'Miele Vacuum Cleaner' => 'miele-vacuum-cleaner', + 'Milk' => 'milk', + 'Milk Frother' => 'milk-frother', + 'Milk Tray' => 'milk-tray', + 'Milwaukee' => 'milwaukee', + 'Mince' => 'mince', + 'Minecraft Game' => 'minecraft', + 'Mineral Water' => 'mineral-water', + 'Mini Fridge' => 'mini-fridge', + 'Minions' => 'minions', + 'Mini PC' => 'mini-pc', + 'Minky' => 'minky', + 'Mira' => 'mira', + 'Mirror' => 'mirror', + 'Mirror's Edge' => 'mirrors-edge', + 'Misc' => 'misc', + 'Misfit' => 'misfit', + 'Mitre Saw' => 'mitre-saw', + 'Mitsubishi' => 'mitsubishi', + 'Mixer & Blender' => 'mixer-and-blender', + 'Mobile Contracts' => 'mobile-contract', + 'Mobile Phone' => 'mobile-phone', + 'Model Building' => 'model-building', + 'Moët' => 'moet', + 'Molton Brown' => 'molton-brown', + 'Money Saving Tips and Tricks' => 'money-saving-tips', + 'Monitor' => 'monitor', + 'Monopoly' => 'monopoly', + 'Monsoon' => 'monsoon', + 'Monster Energy' => 'monster-energy', + 'Monster High' => 'monster-high', + 'Monster Hunter' => 'monster-hunter', + 'Monster Hunter World' => 'monster-hunter-world', + 'Mont Blanc' => 'mont-blanc', + 'Mop' => 'mop', + 'Morphy Richards' => 'morphy-richards', + 'Mortal Kombat' => 'mortal-kombat', + 'Mortal Kombat 11' => 'mortal-kombat-11', + 'Mortgage' => 'mortgage', + 'Moschino' => 'moschino', + 'Moses Basket' => 'moses-basket', + 'MOT' => 'mot', + 'Motherboard' => 'motherboard', + 'Moto 360' => 'moto-360', + 'Moto E' => 'moto-e', + 'Moto G' => 'moto-g', + 'Moto G4' => 'moto-g4', + 'Moto G5' => 'moto-g5', + 'Moto G6' => 'moto-g6', + 'Moto G7' => 'moto-g7', + 'Motorcycle' => 'motorcycle', + 'Motorcycle Accessories' => 'motorcycle-accessories', + 'Motorcycle Helmet' => 'motorcycle-helmet', + 'Motorola' => 'motorola', + 'Motorola Smartphone' => 'motorola-smartphone', + 'Moto X' => 'moto-x', + 'Moto Z' => 'moto-z', + 'Mountain Bike' => 'mountain-bike', + 'Mouse & Keyboard Bundles' => 'mouse-and-keyboard-bundle', + 'Mouse Mat' => 'mouse-mat', + 'Mouthwash' => 'mouthwash', + 'Movie and TV Box Set' => 'box-set', + 'Movies & Series' => 'movie', + 'MP3 Player' => 'mp3-player', + 'Mr Kipling' => 'mr-kipling', + 'Mr Men' => 'mr-men', + 'MSI' => 'msi', + 'MSI Laptop' => 'msi-laptop', + 'Muc-Off' => 'muc-off', + 'Mug' => 'mug', + 'Muller' => 'muller', + 'Multi-Room Audio System' => 'multi-room-audio-system', + 'Multitool' => 'multitool', + 'Museums' => 'museums', + 'Music' => 'music', + 'Musical Instruments' => 'musical-instrument', + 'Music App' => 'music-app', + 'Music Streaming' => 'music-streaming', + 'My Little Pony' => 'my-little-pony', + 'Nail Gun' => 'nail-gun', + 'Nail Polish' => 'nail-polish', + 'Nails' => 'nails', + 'Nails Inc.' => 'nails-inc', + 'Nakd' => 'nakd', + 'Nando's' => 'nandos', + 'Nappy' => 'nappy', + 'NAS' => 'nas', + 'National Express Ticket' => 'national-express', + 'National Trust' => 'national-trust', + 'Nature Observation' => 'nature-observation', + 'NatWest' => 'natwest', + 'NBA 2K' => 'nba-2k', + 'NBA Live' => 'nba', + 'Necklace' => 'necklace', + 'Need for Speed' => 'need-for-speed', + 'Need for Speed: Payback' => 'need-for-speed-payback', + 'Need for Speed Heat' => 'need-for-speed-heat', + 'Neff' => 'neff', + 'Nerf Guns' => 'nerf', + 'Nescafé Azera' => 'azera', + 'Nescafé Coffee' => 'nescafe', + 'Nespresso' => 'nespresso', + 'Nespresso Coffee Machine' => 'nespresso-coffee-machine', + 'Nest Hello' => 'nest-hello', + 'Nestlé' => 'nestle', + 'Nest Learning Thermostat' => 'nest-learning-thermostat', + 'Nestlé Cheerios' => 'cheerios', + 'Nestlé Shreddies' => 'shreddies', + 'Netatmo' => 'netatmo', + 'Netflix' => 'netflix', + 'Netgear' => 'netgear', + 'Netgear Arlo' => 'arlo', + 'New Balance' => 'new-balance', + 'New Balance Trainers' => 'new-balance-trainers', + 'New Look' => 'new-look', + 'Newspapers' => 'newspapers', + 'Nextbase' => 'nextbase', + 'NFL' => 'nfl', + 'NHL' => 'nhl', + 'NHL 20' => 'nhl-20', + 'NHS' => 'nhs', + 'NieR: Automata' => 'nier', + 'Night Light' => 'night-light', + 'Nike' => 'nike', + 'Nike Air Max' => 'nike-air-max', + 'Nike Air Max 200' => 'nike-air-max-200', + 'Nike Air Max 270' => 'nike-air-max-270', + 'Nike Air Max 720' => 'nike-air-max-720', + 'Nike Free' => 'nike-free', + 'Nike Huarache' => 'nike-huarache', + 'Nike Jordan' => 'jordan', + 'Nike Presto' => 'nike-presto', + 'Nike Roshe' => 'nike-roshe', + 'Nike Trainers' => 'nike-shoes', + 'Nikon' => 'nikon', + 'Nikon Camera' => 'nikon-camera', + 'Nikon Coolpix' => 'nikon-coolpix', + 'Nikon D3400' => 'nikon-d3400', + 'Nikon Lens' => 'nikon-lens', + 'Nilfisk' => 'nilfisk', + 'Ni No Kuni' => 'ni-no-kuni', + 'Ni No Kuni: Wrath of the White Witch' => 'ni-no-kuni-white-witch', + 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-2', + 'Nintendo' => 'nintendo', + 'Nintendo 2DS' => '2ds', + 'Nintendo 3DS' => '3ds', + 'Nintendo 3DS Game' => '3ds-games', + 'Nintendo 3DS XL' => 'nintendo-3ds-xl', + 'Nintendo Accessories' => 'nintendo-accessories', + 'Nintendo Classic Mini' => 'nintendo-classic-mini', + 'Nintendo DS Game' => 'ds-games', + 'Nintendo Labo' => 'switch-labo', + 'Nintendo Switch' => 'nintendo-switch', + 'Nintendo Switch Accessories' => 'switch-accessories', + 'Nintendo Switch Case' => 'switch-case', + 'Nintendo Switch Controller' => 'switch-controller', + 'Nintendo Switch Game' => 'switch-game', + 'Nintendo Switch Joy-Con' => 'switch-joy-con', + 'Nintendo Switch Lite' => 'nintendo-switch-lite', + 'Nintendo Switch Pro Controller' => 'switch-pro-controller', + 'Nioh' => 'nioh', + 'Nissan' => 'nissan', + 'Nivea' => 'nivea', + 'No7' => 'no7', + 'Noise Cancelling Headphones' => 'noise-cancelling-headphones', + 'Nokia' => 'nokia', + 'Nokia Smartphones' => 'nokia-mobile', + 'No Man's Sky' => 'no-man-s-sky', + 'Noodles' => 'noodles', + 'Norton' => 'norton', + 'Now' => 'now-tv', + 'Numatic' => 'numatic', + 'Nursery' => 'nursery', + 'Nutella' => 'nutella', + 'NutriBullet' => 'nutribullet', + 'Nutri Ninja' => 'nutri-ninja', + 'Nuts' => 'nuts', + 'Nvidia' => 'nvidia', + 'Nvidia GeForce' => 'geforce', + 'Nvidia Shield' => 'nvidia-shield', + 'NYX' => 'nyx', + 'NZXT' => 'nzxt', + 'O2' => 'o2', + 'O2 Refresh' => 'o2-refresh', + 'Oakley' => 'oakley', + 'Octonauts' => 'octonauts', + 'Oculus Game' => 'oculus-game', + 'Oculus Go' => 'oculus-go', + 'Oculus Quest' => 'oculus-quest', + 'Oculus Rift' => 'oculus', + 'Oculus Rift S' => 'oculus-rift-s', + 'Odeon' => 'odeon', + 'Office' => 'office', + 'Office Chair' => 'office-chair', + 'Official Announcements' => 'official-announcements', + 'Olay' => 'olay', + 'OLED TV' => 'oled', + 'Olive Oil' => 'olive-oil', + 'Olympus' => 'olympus', + 'Omega Seamaster' => 'omega-seamaster', + 'Omega Speedmaster' => 'omega-speedmaster', + 'Omega Watches' => 'omega-watch', + 'OnePlus 3' => 'oneplus-3', + 'OnePlus 5' => 'oneplus-5', + 'OnePlus 6' => 'oneplus-6', + 'OnePlus 6T' => 'oneplus-6t', + 'OnePlus 7' => 'oneplus-7', + 'OnePlus 7 Pro' => 'oneplus-7-pro', + 'OnePlus 7T' => 'oneplus-7t', + 'OnePlus 7T Pro' => 'one-plus-7t-pro', + 'OnePlus 8' => 'oneplus-8', + 'OnePlus 8 Pro' => 'oneplus-8-pro', + 'OnePlus 8T' => 'oneplus-8t', + 'OnePlus 9' => 'oneplus-9', + 'OnePlus 9 Pro' => 'oneplus-9-pro', + 'OnePlus Nord' => 'oneplus-nord', + 'OnePlus Nord N10 5G' => 'oneplus-n10', + 'OnePlus Nord N100' => 'oneplus-n100', + 'OnePlus Smartphone' => 'oneplus', + 'Onesie' => 'onesie', + 'Onkyo' => 'onkyo', + 'Online Courses' => 'online-courses', + 'Operating System' => 'operating-system', + 'Oppo Find X2 Lite' => 'oppo-find-x2-lite', + 'Oppo Find X2 Neo' => 'oppo-find-x2-neo', + 'Oppo Find X2 Pro' => 'oppo-find-x2-pro', + 'Oppo Reno' => 'oppo-reno', + 'Oppo Reno4 5G' => 'oppo-reno4', + 'Oppo Reno4 Z 5G' => 'oppo-reno4-z', + 'Oppo Smartphone' => 'oppo-smartphone', + 'Opticians' => 'opticians', + 'Optoma' => 'optoma', + 'Oral-B' => 'oral-b', + 'Oral-B Toothbrush' => 'oral-b-toothbrush', + 'Oreo' => 'oreo', + 'Origin' => 'origin', + 'Original Penguin' => 'penguin', + 'Orla Kiely' => 'orla-kiely', + 'Osprey' => 'osprey', + 'Osram' => 'osram', + 'Other' => 'other-deals', + 'Ottoman' => 'ottoman', + 'Oukitel' => 'oukitel', + 'Outdoor Clothing' => 'outdoor-clothing', + 'Outdoor Lighting' => 'outdoor-lighting', + 'Outdoor Sports & Camping' => 'outdoor', + 'Outdoor Toys' => 'outdoor-toys', + 'Outlast' => 'outlast', + 'Outlet' => 'outlet', + 'Outwell' => 'outwell', + 'Oven' => 'oven', + 'Overcooked' => 'overcooked', + 'Overcooked 2' => 'overcooked-2', + 'Overwatch' => 'overwatch', + 'Oyster Card' => 'oyster', + 'Package Holidays' => 'holiday', + 'Paco Rabanne' => 'paco-rabanne', + 'Paco Rabanne 1 Million' => 'paco-rabanne-1-million', + 'Paco Rabanne Lady Million' => 'lady-million', + 'Paddling Pool' => 'paddling-pool', + 'Padlock' => 'padlock', + 'Paint' => 'paint', + 'Paint Brush' => 'paint-brush', + 'Pampers' => 'pampers', + 'Panasonic' => 'panasonic', + 'Panasonic Camera' => 'panasonic-camera', + 'Panasonic Lumix' => 'lumix', + 'Panasonic TV' => 'panasonic-tv', + 'Pandora' => 'pandora', + 'Panini' => 'panini', + 'Panini Stickers' => 'panini-stickers', + 'Papa Johns' => 'papa-johns', + 'Paper Mario' => 'paper-mario', + 'Parasol' => 'parasol', + 'Parcel and Delivery Services' => 'parcel', + 'Parka' => 'parka', + 'Parking' => 'parking', + 'Parrot' => 'parrot', + 'Paul Smith' => 'paul-smith', + 'PAW Patrol' => 'paw-patrol', + 'Payday' => 'payday', + 'Payday 2' => 'payday-2', + 'PAYG' => 'payg', + 'Pay Monthly' => 'pay-monthly', + 'PC' => 'pc', + 'PC Case' => 'pc-case', + 'PC Game' => 'pc-game', + 'PC Gaming Accessories' => 'pc-gaming-accessories', + 'PC Gaming Systems' => 'pc-gaming-systems', + 'PC Mouse' => 'mouse', + 'PC Parts' => 'pc-parts', + 'Peanut Butter' => 'peanut-butter', + 'Peanuts' => 'peanuts', + 'Pedometer' => 'pedometer', + 'Pentax' => 'pentax', + 'Peppa Pig' => 'peppa-pig', + 'PepperBonus' => 'pepperbonus', + 'Pepsi' => 'pepsi', + 'Perfume' => 'perfume', + 'Persil' => 'persil', + 'Persona' => 'persona', + 'Persona 5' => 'persona-5', + 'Personal Care & Hygiene' => 'personal-care-hygiene', + 'Petrol and Diesel' => 'petrol', + 'Pet Supplies' => 'pets', + 'Peugeot' => 'peugeot', + 'PG Tips' => 'pg-tips', + 'Philips' => 'philips', + 'Philips Alarm Clock' => 'philips-alarm-clock', + 'Philips Avent' => 'avent', + 'Philips Hue' => 'philips-hue', + 'Philips Lumea' => 'lumea', + 'Philips OneBlade' => 'philips-one-blade', + 'Philips Senseo' => 'philips-senseo', + 'Philips Senseo Coffee Machine' => 'philips-senseo-coffee-machine', + 'Philips Shaver' => 'philips-shaver', + 'Philips Sonicare' => 'sonicare', + 'Philips TV' => 'philips-tv', + 'Phone Holder' => 'phone-holder', + 'Phones & Accessories' => 'phone', + 'Photo & Cameras' => 'photo-video', + 'Photo & Video App' => 'photo-video-app', + 'Photo Editing' => 'photo-editing', + 'Photo Frame' => 'photo-frame', + 'Photo Paper' => 'photo-paper', + 'Piano' => 'piano', + 'Picnic & Outdoor Cooking' => 'picnic', + 'Pikmin 3 Deluxe' => 'pikmin-3-deluxe', + 'Pillow' => 'pillow', + 'Pimm's' => 'pimms', + 'Pioneer' => 'pioneer', + 'Pirate Toys' => 'pirates', + 'PIR Lights' => 'pir', + 'Pixel C' => 'pixel-c', + 'Piz Buin' => 'piz-buin', + 'Pizza' => 'pizza', + 'Pizza Stone' => 'pizza-stone', + 'Planer' => 'planer', + 'Planet Earth' => 'planet-earth', + 'Plant' => 'plant', + 'Plant Pot' => 'plant-pots', + 'Plants vs. Zombies: Battle for Neighborville' => 'battle-for-neighborville', + 'Plants vs Zombies' => 'plants-vs-zombies', + 'Play-Doh' => 'play-doh', + 'PlayerUnknown's Battlegrounds' => 'playerunknown-s-battlegrounds', + 'Playhouse' => 'playhouse', + 'Playing Cards' => 'playing-cards', + 'Playmat' => 'playmat', + 'Playmobil' => 'playmobil', + 'Playmobil Advent Calendar' => 'playmobil-advent-calendar', + 'PlayStation' => 'playstation', + 'PlayStation 5 DualSense Controller' => 'ps5-controller', + 'PlayStation Accessories' => 'playstation-accessories', + 'PlayStation Classic' => 'playstation-classic', + 'PlayStation Move' => 'playstation-move', + 'PlayStation Now' => 'playstation-now', + 'PlayStation Plus' => 'playstation-plus', + 'PlayStation VR' => 'playstation-vr', + 'PlayStation VR Aim Controller' => 'aim-controller-ps4', + 'Pliers' => 'pliers', + 'Plumbing & Fittings' => 'plumbing-and-fitting', + 'Plus Size' => 'plus-size', + 'PNY' => 'pny', + 'POCO F2 Pro' => 'poco-f2-pro', + 'POCO F3' => 'poco-f3', + 'Poco M3' => 'poco-m3', + 'POCO X3' => 'poco-x3', + 'POCO X3 Pro' => 'poco-x3-pro', + 'Pokémon' => 'pokemon', + 'Pokémon: Let's Go' => 'pokemon-lets-go', + 'Pokémon Go' => 'pokemon-go', + 'Pokemon Sword and Shield' => 'pokemon-sword-and-shield', + 'Pokémon Ultra Sun and Ultra Moon' => 'pokemon-ultra-sun-ultra-moon', + 'Poker' => 'poker', + 'Pokken Tournament' => 'pokken-tournament', + 'Polaroid' => 'polaroid', + 'Police Toys' => 'police', + 'Polo Shirt' => 'polo-shirt', + 'Pool' => 'pool', + 'Pool & Snooker' => 'pool-table', + 'Popcorn' => 'popcorn', + 'Pork' => 'pork', + 'Porridge & Oats' => 'porridge-and-oats', + 'Portable Wireless Speaker' => 'wireless-speaker', + 'Poster' => 'poster', + 'Pots and Pans' => 'pan', + 'Potty' => 'potty', + 'Power Bank' => 'power-bank', + 'Powerbeats Pro' => 'powerbeats-pro', + 'Power Dental Flosser' => 'floss', + 'Powerline' => 'powerline', + 'Power Rangers' => 'power-rangers', + 'Power Tool' => 'power-tool', + 'Prada' => 'prada', + 'Pram' => 'pram', + 'Pregnancy' => 'pregnancy', + 'Prescription Glasses' => 'prescription-glasses', + 'Pressure Cooker' => 'pressure-cooker', + 'Pressure Washer' => 'pressure-washer', + 'Price Glitch' => 'price-glitch', + 'Prime Gaming' => 'twitch', + 'Pringles' => 'pringles', + 'Printer & Printer Supplies' => 'printer', + 'Printer Supplies' => 'printer-supplies', + 'Productivity App' => 'productivity-app', + 'Pro Evolution Soccer' => 'pro-evolution-soccer', + 'Pro Evolution Soccer 2018' => 'pro-evolution-soccer-2018', + 'Pro Evolution Soccer 2019' => 'pro-evolution-soccer-2019', + 'Pro Evolution Soccer 2020' => 'pes-2020', + 'Project Cars' => 'project-cars', + 'Project Cars 2' => 'project-cars-2', + 'Projector' => 'projector', + 'Protein' => 'protein', + 'Protein Bars' => 'protein-bars', + 'Protein Shaker' => 'shaker', + 'PS4' => 'ps4-slim', + 'PS4 Camera' => 'ps4-camera', + 'PS4 Controller' => 'ps4-controller', + 'PS4 Games' => 'ps4-games', + 'PS4 Headset' => 'ps4-headset', + 'PS4 Pro' => 'ps4-pro', + 'PS5' => 'ps5', + 'PS5 Games' => 'ps5-game', + 'PSU' => 'psu', + 'Public Transport' => 'public-transport', + 'Pukka' => 'pukka', + 'Pulse Light Epilator' => 'pulse-light-epilator', + 'Puma' => 'puma', + 'Puma Trainers' => 'puma-trainers', + 'Puppy Supplies' => 'puppy', + 'Purse' => 'purse', + 'Pushchair' => 'pushchair', + 'Pushchairs and Strollers' => 'baby-transport', + 'Puzzle' => 'puzzle', + 'PVR' => 'pvr', + 'Pyjamas' => 'pyjamas', + 'Pyrex' => 'pyrex', + 'Q Acoustics' => 'q-acoustics', + 'QNAP' => 'qnap', + 'Qualcast' => 'qualcast', + 'Quality Street' => 'quality-street', + 'Quantum Break' => 'quantum-break', + 'Quechua' => 'quechua', + 'Quick Charge' => 'quick-charge', + 'Quiksilver' => 'quiksilver', + 'Quinny' => 'quinny', + 'Quorn' => 'quorn', + 'Rab' => 'rab', + 'Radeon RX 480' => 'rx-480', + 'Radeon RX 5700' => 'radeon-rx-5700', + 'Radeon RX 5700 XT' => 'radeon-rx-5700-xt', + 'Radeon RX 6800' => 'radeon-rx-6800', + 'Radeon RX 6800 XT' => 'radeon-rx-6800-xt', + 'Radeon RX 6900 XT' => 'radeon-rx-6900-xt', + 'Radiator' => 'radiator', + 'Radio' => 'radio', + 'Radley' => 'radley', + 'Rage 2' => 'rage-2', + 'Railcard' => 'railcard', + 'Rainbow Six' => 'rainbow-six', + 'Rake' => 'rake', + 'Ralph Lauren' => 'ralph-lauren', + 'RAM' => 'ram', + 'Raspberry Pi' => 'raspberry-pi', + 'Ratchet' => 'ratchet', + 'Ratchet and Clank' => 'ratchet-and-clank', + 'Rattan Garden Furniture' => 'rattan', + 'RAVPower' => 'ravpower', + 'Ray Ban' => 'ray-ban', + 'Razer' => 'razer', + 'Razor' => 'razor', + 'Razor Blade' => 'razor-blade', + 'Real Madrid' => 'real-madrid', + 'Realme Smartphones' => 'realme-smartphone', + 'Real Techniques' => 'real-techniques', + 'Recliner' => 'recliner', + 'ReCore' => 'recore', + 'Recreational Sports' => 'recreational-sports', + 'Red Bull' => 'red-bull', + 'Red Dead Redemption' => 'red-dead-redemption', + 'Red Dead Redemption 2' => 'red-dead-redemption-2', + 'Redex' => 'redex', + 'Red Kite' => 'red-kite', + 'Reebok' => 'reebok', + 'Reese's' => 'reeses', + 'Regatta' => 'regatta', + 'Regina' => 'regina', + 'Remington' => 'remington', + 'Remote Control Car' => 'remote-control-car', + 'Renault' => 'renault', + 'Resident Evil' => 'resident-evil', + 'Resident Evil 2' => 'resident-evil-2', + 'Resident Evil 7' => 'resident-evil-7', + 'Restaurant, Café & Pub' => 'restaurant', + 'Retailer Offers and Issues' => 'retailer-offers-and-issues', + 'Ribena' => 'ribena', + 'Rice' => 'rice', + 'Rice Cooker' => 'rice-cooker', + 'Rick and Morty' => 'rick-and-morty', + 'Ricoh' => 'ricoh', + 'Ride On' => 'ride-on', + 'Ring' => 'ring', + 'Ring Door View Cam' => 'ring-door-view-cam', + 'Ring Fit Adventures' => 'ring-fit-adventures', + 'Ring Stick Up Cam' => 'ring-stick-up-cam', + 'Ring Video Doorbell' => 'ring-video-doorbell', + 'Ring Video Doorbell 2' => 'ring-video-doorbell-2', + 'Ring Video Doorbell 3' => 'ring-video-doorbell-3', + 'Ring Video Doorbell Pro' => 'ring-video-doorbell-pro', + 'Road Bike' => 'road-bike', + 'Roaming' => 'roaming', + 'Robinsons' => 'robinsons', + 'Robotic Lawnmower' => 'robotic-lawnmower', + 'Robot Vacuum Cleaner' => 'robot-vacuum-cleaner', + 'Rock Band' => 'rock-band', + 'Rocket League' => 'rocket-league', + 'Rocking Horse' => 'rocking-horse', + 'Rogue One: A Star Wars Story' => 'rogue-one', + 'Roku' => 'roku', + 'Rolex' => 'rolex', + 'Rollerskates' => 'skate', + 'Ronseal' => 'ronseal', + 'Roof Box' => 'roof-box', + 'Roses' => 'roses', + 'Rotary' => 'rotary', + 'Router' => 'router', + 'Rowenta' => 'rowenta', + 'RTX 2060' => 'rtx-2060', + 'RTX 2070' => 'rtx-2070', + 'RTX 2080' => 'rtx-2080', + 'RTX 2080 Ti' => 'rtx-2080-ti', + 'RTX 3070' => 'rtx-3070', + 'RTX 3080' => 'rtx-3080', + 'RTX 3090' => 'rtx-3090', + 'Rug' => 'rug', + 'Rugby' => 'rugby', + 'Rum' => 'rum', + 'Running' => 'running', + 'Running Shoes' => 'running-shoes', + 'Russell Hobbs' => 'russell-hobbs', + 'RX 570' => 'rx-570', + 'RX 580' => 'rx-580', + 'RX 590' => 'rx-590', + 'RX Vega 56' => 'rx-vega-56', + 'RX Vega 64' => 'rx-vega-64', + 'Ryanair' => 'ryanair', + 'Ryobi' => 'ryobi', + 'Safari' => 'safari', + 'Safety Boots' => 'safety-boots', + 'Sage by Heston Blumenthal' => 'sage', + 'Saints Row' => 'saints-row', + 'Saitek' => 'saitek', + 'Sale' => 'sale', + 'Salmon' => 'salmon', + 'Salomon' => 'salomon', + 'Salter' => 'salter', + 'Samsonite' => 'samsonite', + 'Samsung' => 'samsung', + 'Samsung Ecobubble' => 'ecobubble', + 'Samsung Fridge' => 'samsung-fridge', + 'Samsung Galaxy' => 'samsung-galaxy', + 'Samsung Galaxy A10' => 'samsung-galaxy-a10', + 'Samsung Galaxy A20e' => 'samsung-galaxy-a20e', + 'Samsung Galaxy A40' => 'samsung-galaxy-a40', + 'Samsung Galaxy A42 5G' => 'samsung-galaxy-a42-5g', + 'Samsung Galaxy A50' => 'samsung-galaxy-a50', + 'Samsung Galaxy A51' => 'samsung-galaxy-a51', + 'Samsung Galaxy A52 5G' => 'samsung-galaxy-a52', + 'Samsung Galaxy A60' => 'samsung-galaxy-a60', + 'Samsung Galaxy A70' => 'samsung-galaxy-a70', + 'Samsung Galaxy A71' => 'samsung-galaxy-a71', + 'Samsung Galaxy A72' => 'samsung-galaxy-a72', + 'Samsung Galaxy A80' => 'samsung-galaxy-a80', + 'Samsung Galaxy A90' => 'samsung-galaxy-a90', + 'Samsung Galaxy Buds' => 'samsung-galaxy-buds', + 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus', + 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live', + 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro', + 'Samsung Galaxy Fold' => 'samsung-galaxy-fold', + 'Samsung Galaxy J5' => 'galaxy-j5', + 'Samsung Galaxy Note' => 'samsung-galaxy-note', + 'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8', + 'Samsung Galaxy Note 9' => 'samsung-galaxy-note-9', + 'Samsung Galaxy Note 10' => 'samsung-galaxy-note-10', + 'Samsung Galaxy Note 10+' => 'samsung-galaxy-note-10-plus', + 'Samsung Galaxy Note20' => 'samsung-galaxy-note20', + 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note20-ultra', + 'Samsung Galaxy S6' => 'samsung-galaxy-s6', + 'Samsung Galaxy S7' => 'samsung-galaxy-s7', + 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge', + 'Samsung Galaxy S8' => 'samsung-galaxy-s8', + 'Samsung Galaxy S8+' => 'samsung-s8-plus', + 'Samsung Galaxy S9' => 'samsung-galaxy-s9', + 'Samsung Galaxy S9 Plus' => 'samsung-s9-plus', + 'Samsung Galaxy S10' => 'samsung-galaxy-s10', + 'Samsung Galaxy S10 Lite' => 'samsung-galaxy-s10-lite', + 'Samsung Galaxy S10 Plus' => 'samsung-galaxy-s10-plus', + 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e', + 'Samsung Galaxy S20' => 'samsung-galaxy-s20', + 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe', + 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra', + 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus', + 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g', + 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g', + 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g', + 'Samsung Galaxy Tab' => 'samsung-galaxy-tab', + 'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a', + 'Samsung Galaxy Tab A7' => 'samsung-galaxy-tab-a7', + 'Samsung Galaxy Tab S' => 'samsung-galaxy-tab-s', + 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4', + 'Samsung Galaxy Tab S5e' => 'samsung-galaxy-tab-s5e', + 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6', + 'Samsung Galaxy Watch' => 'samsung-galaxy-watch', + 'Samsung Galaxy Watch3' => 'samsung-galaxy-watch3', + 'Samsung Galaxy Watch Active2' => 'samsung-galaxy-watch-active-2', + 'Samsung Gear' => 'samsung-gear', + 'Samsung Gear S3' => 'gear-s3', + 'Samsung Gear VR' => 'samsung-gear-vr', + 'Samsung Headphones' => 'samsung-headphones', + 'Samsung Monitor' => 'samsung-monitor', + 'Samsung QLED TVs' => 'samsung-qled-tv', + 'Samsung Smartphone' => 'samsung-smartphone', + 'Samsung SSD' => 'samsung-ssd', + 'Samsung The Frame TV' => 'samsung-the-frame', + 'Samsung TV' => 'samsung-tv', + 'Samsung Washing Machine' => 'samsung-washing-machine', + 'Samsung Watch' => 'samsung-watch', + 'Sandals' => 'sandals', + 'Sander' => 'sander', + 'SanDisk' => 'sandisk', + 'SanDisk SSD' => 'sandisk-ssd', + 'Sand Pit' => 'sand-pit', + 'Sandwich Maker' => 'sandwich', + 'San Miguel' => 'san-miguel', + 'Santander' => 'santander', + 'Satchel' => 'satchel', + 'Sat Nav' => 'sat-nav', + 'Sauce' => 'sauce', + 'Saw' => 'saw', + 'Scalextric' => 'scalextric', + 'Scanner' => 'scanner', + 'School Bag' => 'school-bag', + 'School Supplies' => 'school', + 'School Uniform' => 'school-uniform', + 'Schwalbe' => 'schwalbe', + 'Scooby Doo' => 'scooby-doo', + 'Scooter' => 'scooter', + 'Scotch Whisky' => 'scotch', + 'Scrabble' => 'scrabble', + 'Screen Protector' => 'screen-protector', + 'Screenwash' => 'screenwash', + 'Screwdriver' => 'screwdriver', + 'Screws' => 'screws', + 'SD Cards' => 'sd-card', + 'SDHC' => 'sdhc', + 'SDXC' => 'sdxc', + 'Seagate' => 'seagate', + 'Sea Life' => 'sea-life', + 'Sea of Thieves' => 'sea-of-thieves', + 'Season Pass' => 'season-pass', + 'Seaworld' => 'seaworld', + 'Security Camera' => 'security-camera', + 'Seeds & Bulbs' => 'seeds-and-bulbs', + 'Sega' => 'sega', + 'SEGA Mega Drive Mini' => 'sega-mega-drive-mini', + 'Segway' => 'segway', + 'Seiko' => 'seiko', + 'Sekiro: Shadows Die Twice' => 'sekiro', + 'Sekonda' => 'sekonda', + 'Selfie Stick' => 'selfie-stick', + 'Sennheiser' => 'sennheiser', + 'Sennheiser Headphones' => 'sennheiser-headphones', + 'Sensodyne' => 'sensodyne', + 'Server' => 'server', + 'Services & Contracts' => 'services-contracts', + 'Services and Subscriptions' => 'service-contract', + 'Sewing' => 'sewing', + 'Sewing Machine' => 'sewing-machine', + 'Sex Toys' => 'sex-toys', + 'Shadow of the Tomb Raider' => 'shadow-of-the-tomb-raider', + 'Shampoo' => 'shampoo', + 'Shark' => 'shark', + 'Shark DuoClean' => 'shark-duoclean', + 'Shark Vacuum Cleaner' => 'shark-vacuum-cleaner', + 'Sharp' => 'sharp', + 'Sharpener' => 'sharpener', + 'Sharpie' => 'sharpie', + 'Shaver' => 'shaver', + 'Shaving & Beard Care' => 'shaving', + 'Shaving, Trimming, & Hair Removal' => 'hair-removal', + 'Shaving Foam' => 'shaving-foam', + 'Shears' => 'shears', + 'Sheba' => 'sheba', + 'Shed' => 'shed', + 'Shelter' => 'shelter', + 'Shelves' => 'shelves', + 'Shenmue I & II' => 'shenmue-one-and-two', + 'Shenmue III' => 'shenmue-3', + 'Shenmue Series' => 'shenmue-series', + 'Shimano' => 'shimano', + 'Shirt' => 'shirt', + 'Shoe Rack' => 'shoe-rack', + 'Shoes' => 'shoe', + 'Shopkins' => 'shopkins', + 'Shortbread' => 'shortbread', + 'Shorts' => 'shorts', + 'Short Trip' => 'break', + 'Shoulder Bag' => 'shoulder-bag', + 'Shovel' => 'shovel', + 'Shower Curtain' => 'shower-curtain', + 'Shower Enclosure' => 'shower-enclosure', + 'Shower Fittings' => 'shower', + 'Shower Gel' => 'shower-gel', + 'Shower Head' => 'shower-head', + 'Shredder' => 'shredder', + 'Side-by-Side-Fridge' => 'side-by-side-fridge', + 'Sideboard' => 'sideboard', + 'Sid Meier's Civilization VI' => 'civilization-vi', + 'Siemens' => 'siemens', + 'Siemens Washing Machine' => 'siemens-washing-machine', + 'Sigma' => 'sigma', + 'Silentnight' => 'silentnight', + 'Silvercrest' => 'silvercrest', + 'Silver Cross' => 'silver-cross', + 'Sim Free' => 'sim-free', + 'Sim Only' => 'sim-only', + 'Simplehuman' => 'simplehuman', + 'Simpsons' => 'simpsons', + 'Single Malt' => 'single-malt', + 'Sink' => 'sink', + 'Sistema' => 'sistema', + 'Skateboard' => 'skateboard', + 'Skating' => 'skating', + 'Skechers' => 'skechers', + 'Skiing' => 'ski', + 'Skin Care' => 'skincare', + 'Skittles' => 'skittles', + 'Skoda' => 'skoda', + 'Skullcandy' => 'skullcandy', + 'Sky' => 'sky', + 'Sky Cinema' => 'sky-cinema', + 'Skylanders' => 'skylanders', + 'Skylanders Battlecast' => 'skylanders-battlecast', + 'Skylanders Imaginators' => 'skylanders-imaginators', + 'Sleeping Bag' => 'sleeping-bag', + 'Sleeping Dogs' => 'sleeping-dogs', + 'Sleepwear' => 'sleepwear', + 'Slide' => 'slide', + 'Slimming World' => 'slimming-world', + 'Slippers' => 'slippers', + 'Slow Cooker' => 'slow-cooker', + 'Smart Clock' => 'clock', + 'Smart Doorbells' => 'smart-doorbell', + 'Smart Home' => 'smart-home', + 'Smart Light' => 'smart-light', + 'Smart Lock' => 'smart-lock', + 'Smartphone Accessories' => 'smartphone-accessories', + 'Smartphone Case' => 'smartphone-case', + 'Smartphone under £200' => 'smartphone-under-200-pounds', + 'Smartphone under £400' => 'smartphone-under-400-pounds', + 'Smart Plugs' => 'smart-plugs', + 'Smart Speaker' => 'smart-speaker', + 'Smart Tech & Gadgets' => 'smart-tech', + 'Smart Thermostat' => 'thermostat', + 'SmartThings' => 'smartthings', + 'Smart TV' => 'smart-tv', + 'Smart Watch' => 'smartwatch', + 'Smeg' => 'smeg', + 'Smirnoff' => 'smirnoff', + 'Smoke Alarm' => 'smoke-alarm', + 'Smoothie' => 'smoothie', + 'Smoothie Maker' => 'smoothie-maker', + 'Snacks' => 'snacks', + 'Sneakers' => 'sneakers', + 'SNES Nintendo Classic Mini' => 'snes-nintendo-classic', + 'Snickers' => 'snickers', + 'Sniper Elite' => 'sniper-elite', + 'Snowboard' => 'snowboard', + 'Snow Boots' => 'snow-boots', + 'Soap' => 'soap', + 'Soap and Glory' => 'soap-and-glory', + 'Socket Set' => 'socket-set', + 'Socks' => 'socks', + 'SodaStream' => 'soda-stream', + 'Sofa' => 'sofa', + 'Soft Drinks' => 'soft-drinks', + 'Soft Toy' => 'soft-toy', + 'Software' => 'software', + 'Software & Apps' => 'software-apps', + 'Solar Lights' => 'solar-lights', + 'Soldering Iron' => 'soldering', + 'Sonic' => 'sonic', + 'Sonos' => 'sonos', + 'Sonos Beam' => 'sonos-beam', + 'Sonos Move' => 'sonos-move', + 'Sonos One' => 'sonos-one', + 'Sonos PLAY:1' => 'sonos-play-1', + 'Sonos PLAY:3' => 'sonos-play-3', + 'Sonos PLAY:5' => 'sonos-play-5', + 'Sonos PLAYBAR' => 'sonos-playbar', + 'Sonos PLAYBASE' => 'sonos-playbase', + 'Sony' => 'sony', + 'Sony Camera' => 'sony-camera', + 'Sony Headphones' => 'sony-headphones', + 'Sony Pulse 3D Wireless Headset' => 'pulse-3d-wireless-headsets', + 'Sony TV' => 'sony-tv', + 'Sony WF-1000XM3' => 'sony-wf1000xm3', + 'Sony WH-1000XM3' => 'sony-wh-1000xm3', + 'Sony WH-1000XM4' => 'sony-wh1000xm4', + 'Sony Xperia' => 'xperia', + 'Sony Xperia 5' => 'sony-xperia-5', + 'Sony Xperia 10' => 'sony-xperia-10', + 'Sony Xperia Xa' => 'sony-xperia-xa', + 'Sony Xperia Z3' => 'xperia-z3', + 'Sony Xperia Z5' => 'xperia-z5', + 'Soulcalibur' => 'soulcalibur', + 'Soundbar' => 'soundbar', + 'Soundbase' => 'soundbase', + 'Sound Card' => 'sound-card', + 'Soundmagic' => 'soundmagic', + 'Soup' => 'soup', + 'Soup Maker' => 'soup-maker', + 'Sous-Vide' => 'sousvide', + 'Southern Comfort' => 'southern-comfort', + 'South Park' => 'south-park', + 'Spa' => 'spa', + 'Spade' => 'spade', + 'Spanner' => 'spanner', + 'Speaker' => 'speakers', + 'Specialized' => 'specialized', + 'Speedo' => 'speedo', + 'Sphero' => 'sphero', + 'Spice Rack' => 'spice-rack', + 'Spiderman' => 'spiderman', + 'Spiralizer' => 'spiralizer', + 'Spirit & Liqueur' => 'spirits', + 'Spirit Level' => 'spirit-level', + 'Splatoon' => 'splatoon', + 'Sports & Outdoors' => 'sports-fitness', + 'Sports Events' => 'sports-events', + 'Sports Nutrition' => 'nutrition', + 'Spreads' => 'spreads', + 'Spyro Reignited Trilogy' => 'spyro-reignited-trilogy', + 'SSD' => 'ssd', + 'SSHD' => 'sshd', + 'Staedtler' => 'staedtler', + 'Stair Gate' => 'stair-gate', + 'Stanley' => 'stanley', + 'Stapler' => 'stapler', + 'Starbucks' => 'starbucks', + 'Starlink: Battle for Atlas' => 'starlink-battle-for-atlas', + 'Star Ocean' => 'star-ocean', + 'Star Trek' => 'star-trek', + 'Star Wars' => 'star-wars', + 'Star Wars: Battlefront' => 'star-wars-battlefront', + 'Star Wars: Battlefront II' => 'star-wars-battlefront-2', + 'Star Wars: Squadrons' => 'star-wars-squadrons', + 'Star Wars Jedi: Fallen Order' => 'star-wars-jedi-fallen-order', + 'Stationery' => 'stationery', + 'Stationery & Office Supplies' => 'stationery-office-supplies', + 'Staycation' => 'staycation', + 'Steak' => 'steak', + 'Steam Cleaner' => 'steam-cleaner', + 'Steam Controller' => 'steam-controller', + 'Steamer' => 'steamer', + 'Steam Gaming' => 'steam', + 'Steam Iron' => 'steam-iron', + 'Steam Link' => 'steam-link', + 'Steam Mop' => 'steam-mop', + 'SteelSeries' => 'steelseries', + 'Steering Wheel' => 'steering-wheel', + 'Stella' => 'stella', + 'Stool' => 'stool', + 'Storage Box' => 'storage-box', + 'Stormtrooper' => 'stormtrooper', + 'Straightener' => 'straightener', + 'Streaming' => 'streaming', + 'Street Fighter' => 'street-fighter', + 'Street Fighter V' => 'street-fighter-v', + 'Streetwear' => 'streetwear', + 'Strimmer' => 'strimmer', + 'Strongbow' => 'strongbow', + 'Student Discount' => 'student-discount', + 'Subwoofer' => 'subwoofer', + 'Suitcase' => 'suitcase', + 'Suncare' => 'suncare', + 'Sun Cream' => 'sun-cream', + 'Sunglasses' => 'sunglasses', + 'Superdry' => 'superdry', + 'Superfast Broadband' => 'superfast-broadband', + 'Superking' => 'superking', + 'Super Mario' => 'mario', + 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars', + 'Super Mario 3D World' => 'super-mario-3d-world', + 'Super Mario Maker 2' => 'super-mario-maker-2', + 'Super Mario Odyssey' => 'super-mario-odyssey', + 'Super Mario Party' => 'mario-party', + 'Supermarket' => 'supermarket', + 'Super Smash Bros.' => 'super-smash-bros', + 'Surf' => 'surf', + 'Swarovski' => 'swarovski', + 'Sweets' => 'sweets', + 'Swimming' => 'swimming', + 'Swimming Goggles' => 'goggles', + 'Swimwear' => 'swimwear', + 'Swing' => 'swing', + 'Swingball' => 'swingball', + 'Syberia' => 'syberia', + 'Sylvanian' => 'sylvanian', + 'Synology' => 'synology', + 'T-Mobile' => 't-mobile', + 'T-Shirt' => 't-shirt', + 'Table Lamp' => 'table-lamp', + 'Tablet' => 'tablet', + 'Tablet Accessories' => 'tablet-accessories', + 'Table Tennis' => 'table-tennis', + 'Tableware' => 'tableware', + 'Tacx' => 'tacx', + 'Tado' => 'tado', + 'Tag Heuer' => 'tag-heuer', + 'Takeaway and Food Delivery' => 'takeaway', + 'Tales of Vesperia: Definitive Edition' => 'tales-of-vesperia-definitive-edition', + 'Talisker' => 'talisker', + 'Talkmobile' => 'talkmobile', + 'Tamron' => 'tamron', + 'Tangle Teezer' => 'tangle-teezer', + 'Tank Top' => 'tank-top', + 'Tannoy' => 'tannoy', + 'Tanqueray' => 'tanqueray', + 'Tape' => 'tape', + 'Tassimo' => 'tassimo', + 'Tassimo Coffee Machine' => 'tassimo-coffee-machine', + 'tastecard' => 'tastecard', + 'Taxi' => 'taxi', + 'Tea' => 'tea', + 'Team Sonic Racing' => 'team-sonic-racing', + 'Team Sports' => 'team-sports', + 'Teapot' => 'teapot', + 'Technika' => 'technika', + 'Techwood' => 'techwood', + 'Ted Baker' => 'ted-baker', + 'Teddy Bear' => 'teddy-bear', + 'Teenage Mutant Ninja Turtles' => 'turtle', + 'Teeth Care' => 'teeth-care', + 'Teeth Whitening' => 'teeth-whitening', + 'Tefal' => 'tefal', + 'Tefal Actifry' => 'actifry', + 'Tefal Pan' => 'tefal-pan', + 'Tekken' => 'tekken', + 'Tekken 7' => 'tekken-7', + 'Telegraph' => 'telegraph', + 'Telescope' => 'telescope', + 'Telltale' => 'telltale', + 'Tennis' => 'tennis', + 'Tent' => 'tent', + 'Tequila' => 'tequila', + 'Tesco Clothing' => 'tesco-clothing', + 'Tesla' => 'tesla', + 'Tetris' => 'tetris', + 'Tetris 99' => 'tetris-99', + 'Theatre & Musical' => 'theatre', + 'The Beatles' => 'beatles', + 'The Big Bang Theory' => 'big-bang-theory', + 'The Crew' => 'the-crew', + 'The Dark Pictures: Anthology Man of Medan' => 'the-dark-pictures-anthology-man-of-medan', + 'The Elder Scrolls' => 'elder-scrolls', + 'The Elder Scrolls V: Skyrim' => 'skyrim', + 'The Evil Within' => 'the-evil-within', + 'The Evil Within 2' => 'the-evil-within-2', + 'The Last Guardian' => 'the-last-guardian', + 'The Last of Us' => 'the-last-of-us', + 'The Last of Us Part II' => 'the-last-of-us-part-2', + 'The Legend of Zelda' => 'zelda', + 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild', + 'The Legend of Zelda: Link's Awakening' => 'the-legend-of-zelda-links-awakening', + 'The Legend of Zelda: Skyward Sword HD' => 'the-legend-of-zelda-skyward-sword-hd', + 'Theme Park' => 'theme-park', + 'The North Face' => 'north-face', + 'The Outer Worlds' => 'the-outer-worlds', + 'Thermos Storage' => 'thermos', + 'The Sims' => 'sims', + 'The Sims 4' => 'the-sims-4', + 'The Sinking City' => 'the-sinking-city', + 'The Sun' => 'the-sun', + 'The Sunday Times' => 'sunday-times', + 'The Walking Dead' => 'walking-dead', + 'The Witcher' => 'witcher', + 'The Witcher 3' => 'the-witcher-3', + 'Thierry Mugler' => 'thierry-mugler', + 'Thomas Sabo' => 'thomas-sabo', + 'Thomas The Tank Engine' => 'thomas-the-tank', + 'Thornton's' => 'thorntons', + 'Thorpe Park' => 'thorpe-park', + 'Throw' => 'throw', + 'Thrustmaster' => 'thrustmaster', + 'Thule' => 'thule', + 'Tickets & Shows' => 'tickets-shows', + 'Tie' => 'tie', + 'Tights' => 'tights', + 'TIGI' => 'tigi', + 'Tilda' => 'tilda', + 'Tile' => 'tile', + 'Timberland' => 'timberland', + 'Timex' => 'timex', + 'Tissot' => 'tissot', + 'Tissues' => 'tissues', + 'Titanfall' => 'titanfall', + 'Titanfall 2' => 'titanfall-2', + 'Toaster' => 'toaster', + 'Toblerone' => 'toblerone', + 'Toddler Bed' => 'toddler-bed', + 'Toilet Brush' => 'brush', + 'Toilet Cleaner' => 'toilet', + 'Toilet Roll' => 'toilet-roll', + 'Toilet Seat' => 'toilet-seat', + 'Tokyo Laundry' => 'tokyo-laundry', + 'Tomb Raider' => 'tomb-raider', + 'Tom Clancy's' => 'tom-clancy', + 'Tom Clancy's: Ghost Recon' => 'ghost-recon', + 'Tom Clancy's Ghost Recon: Wildlands' => 'ghost-recon-wildlands', + 'Tom Clancy's Ghost Recon Breakpoint' => 'tom-clancys-ghost-recon-breakpoint', + 'Tom Clancy's The Division' => 'tom-clancy-the-division', + 'Tom Clancy's The Division 2' => 'tom-clancy-the-division-2', + 'Tom Ford' => 'tom-ford', + 'Tommee Tippee' => 'tommee-tippee', + 'Tommy Hilfiger' => 'tommy-hilfiger', + 'Toms' => 'toms', + 'TomTom' => 'tomtom', + 'Tonic Water' => 'tonic-water', + 'Tony Hawk's Pro Skater 1 + 2' => 'tony-hawks-pro-skater-1-2', + 'Tools' => 'tool', + 'Toothbrush' => 'toothbrush', + 'Toothpaste' => 'toothpaste', + 'Torch' => 'torch', + 'Torque Wrench' => 'torque-wrench', + 'Toshiba' => 'toshiba', + 'Toshiba Laptop' => 'toshiba-laptop', + 'Toshiba TV' => 'toshiba-tv', + 'Total War' => 'total-war', + 'Tottenham Hotspur F. C.' => 'tottenham', + 'Towel' => 'towel', + 'Toy Box' => 'toy-box', + 'Toy Cars' => 'toy-cars', + 'Toy Castle' => 'castle', + 'Toy Digger' => 'digger', + 'Toy Helicopter' => 'helicopter', + 'Toy Kitchen' => 'toy-kitchen', + 'Toy Mask' => 'mask', + 'Toyota' => 'toyota', + 'Toys' => 'toy', + 'Toy Story' => 'toy-story', + 'Toy Tractor' => 'tractor', + 'Toy Train' => 'train', + 'TP-Link' => 'tp-link', + 'TP-Link Archer' => 'archer', + 'TP-Link Router' => 'tp-link-router', + 'Tracksuit' => 'tracksuit', + 'Trainers' => 'trainers', + 'Trains & Buses' => 'train-and-bus-ticket', + 'Train Ticket' => 'train-ticket', + 'Trampoline' => 'trampoline', + 'Transcend' => 'transcend', + 'Transformers' => 'transformers', + 'Travel' => 'travel', + 'Travel App' => 'travel-app', + 'Travel Insurance' => 'travel-insurance', + 'Travelodge' => 'travelodge', + 'Travel System' => 'travel-system', + 'Treadmill' => 'treadmill', + 'TRESemmé' => 'tresemme', + 'Trespass' => 'trespass', + 'Triathlon' => 'triathlon', + 'Trike' => 'trike', + 'Trine 4' => 'trine-4', + 'Tripod' => 'tripod', + 'Tripp' => 'tripp', + 'Triton Shower' => 'triton', + 'Trolley Bag' => 'trolley', + 'Tropico 5' => 'tropico-5', + 'Tropico 6' => 'tropico-6', + 'Tropico Series' => 'tropico-deals', + 'Trousers' => 'trousers', + 'True Wireless Earbuds' => 'wireless-earphones', + 'Trunki' => 'trunki', + 'Tumble Dryer' => 'tumble-dryer', + 'Tuna' => 'tuna', + 'Turbo Trainer' => 'turbo-trainer', + 'Turntable' => 'turntable', + 'Turtle Beach' => 'turtle-beach', + 'TV' => 'tv', + 'TV & Video' => 'tv-video', + 'TV Accessories' => 'tv-accessories', + 'TV Mount' => 'tv-mount', + 'TV Series' => 'tv-series', + 'TV Stand' => 'tv-stand', + 'Twinings' => 'twinings', + 'Twin Peaks' => 'twin-peaks', + 'Twix' => 'twix', + 'Typhoo' => 'typhoo', + 'Tyres' => 'tyres', + 'Ubisoft' => 'ubisoft', + 'UE BOOM' => 'ue-boom', + 'UE Boom 2' => 'ue-boom-2', + 'UEFA' => 'uefa', + 'UE Megablast' => 'ue-megablast', + 'UE Megaboom' => 'ue-megaboom', + 'UGG' => 'ugg', + 'Ulefone' => 'ulefone', + 'Ultrabook' => 'ultrabook', + 'Ultrawide Monitor' => 'ultrawide', + 'Umbrella' => 'umbrella', + 'UMI' => 'umidigi', + 'Uncharted' => 'uncharted', + 'Uncharted 4: A Thief's End' => 'uncharted-4', + 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy', + 'Under Armour' => 'under-armour', + 'Underwear' => 'underwear', + 'Unicorn' => 'unicorn', + 'UNiDAYS' => 'unidays', + 'Universal Remote' => 'universal-remote', + 'Uno' => 'uno', + 'Uplay' => 'uplay', + 'Urban Decay' => 'urban-decay', + 'Urban Sports' => 'urban-sports', + 'USB Cable' => 'usb-cable', + 'USB Hub' => 'usb-hub', + 'USB Memory Stick' => 'flash-drive', + 'USB Type C' => 'usb-type-c', + 'USN' => 'usn', + 'Vacuum Cleaner' => 'vacuum-cleaners', + 'Vacuum Flask' => 'flask', + 'Valkyria Chronicles' => 'valkyria-chronicles', + 'Valkyria Chronicles 4' => 'valkyria-chronicles-4', + 'Vango' => 'vango', + 'Vanish' => 'vanish', + 'Vans' => 'vans', + 'Vans Old Skool' => 'vans-old-skool', + 'Vans Shoes' => 'vans-shoes', + 'Vase' => 'vase', + 'Vaseline' => 'vaseline', + 'Vauxhall' => 'vauxhall', + 'VAX' => 'vax', + 'Vax Blade' => 'vax-blade', + 'Vax Vacuum Cleaner' => 'vax-vacuum', + 'Veet' => 'veet', + 'Vega 7' => 'vega-7', + 'Vegetables' => 'vegetables', + 'Vegetarian' => 'vegetarian', + 'Vehicles' => 'vehicles', + 'Velvet Comfort' => 'velvet', + 'Vera Wang' => 'vera-wang', + 'Verbatim' => 'verbatim', + 'Versace' => 'versace', + 'Vibrator' => 'vibrator', + 'Victorinox' => 'victorinox', + 'Video Games' => 'videogame', + 'Video Streaming' => 'video-streaming', + 'Viktor & Rolf Spicebomb' => 'spicebomb', + 'Vileda' => 'vileda', + 'Villeroy & Boch' => 'villeroy-boch', + 'Viners' => 'viners', + 'Vinyl' => 'vinyl', + 'Virgin' => 'virgin', + 'Vitamins & Supplements' => 'vitamins', + 'Vitamix' => 'vitamix', + 'Vodafone' => 'vodafone', + 'Vodka' => 'vodka', + 'Volvo' => 'volvo', + 'VPN' => 'vpn', + 'VR Headset' => 'vr-headset', + 'VTech' => 'vtech', + 'VTech Toot Toot' => 'toot-toot', + 'Vue' => 'vue', + 'VW' => 'vw', + 'Wacom' => 'wacom', + 'Waffle Maker' => 'waffle-maker', + 'Wahl' => 'wahl', + 'Walkers' => 'walkers', + 'Walking Boots' => 'walking-boots', + 'Wall Art' => 'wall-art', + 'Wallet' => 'wallet', + 'Wallpaper' => 'wallpaper', + 'Wardrobe' => 'wardrobe', + 'Warhammer' => 'warhammer', + 'Washbag' => 'washbag', + 'Washer Dryer' => 'washer-dryer', + 'Washing Machine' => 'washing-machine', + 'Washing Powder' => 'washing-powder', + 'Watch' => 'watch', + 'Watch Dogs' => 'watch-dogs', + 'Watch Dogs 2' => 'watch-dogs-2', + 'Watch Dogs: Legion' => 'watch-dogs-legion', + 'Water Bottle' => 'water-bottle', + 'Water Butt' => 'water-butt', + 'Water Dispenser' => 'water-dispenser', + 'Water Filter' => 'water-filter', + 'Water Gun' => 'water-gun', + 'Waterproof Camera' => 'waterproof-camera', + 'Waterproof Jacket' => 'waterproof-jacket', + 'Watersports' => 'watersport', + 'Water Toys' => 'water-toys', + 'Wayfarer' => 'wayfarer', + 'WD40' => 'wd40', + 'Wearable' => 'wearable', + 'Weather Station' => 'weather-station', + 'Webcam' => 'webcam', + 'Weber' => 'weber', + 'Web Hosting' => 'web-hosting', + 'Wedding' => 'wedding', + 'Weed Killer' => 'weed', + 'Weekend Break' => 'weekend-break', + 'Weetabix' => 'weetabix', + 'Weightlifting' => 'weightlifting', + 'Weight Watchers' => 'weight-watchers', + 'Wellies' => 'wellies', + 'Wellness and Health' => 'wellness-and-health', + 'Wenger' => 'wenger', + 'Western Digital' => 'western-digital', + 'Wetsuit' => 'wetsuit', + 'Wheelbarrow' => 'wheelbarrow', + 'Wheelchair' => 'wheelchair', + 'Whey' => 'whey', + 'Whiskas' => 'whiskas', + 'Whisky' => 'whisky', + 'Whole Home Mesh Wi-Fi System' => 'whole-home-mesh-wifi-system', + 'Wi-Fi Camera' => 'wifi-camera', + 'Wi-Fi Dongle' => 'dongle', + 'Wi-Fi Extender' => 'wifi-extender', + 'Wii' => 'wii', + 'Wii Game' => 'wii-games', + 'Wii U Game' => 'wii-u-game', + 'Wii U Pro Controller' => 'wii-u-pro-controller', + 'Wild Turkey' => 'wild-turkey', + 'Wileyfox' => 'wileyfox', + 'Wilkinson Sword Hydro 5' => 'hydro-5', + 'Wilkinson Sword Razor' => 'wilkinson-sword', + 'Wimbledon Tennis' => 'wimbledon', + 'Window Cleaner' => 'window-cleaner', + 'Windows' => 'windows', + 'Windows 8' => 'windows-8', + 'Windows 10' => 'windows-10', + 'Wine' => 'wine', + 'Wine Advent Calendar' => 'wine-advent-calendar', + 'Wine Glasses' => 'wine-glasses', + 'Winter Jacket' => 'winter-jacket', + 'Wiper Blades' => 'wiper-blades', + 'Wireless Adapter' => 'wireless-adapter', + 'Wireless Charger' => 'wireless-charger', + 'Wireless Controller' => 'wireless-controller', + 'Wireless Headphones' => 'wireless-headphones', + 'Wireless Headset' => 'wireless-headset', + 'Wireless Keyboard' => 'wireless-keyboard', + 'Wireless Mouse' => 'wireless-mouse', + 'Wok' => 'wok', + 'Wolfenstein' => 'wolfenstein', + 'Wolfenstein 2: The New Colossus' => 'wolfenstein-2', + 'Women's Boots' => 'womens-boots', + 'Women's Fragrance' => 'womens-fragrance', + 'Women's Shoes' => 'womens-shoes', + 'Workbench' => 'workbench', + 'World of Warcraft' => 'world-of-warcraft', + 'World War Z' => 'world-war-z', + 'WORX' => 'worx', + 'Wreckfest' => 'wreckfest', + 'Wuaki' => 'wuaki', + 'WWE 2K' => 'wwe', + 'Xbox' => 'xbox', + 'Xbox 360 Game' => 'xbox-360-game', + 'Xbox Accessories' => 'xbox-accessories', + 'Xbox Controller' => 'xbox-controller', + 'Xbox Game Pass' => 'xbox-game-pass', + 'Xbox Gift Card' => 'xbox-gift-card', + 'Xbox Headset' => 'xbox-headset', + 'Xbox Kinect' => 'kinect', + 'Xbox Live' => 'xbox-live', + 'Xbox One Controller' => 'xbox-one-controller', + 'Xbox One Elite Controller' => 'xbox-one-elite-controller', + 'Xbox One Games' => 'xbox-one-games', + 'Xbox One S' => 'xbox-one-s', + 'Xbox One X' => 'xbox-one-x', + 'Xbox Series S' => 'xbox-series-s', + 'Xbox Series X' => 'xbox-series-x', + 'Xbox Series X Controller' => 'xbox-series-x-controller', + 'Xbox Series X Games' => 'xbox-series-x-game', + 'Xbox Wireless Adapter' => 'xbox-wireless-adapter', + 'Xbox Wireless Headset' => 'xbox-wireless-headset', + 'XCOM' => 'xcom', + 'XCOM 2' => 'xcom-2', + 'Xenoblade Chronicles' => 'xenoblade-chronicles', + 'XFX' => 'xfx', + 'Xiaomi' => 'xiaomi', + 'Xiaomi AirDots' => 'xiaomi-airdots', + 'Xiaomi Black Shark' => 'xiaomi-black-shark', + 'Xiaomi Black Shark 2' => 'xiaomi-black-shark-2', + 'Xiaomi Headphones' => 'xiaomi-headphones', + 'Xiaomi Laptop' => 'xiaomi-laptop', + 'Xiaomi Mi 5' => 'xiaomi-mi-5', + 'Xiaomi Mi 6' => 'xiaomi-mi-6', + 'Xiaomi Mi 8' => 'xiaomi-mi-8', + 'Xiaomi Mi 8 Lite' => 'xiaomi-mi-8-lite', + 'Xiaomi Mi 8 Pro' => 'xiaomi-mi-8-pro', + 'Xiaomi Mi 9' => 'xiaomi-mi-9', + 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite', + 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se', + 'Xiaomi Mi 9T' => 'xiaomi-mi-9t', + 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro', + 'Xiaomi Mi 10' => 'xiaomi-mi-10', + 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite', + 'Xiaomi Mi 10T' => 'xiaomi-mi-10t', + 'Xiaomi Mi 10T Lite' => 'xiaomi-mi-10t-lite', + 'Xiaomi Mi 10T Pro' => 'xiaomi-mi-10t-pro', + 'Xiaomi Mi 11' => 'xiaomi-mi-11', + 'Xiaomi Mi 11 Lite 4G' => 'xiaomi-mi-11-lite-4g', + 'Xiaomi Mi 11 Lite 5G' => 'xiaomi-mi-11-lite-5g', + 'Xiaomi Mi 11 Pro' => 'xiaomi-mi-11-pro', + 'Xiaomi Mi 11 Ultra' => 'xiaomi-mi-11-ultra', + 'Xiaomi Mi 11i' => 'xiaomi-mi-11i', + 'Xiaomi Mi A1' => 'xiaomi-mi-a1', + 'Xiaomi Mi A2' => 'mi-a2', + 'Xiaomi Mi A3' => 'xiaomi-mi-a3', + 'Xiaomi Mi Band' => 'xiaomi-mi-band', + 'Xiaomi Mi Band 3' => 'xiaomi-mi-band-3', + 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4', + 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5', + 'Xiaomi Mi Box' => 'xiaomi-mi-box', + 'Xiaomi Mi Max 3' => 'xiaomi-mi-max3', + 'Xiaomi Mi Mix' => 'xiaomi-mi-mix', + 'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2', + 'Xiaomi Mi Mix 2S' => 'xiaomi-mi-mix-2s', + 'Xiaomi Mi Mix 3' => 'xiaomi-mi-mix-3', + 'Xiaomi Mi Note' => 'xiaomi-mi-note', + 'Xiaomi Mi Note 10' => 'mi-note-10', + 'Xiaomi Mi Pad 4' => 'xiaomi-mi-pad-4', + 'Xiaomi Pocophone F1' => 'pocophone-f1', + 'Xiaomi Redmi' => 'redmi', + 'Xiaomi Redmi 4' => 'xiaomi-redmi-4', + 'Xiaomi Redmi 5' => 'redmi-5', + 'Xiaomi Redmi 6' => 'redmi-6', + 'Xiaomi Redmi 8' => 'redmi-8', + 'Xiaomi Redmi Note 4' => 'note-4', + 'Xiaomi Redmi Note 5' => 'redmi-note-5', + 'Xiaomi Redmi Note 6' => 'redmi-note-6', + 'Xiaomi Redmi Note 6 Pro' => 'xiaomi-redmi-note-6-pro', + 'Xiaomi Redmi Note 7' => 'redmi-note-7', + 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8', + 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro', + 'Xiaomi Redmi Note 8T' => 'redmi-note-8t', + 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9', + 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro', + 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s', + 'Xiaomi Roborock' => 'xiaomi-roborock', + 'Xiaomi Roborock S5' => 'xiaomi-roborock-s5', + 'Xiaomi Scooter' => 'xiaomi-scooter', + 'Xiaomi Smartphones' => 'xiaomi-smartphone', + 'Xiaomi Tablets' => 'xiaomi-tablet', + 'Yakuza' => 'yakuza', + 'Yale' => 'yale', + 'Yale Smart Lock' => 'yale-smart-lock', + 'Yamaha' => 'yamaha', + 'Yankee Candle' => 'yankee-candle', + 'Yeelight' => 'xiaomi-yeelight', + 'Yoga' => 'yoga', + 'Yoghurt' => 'yoghurt', + 'Yoshi' => 'yoshi', + 'Yoshi's Crafted World' => 'yoshis-crafted-world', + 'YouView' => 'youview', + 'Yves Saint Laurent' => 'yves-saint-laurent', + 'Zanussi' => 'zanussi', + 'Zippo' => 'zippo', + 'Zizzi' => 'zizzi', + 'Zoo' => 'zoo', + 'Zoostorm' => 'zoostorm', + 'ZOTAC' => 'zotac', + 'ZTE' => 'zte', + 'ZTE Smartphone' => 'zte-smartphone', + 'ZyXEL' => 'zyxel', + ] + ], + 'order' => [ + 'name' => 'Order by', + 'type' => 'list', + 'title' => 'Sort order of deals', + 'values' => [ + 'From the most to the least hot deal' => '-hot', + 'From the most recent deal to the oldest' => '-new', + ] + ] + ], + 'Discussion Monitoring' => [ + 'url' => [ + 'name' => 'Discussion URL', + 'type' => 'text', + 'required' => true, + 'title' => 'Discussion URL to monitor. Ex: https://www.hotukdeals.com/discussions/title-123', + 'exampleValue' => 'https://www.hotukdeals.com/discussions/the-hukd-lego-thread-3599357', + ], + 'only_with_url' => [ + 'name' => 'Exclude comments without URL', + 'type' => 'checkbox', + 'title' => 'Exclude comments that does not contains URL in the feed', + 'defaultValue' => false, + ] + ] - 'Deals per group' => array( - 'group' => array( - 'name' => 'Group', - 'type' => 'list', - 'title' => 'Group whose deals must be displayed', - 'values' => array( - '3D Blu-ray' => '3d-bluray', - '3D Printer' => '3d-printer', - '3D TV' => '3d-tv', - '4K Blu-ray' => '4k-bluray', - '4K Monitor' => '4k-monitor', - '4K TV' => '4k-tv', - '5G Phones' => '5g-phones', - '7 Up' => '7up', - '8K TV' => '8k-tv', - '32 inch TV' => '32-inch-tv', - '40 inch TV' => '40-inch-tv', - '55 inch TV' => '55-inch-tv', - '65 inch TV' => '65-inch-tv', - '75 inch TV' => '75-inch-tv', - '144Hz Monitor' => '144hz', - 'A4 Paper' => 'a4-paper', - 'AAA Battery' => 'aaa', - 'AA Battery' => 'aa', - 'Abercrombie' => 'abercrombie', - 'Aberlour' => 'aberlour', - 'Accommodation' => 'accomodation', - 'Accurist' => 'accurist', - 'Ace Combat 7: Skies Unknown' => 'ace-combat-7', - 'Acer' => 'acer', - 'Acer Aspire' => 'acer-aspire', - 'Acer Laptop' => 'acer-laptop', - 'Acer PC Monitor' => 'acer-pc-monitor', - 'Acer Predator' => 'acer-predator', - 'Action Camera' => 'action-camera', - 'Action Figure & Playsets' => 'playsets', - 'Activewear' => 'sports-clothes', - 'Activia' => 'activia', - 'adidas' => 'adidas', - 'adidas Continental' => 'continental', - 'Adidas Gazelle' => 'gazelle', - 'Adidas Originals' => 'adidas-originals', - 'Adidas Samba' => 'samba', - 'Adidas Stan Smith' => 'stan-smith', - 'Adidas Superstar' => 'adidas-superstar', - 'Adidas Trainers' => 'adidas-shoes', - 'Adidas Ultraboost' => 'adidas-ultraboost', - 'Adidas ZX Flux' => 'adidas-zx-flux', - 'Adobe' => 'adobe', - 'Adobe Lightroom' => 'lightroom', - 'Adobe Photoshop' => 'photoshop', - 'Adult Products' => 'adult', - 'Advent Calendar' => 'advent-calendar', - 'Adventure Time' => 'adventure-time', - 'AEG' => 'aeg', - 'Aftershave' => 'aftershave', - 'Age Of Empires' => 'age-of-empires', - 'Air Bed' => 'air-bed', - 'Air Conditioner' => 'air-con', - 'Airer' => 'airer', - 'Airfix' => 'airfix', - 'Air Fryer' => 'air-fryer', - 'Airline' => 'airline', - 'Airport' => 'airport', - 'Airport Parking' => 'airport-parking', - 'Air Purifier' => 'air-purifier', - 'AirTag' => 'airtag', - 'Air Treatment' => 'air-treatment', - 'AKG' => 'akg', - 'Alarm Clock' => 'alarm-clock', - 'Alarm System' => 'alarm-system', - 'Alcatel' => 'alcatel', - 'Alcohol' => 'alcohol', - 'Alesis' => 'alesis', - 'Alien: Isolation' => 'alien-isolation', - 'Alienware' => 'alienware', - 'All-in-One PC' => 'all-in-one-pc', - 'All-in-One Printer' => 'all-in-one-printer', - 'Alloy Wheel' => 'alloy-wheels', - 'All Saints' => 'all-saints', - 'Almonds' => 'almonds', - 'Alpro' => 'alpro', - 'Alton Towers' => 'alton-towers', - 'Amazfit' => 'xiaomi-amazfit', - 'Amazfit Bip' => 'xiaomi-amazfit-bip', - 'Amazfit GTS' => 'amazfit-gts', - 'Amazfit Verge' => 'amazfit-verge', - 'Amazfit Verge Lite' => 'amazfit-verge-lite', - 'Amazfit Watch' => 'amazfit-watch', - 'Amazon Add On Item' => 'add-on-item', - 'Amazon Business' => 'amazon-business', - 'Amazon Echo' => 'amazon-echo', - 'Amazon Echo Dot' => 'amazon-echo-dot', - 'Amazon Echo Plus' => 'amazon-echo-plus', - 'Amazon Echo Show' => 'amazon-echo-show', - 'Amazon Echo Show 5' => 'echo-show-5', - 'Amazon Echo Show 8' => 'amazon-echo-show-8', - 'Amazon Echo Spot' => 'amazon-echo-spot', - 'Amazon Fire 7' => 'amazon-fire-7', - 'Amazon Fire HD 8' => 'amazon-fire-hd-7', - 'Amazon Fire HD 10 Tablet' => 'amazon-fire-hd-10', - 'Amazon Fire Tablet' => 'amazon-tablet', - 'Amazon Fire TV Cube' => 'fire-tv-cube', - 'Amazon Fire TV Stick' => 'amazon-fire-stick', - 'Amazon Pantry' => 'amazon-pantry', - 'Amazon Prime' => 'amazon-prime', - 'Amazon Prime Video' => 'amazon-video', - 'Amazon Warehouse' => 'amazon-warehouse', - 'AMD' => 'amd', - 'AMD Radeon' => 'radeon', - 'AMD Ryzen' => 'amd-ryzen', - 'AMD Ryzen 5 5600X' => 'amd-ryzen-5-5600x', - 'AMD Ryzen 7 5800X' => 'amd-ryzen-7-5800x', - 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x', - 'AMD Ryzen 9 5950X' => 'amd-ryzen-9-5950x', - 'Amex' => 'amex', - 'Amiibo' => 'amiibo', - 'Amplifier' => 'amplifier', - 'Anchor Butter' => 'anchor-butter', - 'Andrex' => 'andrex', - 'Android Apps' => 'android-app', - 'Android Smartphone' => 'android-smartphone', - 'Android Tablet' => 'android-tablet', - 'Angelcare' => 'angelcare', - 'Angle Grinder' => 'grinder', - 'Anglepoise' => 'anglepoise', - 'Angry Birds' => 'angry-birds', - 'Animal Crossing' => 'animal-crossing', - 'Anime' => 'anime', - 'Anker' => 'anker', - 'Ankle Boots' => 'ankle-boots', - 'Anno 1800' => 'anno-1800', - 'Anthem' => 'anthem', - 'Antibacterial Hand Gel' => 'hand-gel', - 'Antibacterial Wipes' => 'cleaning-wipes', - 'Antivirus' => 'antivirus', - 'Antler' => 'antler', - 'AOC' => 'aoc', - 'Apex Legends' => 'apex-legends', - 'A Plague Tale: Innocence' => 'a-plague-tale-innocence', - 'App' => 'app', - 'Apple' => 'apple', - 'Apple AirPods' => 'apple-airpods', - 'Apple Airpods 2' => 'airpods-2', - 'Apple Airpods Max' => 'airpods-max', - 'Apple Airpods Pro' => 'airpods-pro', - 'Apple EarPods' => 'earpods', - 'Apple Headphones' => 'apple-headphones', - 'Apple HomePod' => 'apple-homepod', - 'Apple HomePod mini' => 'apple-homepod-mini', - 'Apple Keyboard' => 'apple-keyboard', - 'Apple Pencil' => 'apple-pencil', - 'Apple TV' => 'apple-tv', - 'Apple TV 4K' => 'apple-tv-4k', - 'Apple Watch' => 'apple-watch', - 'Apple Watch 3' => 'apple-watch-3', - 'Apple Watch 4' => 'apple-watch-4', - 'Apple Watch 5' => 'apple-watch-5', - 'Apple Watch 6' => 'apple-watch-6', - 'Apple Watch SE' => 'apple-watch-se', - 'Apron' => 'apron', - 'Aquadoodle' => 'aquadoodle', - 'Aqua Optima' => 'aqua-optima', - 'Aquarium' => 'aquarium', - 'Aramis' => 'aramis', - 'Argan Oil' => 'argan-oil', - 'Ariel' => 'ariel', - 'Ark' => 'ark', - 'Armani' => 'armani', - 'Armchair' => 'armchair', - 'Armed Forces Discount' => 'armed-forces', - 'Arsenal F. C.' => 'arsenal', - 'Arts and Crafts' => 'craft', - 'Asics' => 'asics', - 'Ask' => 'ask', - 'ASRock' => 'asrock', - 'Assassin's Creed' => 'assassins-creed', - 'Assassin's Creed: Odyssey' => 'assassins-creed-odyssey', - 'Assassin's Creed: Origins' => 'assassins-creed-origins', - 'Assassin's Creed: Unity' => 'assassins-creed-unity', - 'Assassin's Creed: Valhalla' => 'assasins-creed-valhalla', - 'Astral Chain' => 'astral-chain', - 'ASTRO Gaming' => 'astro-gaming', - 'Astro Gaming A40' => 'astro-gaming-a40', - 'Astro Gaming A50' => 'astro-gaming-a50', - 'Asus' => 'asus', - 'ASUS Laptop' => 'asus-laptop', - 'ASUS Monitor' => 'asus-monitor', - 'ASUS ROG' => 'asus-rog', - 'Asus ROG Phone' => 'asus-rog-phone', - 'Asus ROG Phone 2' => 'asus-rog-phone-2', - 'ASUS Router' => 'asus-router', - 'Asus Smartphone' => 'asus-smartphone', - 'ASUS Vivobook' => 'asus-vivobook', - 'ASUS Zenbook' => 'zenbook', - 'Asus ZenFone 6' => 'asus-zenfone-6', - 'Atari' => 'atari', - 'Audi' => 'audi', - 'Audio & Hi-Fi' => 'audio', - 'Audio Accessories' => 'audio-accessories', - 'Audiobook' => 'audiobook', - 'Audio Technica' => 'audio-technica', - 'Aukey' => 'aukey', - 'Aussie' => 'aussie', - 'Autoglym' => 'autoglym', - 'Aveeno' => 'aveeno', - 'Avengers' => 'avengers', - 'AVG' => 'avg', - 'Aviva' => 'aviva', - 'Avon' => 'avon', - 'AV Receiver' => 'av-receiver', - 'Axe' => 'axe', - 'Baby Annabell' => 'baby-annabell', - 'Baby Bath' => 'baby-bath', - 'Baby Born' => 'baby-born', - 'Baby Bottle' => 'baby-bottles', - 'Baby Bouncer' => 'bouncer', - 'Baby Carrier' => 'baby-carrier', - 'Baby Clothes' => 'baby-clothes', - 'Baby Food' => 'baby-food', - 'Baby Gym' => 'baby-gym', - 'Baby Jogger' => 'baby-jogger', - 'Babyliss' => 'babyliss', - 'Baby Monitor' => 'baby-monitor', - 'Baby Shoes' => 'baby-shoes', - 'Baby Swing' => 'baby-swing', - 'Baby Walker' => 'baby-walker', - 'Baby Wipes' => 'wipes', - 'Bacardi' => 'bacardi', - 'Backpack' => 'backpack', - 'Back to the Future' => 'back-to-the-future', - 'Bacon' => 'bacon', - 'Badminton' => 'badminton', - 'Bag' => 'bag', - 'Bagless Vacuum Cleaner' => 'bagless-vacuum-cleaner', - 'Bahco' => 'bahco', - 'Baileys' => 'baileys', - 'Baked Beans' => 'baked-beans', - 'Bakery Products' => 'bakery-products', - 'Baking' => 'baking', - 'Ball Pit' => 'ball-pit', - 'Ballpoint Pen' => 'pen', - 'Band of Brothers' => 'band-of-brothers', - 'Bang & Olufsen' => 'bang-olufsen', - 'Bank' => 'bank', - 'Bank Account' => 'bank-account', - 'Banks & Credit Cards' => 'bank-credit-card', - 'Barbell' => 'barbell', - 'Barbie' => 'barbie', - 'Barbour' => 'barbour', - 'Barclaycard' => 'barclaycard', - 'Barclays' => 'barclays', - 'Barebones PC' => 'barebones', - 'bareMinerals' => 'bareminerals', - 'Barry M' => 'barry-m', - 'Bar Stools' => 'bar-stools', - 'Base Layer' => 'base-layer', - 'Basket' => 'basket', - 'Basketball' => 'basketball', - 'Basmati Rice' => 'basmati-rice', - 'Bath Mat' => 'bath-mat', - 'Bathroom Accessories' => 'bathroom', - 'Bathroom Cabinet' => 'bathroom-cabinet', - 'Bathroom Scale' => 'bathroom-scales', - 'Bathroom Tap' => 'tap', - 'Batman' => 'batman', - 'Battery' => 'battery', - 'Battleborn' => 'battleborn', - 'Battlefield' => 'battlefield', - 'Battlefield 1' => 'battlefield-1', - 'Battlefield 4' => 'battlefield-4', - 'Battlefield 5' => 'battlefield-5', - 'Battlestar Galactica' => 'battlestar-galactica', - 'Baylis & Harding' => 'baylis-and-harding', - 'Bayonetta' => 'bayonetta', - 'Bayonetta 2' => 'bayonetta-2', - 'Baywatch' => 'baywatch', - 'BB-8' => 'bb-8', - 'BBC' => 'bbc', - 'BBQ Food' => 'bbq', - 'BBQs and Grills' => 'grill', - 'Bean Bag' => 'bean-bag', - 'Beanie Hat' => 'beanie-hat', - 'Bean to Cup Machine' => 'bean-to-cup', - 'Beard Trimmer' => 'beard-trimmer', - 'Beats by Dre' => 'beats-by-dre', - 'Beats Solo 3' => 'beats-solo-3', - 'Beats Studio 3' => 'beats-studio-3', - 'Beauty' => 'beauty-care', - 'Beauty and the Beast' => 'beauty-and-the-beast', - 'Becks' => 'becks', - 'Bed' => 'bed', - 'Bedding' => 'bedding', - 'Bedding & Linens' => 'bedding-linens', - 'Bed Frame' => 'bed-frame', - 'Bedroom' => 'bedroom-furniture', - 'Beef' => 'beef', - 'Beer' => 'beer', - 'Beer Advent Calendar' => 'beer-advent-calendar', - 'Beko' => 'beko', - 'Belkin' => 'belkin', - 'Belstaff' => 'belstaff', - 'Belt' => 'belt', - 'BelVita' => 'belvita', - 'Ben & Jerry's' => 'ben-jerrys', - 'Benefit Cosmetics' => 'benefit-cosmetics', - 'BenQ' => 'benq', - 'BenQ Monitor' => 'benq-monitor', - 'Ben Sherman' => 'ben-sherman', - 'BeoPlay Headphones' => 'beoplay-headphones', - 'Beoplay Speakers' => 'beoplay', - 'Berghaus' => 'berghaus', - 'Bestway' => 'bestway', - 'Betting' => 'betting', - 'Beyerdynamic' => 'beyerdynamic', - 'Bic' => 'bic', - 'Bike' => 'bike', - 'Bike Accessories' => 'bike-accessories', - 'Bike Brake' => 'brakes', - 'Bike Computer' => 'bike-computer', - 'Bike Helmet' => 'bicycle-helmet', - 'Bike Inner Tube' => 'inner-tube', - 'Bike Lights' => 'bike-lights', - 'Bike Lock' => 'bike-lock', - 'Bike Parts' => 'bike-parts', - 'Bike Pump' => 'bike-pump', - 'Biker Equipment' => 'biker-equipment', - 'Bike Saddle' => 'saddle', - 'Biking & Urban Sports' => 'biking-urban-sports', - 'Bikini' => 'bikini', - 'Billabong' => 'billabong', - 'Bin' => 'bin', - 'Binatone' => 'binatone', - 'Bingo' => 'bingo', - 'Binoculars' => 'binoculars', - 'Bio Oil' => 'bio-oil', - 'Bioshock' => 'bioshock', - 'Birds Eye' => 'birds-eye', - 'Birkenstock' => 'birkenstock', - 'Biscuits' => 'biscuits', - 'Bissell' => 'bissell', - 'Bistro Set' => 'bistro-set', - 'Bitdefender' => 'bitdefender', - 'Black & Decker' => 'black-decker', - 'Blackberry Smartphone' => 'blackberry', - 'Blanket' => 'blanket', - 'Blaupunkt' => 'blaupunkt', - 'Blazer' => 'blazer', - 'Bleach' => 'bleach', - 'Blended Malt' => 'malt', - 'Blender' => 'blender', - 'Blinds' => 'blinds', - 'Blink XT2 Smart Security Camera' => 'blink-xt2', - 'Blizzard' => 'blizzard', - 'Blood & Truth' => 'blood-and-truth', - 'Bloodborne' => 'bloodborne', - 'Blood Pressure Monitor' => 'blood-pressure', - 'Blu-ray' => 'blu-ray', - 'Blu-ray Player' => 'blu-ray-player', - 'Bluetooth Headphones' => 'bluetooth-headphones', - 'Bluetooth Speaker' => 'bluetooth-speaker', - 'BMW' => 'bmw', - 'BMW Mini Cooper' => 'mini-cooper', - 'BMX' => 'bmx', - 'Board Game' => 'board-game', - 'Boardman' => 'boardman', - 'Boat Shoes' => 'boat-shoes', - 'Bodum' => 'bodum', - 'Bogof' => 'bogof', - 'Boiler' => 'boiler', - 'Bold' => 'bold', - 'Bombay Sapphire' => 'bombay-sapphire', - 'Bomber Jacket' => 'bomber-jacket', - 'Bonne Maman' => 'bonne-maman', - 'Bonsai' => 'bonsai', - 'Book' => 'book', - 'Bookcase' => 'bookcase', - 'Books & Magazines' => 'books-magazines', - 'Booster Seat' => 'booster-seat', - 'Boots' => 'boots', - 'Borderlands' => 'borderlands', - 'Borderlands 3' => 'borderlands-3', - 'Bosch' => 'bosch', - 'Bosch Dishwasher' => 'bosch-dishwasher', - 'Bosch Drill' => 'bosch-drill', - 'Bosch Fridge' => 'bosch-fridge', - 'Bosch Rotak' => 'rotak', - 'Bosch Washing Machine' => 'bosch-washing-machine', - 'Bose' => 'bose', - 'Bose Headphones' => 'bose-headphones', - 'Bose Noise Cancelling Headphones 700' => 'bose-headphones-700', - 'Bose QuietComfort' => 'bose-quietcomfort', - 'Bose QuietComfort 35 II' => 'bose-quietcomfort-35-ii', - 'Bose SoundLink' => 'bose-soundlink', - 'Bose SoundLink Around-Ear II' => 'bose-soundlink-2', - 'Bose SoundTouch' => 'bose-soundtouch', - 'BOSS' => 'hugo-boss', - 'Boss Bottled' => 'boss-bottled', - 'Bouncy Castle' => 'bouncy-castle', - 'Bourbon' => 'bourbon', - 'Bourjois' => 'bourjois', - 'Bowers & Wilkins' => 'bowers-wilkins', - 'Bowling' => 'bowling', - 'Bowmore' => 'bowmore', - 'Boxers' => 'boxers', - 'Boxing' => 'boxing', - 'Boxing Gloves' => 'boxing-gloves', - 'Boy's Clothes' => 'clothes-for-boys', - 'Bra' => 'bra', - 'Brabantia' => 'brabantia', - 'Bracelet' => 'bracelet', - 'Brands' => 'brand', - 'Brandy' => 'brandy', - 'Branston' => 'branston', - 'Branston Beans' => 'branston-beans', - 'Braun' => 'braun', - 'Braun Series 3' => 'braun-series-3', - 'Braun Series 5' => 'braun-series-5', - 'Braun Series 7' => 'braun-series-7', - 'Braun Series 9' => 'braun-series-9', - 'Braun Shaver' => 'braun-shaver', - 'Bread' => 'bread', - 'Breadmaker' => 'breadmaker', - 'Breakdown Cover' => 'breakdown', - 'Breaking Bad' => 'breaking-bad', - 'Breast Pump' => 'breast-pump', - 'Breville' => 'breville', - 'Breville Blend Active' => 'blendactive', - 'Brewdog' => 'brewdog', - 'Bridge Camera' => 'bridge-camera', - 'Briefcase' => 'briefcase', - 'Brita' => 'brita', - 'Britax' => 'britax', - 'British Airways' => 'british-airways', - 'Broadband' => 'broadband', - 'Broadband & Phone Contracts' => 'broadband-phone-service', - 'Brogues' => 'brogues', - 'Brother' => 'brother', - 'Brother Printer' => 'brother-printer', - 'Brownie' => 'brownie', - 'BT' => 'bt', - 'BT Sport' => 'bt-sport', - 'Budweiser' => 'budweiser', - 'Buffalo' => 'buffalo', - 'Bugaboo' => 'bugaboo', - 'Buggy' => 'buggy', - 'Build-A-Bear' => 'build-a-bear', - 'Bulb' => 'bulbs', - 'Bulletstorm' => 'bulletstorm', - 'Bulmers' => 'bulmers', - 'Bulova' => 'bulova', - 'Burberry' => 'burberry', - 'Burger' => 'burger', - 'Burnout Paradise' => 'burnout-paradise', - 'Burt's Bees' => 'burts-bees', - 'Bus and Coach Ticket' => 'bus', - 'Bush' => 'bush', - 'Bushmills' => 'bushmills', - 'Butter' => 'butter', - 'Buying From Abroad' => 'buying-from-abroad', - 'Bvlgari' => 'bvlgari', - 'Cabin Case' => 'cabin-case', - 'Cabinet' => 'cabinet', - 'Cable Reel' => 'cable-reel', - 'Cables' => 'cables', - 'Cadbury's' => 'cadbury', - 'Café Rouge' => 'cafe-rouge', - 'Cafetière' => 'cafetiere', - 'Caffè Nero' => 'cafe-nero', - 'Cake' => 'cake', - 'Calculator' => 'calculator', - 'Calendar' => 'calendar', - 'Call of Duty' => 'call-of-duty', - 'Call of Duty: Black Ops' => 'black-ops', - 'Call of Duty: Black Ops 3' => 'black-ops-3', - 'Call of Duty: Black Ops 4' => 'black-ops-4', - 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war', - 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare', - 'Call of Duty: Modern Warfare' => 'modern-warfare', - 'Call of Duty: WW2' => 'call-of-duty-ww2', - 'Calpol' => 'calpol', - 'Calvin Klein' => 'calvin-klein', - 'Camcorder' => 'camcorder', - 'Camelbak' => 'camelbak', - 'Camera' => 'camera', - 'Camera Accessories' => 'camera-accessories', - 'Camera Bag' => 'camera-bag', - 'Camera Lens' => 'lens', - 'Camping' => 'camping', - 'Campingaz' => 'campingaz', - 'Candle' => 'candle', - 'Cannondale' => 'cannondale', - 'Canon' => 'canon', - 'Canon Camera' => 'canon-camera', - 'Canon EOS' => 'canon-eos', - 'Canon Lens' => 'canon-lens', - 'Canon Pixma' => 'canon-pixma', - 'Canon PowerShot' => 'canon-powershot', - 'Canon PowerShot SX430 IS' => 'canon-powershot-sx430-is', - 'Canon Printer' => 'canon-printer', - 'Canterbury' => 'canterbury', - 'Canton' => 'canton', - 'Canvas Print' => 'canvas-print', - 'Cap' => 'cap', - 'Capsule Machine' => 'capsule-machine', - 'Captain America' => 'captain-america', - 'Captain Morgan' => 'captain-morgan', - 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker', - 'Car' => 'car', - 'Car & Motorcycle' => 'car-motorcycle', - 'Car Accessories' => 'car-accessories', - 'Caravan' => 'caravan', - 'Car Battery' => 'car-battery', - 'Carbon Monoxide Detector' => 'carbon-monoxide', - 'Car Care' => 'car-care', - 'Car Charger' => 'car-charger', - 'Cardhu' => 'cardhu', - 'Cardigan' => 'cardigan', - 'Card Reader' => 'card-reader', - 'Carex' => 'carex', - 'Carhartt' => 'carhartt', - 'Car Hire' => 'car-hire', - 'Car Insurance' => 'car-insurance', - 'Car Leasing' => 'car-lease', - 'Carling' => 'carling', - 'Car Lock' => 'lock', - 'Carlsberg' => 'carlsberg', - 'Car Mats' => 'car-mats', - 'Carolina Herrera' => 'carolina-herrera', - 'Car Parts' => 'car-parts', - 'Carpet' => 'carpet', - 'Carpet Cleaner' => 'carpet-cleaner', - 'CarPlan' => 'carplan', - 'Car Polish' => 'car-polish', - 'Carrera Bikes' => 'carrera', - 'Car Seat' => 'car-seat', - 'Car Service' => 'car-service', - 'Car Stereo' => 'car-stereo', - 'Car Wash' => 'car-wash', - 'Car Wax' => 'car-wax', - 'Casio' => 'casio', - 'Casio Eco-Drive' => 'eco-drive', - 'Casio Edifice' => 'edifice', - 'Casio G-Shock' => 'g-shock', - 'Casserole' => 'casserole', - 'Cast Iron Pots and Pans' => 'cast-iron', - 'Castrol' => 'castrol', - 'Caterpillar' => 'caterpillar', - 'Cat Flap' => 'cat-flap', - 'Cat Food' => 'cat-food', - 'Cath Kidston' => 'cath-kidston', - 'Cat Supplies' => 'cat-supplies', - 'CCTV' => 'cctv', - 'CD' => 'cd', - 'CD Player' => 'cd-player', - 'Ceiling Light' => 'ceiling-light', - 'Celebrations' => 'celebrations', - 'Cereal' => 'cereal', - 'Cetirizine' => 'cetirizine', - 'Chad Valley' => 'chad-valley', - 'Chainsaw' => 'chainsaw', - 'Champagne' => 'champagne', - 'Champneys' => 'champneys', - 'Chanel' => 'chanel', - 'Chanel Coco Mademoiselle' => 'coco-mademoiselle', - 'Changing Bag' => 'changing-bag', - 'Channel 4' => 'channel-4', - 'Charger' => 'charger', - 'Cheese' => 'cheese', - 'Chelsea Boots' => 'chelsea-boots', - 'Chelsea F. C.' => 'chelsea', - 'Chess' => 'chess', - 'Chessington' => 'chessington', - 'Chest Freezer' => 'chest-freezer', - 'Chest of Drawers' => 'chest-of-drawers', - 'Chicco' => 'chicco', - 'Chicken' => 'chicken', - 'Childcare' => 'baby', - 'Children's Books' => 'childrens-books', - 'Chino' => 'chino', - 'Chisel' => 'chisel', - 'Chloe' => 'chloe', - 'Chocolate' => 'chocolate', - 'Chocolate Advent Calendar' => 'chocolate-advent-calendar', - 'Chopper' => 'chopper', - 'Chopping Board' => 'chopping-board', - 'Christmas Card' => 'christmas-card', - 'Christmas Decoration' => 'christmas-decorations', - 'Christmas Gift' => 'christmas-gifts', - 'Christmas Jumper' => 'christmas-jumper', - 'Christmas Lights' => 'christmas-lights', - 'Christmas Stocking Fillers' => 'christmas-stocking-fillers', - 'Christmas Toys' => 'christmas-toys', - 'Christmas Tree' => 'christmas-tree', - 'Chromebook' => 'chromebook', - 'Chromecast' => 'chromecast', - 'Chromecast Ultra' => 'chromecast-ultra', - 'Chromecast with Google TV' => 'chromecast-google-tv', - 'Chronograph' => 'chronograph', - 'Chupa Chups' => 'chupa-chups', - 'Chuwi' => 'chuwi', - 'Cider' => 'cider', - 'Cinema' => 'cinema', - 'Cineworld' => 'cineworld', - 'Circular Saw' => 'circular-saw', - 'Circulon' => 'circulon', - 'Ciroc' => 'ciroc', - 'Cities Skylines' => 'cities-skylines', - 'Citizen' => 'citizen', - 'Citroen' => 'citroen', - 'City Break' => 'city-breaks', - 'Civilization' => 'civilization', - 'Clarins' => 'clarins', - 'Clarks' => 'clarks', - 'Clearance' => 'clearance', - 'Climbing' => 'climbing', - 'Climbing Frame' => 'climbing-frame', - 'Clinique' => 'clinique', - 'Clothes' => 'clothes', - 'Cloud Service' => 'cloud', - 'Clutch Bag' => 'clutch', - 'Coat' => 'coat', - 'Coca Cola' => 'coke', - 'Cocktail' => 'cocktail', - 'Coconut Oil' => 'coconut', - 'Coffee' => 'coffee', - 'Coffee Beans' => 'coffee-beans', - 'Coffee Machine' => 'coffee-machine', - 'Coffee Pods' => 'coffee-pods', - 'Coffee Table' => 'coffee-table', - 'Cognac' => 'cognac', - 'Cola' => 'cola', - 'Coleman' => 'coleman', - 'Colgate' => 'colgate', - 'Combi Drill' => 'combi', - 'Comfort' => 'comfort', - 'Comic' => 'comic', - 'Command & Conquer' => 'command-and-conquer', - 'Compact Camera' => 'compact-camera', - 'Compact Flash' => 'compact-flash', - 'Competitions' => 'competitions', - 'Compost' => 'compost', - 'Compressor' => 'compressor', - 'Computer Accessories' => 'computer-accessories', - 'Computers & Tablets' => 'computers', - 'Concert' => 'concert', - 'Condé Nast' => 'conde-nast', - 'Conditioner' => 'conditioner', - 'Condom' => 'condom', - 'Connectors' => 'connectors', - 'Contact Lenses' => 'contact-lenses', - 'Contents Insurance' => 'contents-insurance', - 'Controller' => 'controller', - 'Converse' => 'converse', - 'Converse Chuck Taylor' => 'chuck-taylor', - 'Cooker' => 'cooker', - 'Cooking Oil' => 'cooking-oil', - 'Cookware' => 'cooking', - 'Cookware Set' => 'cookware-set', - 'Cookworks' => 'cookworks', - 'Cool Box' => 'cool-box', - 'Coors Light' => 'coors-light', - 'Cordless Drill' => 'cordless-drill', - 'Cordless Phone' => 'cordless-phone', - 'Cornetto' => 'cornetto', - 'Corona Beer' => 'corona', - 'Corsair' => 'corsair', - 'Cosatto' => 'cosatto', - 'Costa Coffee' => 'costa-coffee', - 'Costume' => 'costume', - 'Cot' => 'cot', - 'Counter Strike' => 'counter-strike', - 'Courses and Training' => 'education', - 'Cow & Gate' => 'cow-and-gate', - 'Cozy Coupe' => 'cozy-coupe', - 'CPU' => 'cpu', - 'CPU Cooler' => 'cpu-cooler', - 'Craghoppers' => 'craghoppers', - 'Crash Bandicoot' => 'crash-bandicoot', - 'Crash Team Racing Nitro-Fueled' => 'crash-team-racing-nitro-fueled', - 'Crayola' => 'crayola', - 'Creatine' => 'creatine', - 'Credit Card' => 'credit-card', - 'Creme Egg' => 'creme-egg', - 'Cricket' => 'cricket', - 'Crisps' => 'crisps', - 'Crocs' => 'crocs', - 'Cross Trainer' => 'cross-trainer', - 'Crown Paint' => 'crown', - 'Crucial' => 'crucial', - 'Cruelty Free Makeup' => 'cruelty-free-makeup', - 'Cruises' => 'cruise', - 'Cube Bikes' => 'cube', - 'Cubot' => 'cubot', - 'Cufflinks' => 'cufflinks', - 'Culture & Leisure' => 'entertainment', - 'Cuphead' => 'cuphead', - 'Cuprinol' => 'cuprinol', - 'Curling Wand' => 'curling-wand', - 'Curtain' => 'curtain', - 'Cushelle' => 'cushelle', - 'Cushion' => 'cushion', - 'Cutlery' => 'cutlery', - 'CyberLink' => 'cyberlink', - 'Cyberpunk 2077' => 'cyberpunk-2077', - 'Cybex' => 'cybex', - 'Cycling' => 'cycling', - 'Cycling Jacket' => 'cycling-jacket', - 'D-Link' => 'd-link', - 'DAB Radio' => 'dab-radio', - 'Dacia' => 'dacia', - 'Daily Mail' => 'daily-mail', - 'Dairy Milk' => 'dairy-milk', - 'Darksiders' => 'darksiders', - 'Dark Souls' => 'dark-souls', - 'Dark Souls 3' => 'dark-souls-3', - 'Dartboard' => 'dartboard', - 'Darts' => 'darts', - 'Dash Cam' => 'dash-cam', - 'Data Storage' => 'storage', - 'Davidoff' => 'davidoff', - 'Days Gone' => 'days-gone', - 'Days Out' => 'days-out', - 'Daz' => 'daz', - 'DC Comic' => 'dc', - 'DDR3' => 'ddr3', - 'DDR4' => 'ddr4', - 'Dead Island' => 'dead-island', - 'Dead or Alive 6' => 'dead-or-alive-6', - 'Deadpool' => 'deadpool', - 'Dead Rising' => 'dead-rising', - 'Death Stranding' => 'death-stranding', - 'Deezer' => 'deezer', - 'Dehumidifier' => 'dehumidifier', - 'Dell' => 'dell', - 'Dell Laptop' => 'dell-laptop', - 'Dell Monitor' => 'dell-monitor', - 'Dell XPS' => 'xps', - 'Delonghi' => 'delonghi', - 'Demon's Souls' => 'demon-souls', - 'Denby' => 'denby', - 'Denon' => 'denon', - 'Deodorant' => 'deodorant', - 'Desk' => 'desk', - 'Desperados Beer' => 'desperados', - 'Despicable Me' => 'despicable-me', - 'Destiny' => 'destiny', - 'Destiny 2' => 'destiny-2', - 'Detergent' => 'detergent', - 'Detroit: Become Human' => 'detroit-become-human', - 'Dettol' => 'dettol', - 'Deus Ex' => 'deus-ex', - 'Deus Ex: Mankind Divided' => 'deus-ex-mankind-divided', - 'Development Boards' => 'development-boards', - 'Devil May Cry 5' => 'devil-may-cry-5', - 'DeWalt' => 'dewalt', - 'DFDS' => 'dfds', - 'Diablo 3' => 'diablo-3', - 'Diary' => 'diary', - 'Dickies' => 'dickies', - 'Diesel' => 'diesel', - 'Diet' => 'diet', - 'Diggerland' => 'diggerland', - 'Digihome' => 'digihome', - 'Digimon' => 'digimon', - 'Digital Camera' => 'digital-camera', - 'Digital Watch' => 'digital-watch', - 'Dildo' => 'dildo', - 'Dimplex' => 'dimplex', - 'Dining Room' => 'dining-room', - 'Dining Room Chair' => 'chair', - 'Dining Set' => 'dining-set', - 'Dining Table' => 'dining-table', - 'Dinner Plate' => 'plates', - 'Dinner Set' => 'dinner-set', - 'Dinosaur' => 'dinosaur', - 'Dior' => 'dior', - 'Dior Sauvage' => 'dior-sauvage', - 'Dirt' => 'dirt', - 'Dirt 4' => 'dirt-4', - 'DIRT 5' => 'dirt-5', - 'Dirt Rally 2.0' => 'dirt-rally-2', - 'Disaronno' => 'disaronno', - 'Discord Nitro' => 'discord-nitro', - 'Disgaea' => 'disgaea', - 'Dishonored' => 'dishonored', - 'Dishonored 2' => 'dishonored-2', - 'Dishwasher' => 'dishwasher', - 'Dishwasher Tablets' => 'dishwasher-tablets', - 'Disinfectants' => 'disinfectants', - 'Disney' => 'disney', - 'Disney's Cars' => 'disney-cars', - 'Disney's Frozen' => 'disney-frozen', - 'Disney+' => 'disney-plus', - 'Disney Infinity' => 'disney-infinity', - 'Disneyland' => 'disneyland', - 'Disney Princess' => 'disney-princess', - 'Disney Tsum Tsum' => 'tsum-tsum', - 'Disney World' => 'disney-world', - 'Divan' => 'divan', - 'DIY' => 'diy', - 'DJ Equipment' => 'dj', - 'DJI Phantom' => 'dji-phantom', - 'DKNY' => 'dkny', - 'Doctor Who' => 'doctor-who', - 'Dog Bed' => 'dog-bed', - 'Dog Food' => 'dog-food', - 'Dog Supplies' => 'dog', - 'Dolce & Gabbana' => 'dolce', - 'Dolce Gusto' => 'dolce-gusto', - 'Dolce Gusto Coffee Machine' => 'dolce-gusto-coffee-machine', - 'Doll' => 'doll', - 'Dolls House' => 'dolls-house', - 'Domain Service' => 'domain', - 'Doogee' => 'doogee', - 'Doom' => 'doom', - 'Door' => 'door', - 'Doorbell' => 'doorbell', - 'Door Handles' => 'door-handles', - 'Doormat' => 'doormat', - 'Doritos' => 'doritos', - 'Dove' => 'dove', - 'Down Jacket' => 'down-jacket', - 'Downton Abbey' => 'downton-abbey', - 'Dr. Martens' => 'dr-martens', - 'Dragon Age' => 'dragon-age', - 'Dragon Ball' => 'dragon-ball', - 'Dragon Ball: FighterZ' => 'dragon-ball-fighterz', - 'Dragon Quest' => 'dragon-quest', - 'Dragon Quest Builders' => 'dragon-quest-builders', - 'Dragon Quest Builders 2' => 'dragon-quest-builders-2', - 'Dragon Quest XI: Echoes of an Elusive Age' => 'dragon-quest-xi', - 'Draper' => 'draper', - 'Drayton Manor' => 'drayton-manor', - 'Dreame T20' => 'dreame-t20', - 'Dreame V9' => 'dreame-v9', - 'Dreame V9P' => 'dreame-v9p', - 'Dreame V10' => 'dreame-v10', - 'Dreame V11' => 'dreame-v11', - 'Dreame Vacuum Cleaner' => 'xiaomi-vacuum-cleaner', - 'Dremel' => 'dremel', - 'Dress' => 'dress', - 'Dressing Gown' => 'dressing-gown', - 'Drill' => 'drill', - 'Drill Driver' => 'driver', - 'Drinks' => 'drinks', - 'Driveclub' => 'driveclub', - 'Driving Lessons' => 'driving-lessons', - 'Drone' => 'drone', - 'Dryer' => 'dryer', - 'DSLR Camera' => 'dslr', - 'Dual Fuel Cooker' => 'dual-fuel', - 'Dualit' => 'dualit', - 'Dual Sim' => 'sim', - 'Dulux' => 'dulux', - 'Duracell' => 'duracell', - 'Durex' => 'durex', - 'Duvet' => 'duvet', - 'DVD' => 'dvd', - 'DVD Player' => 'dvd-player', - 'Dying Light' => 'dying-light', - 'Dymo' => 'dymo', - 'Dyson' => 'dyson', - 'Dyson Supersonic' => 'dyson-supersonic', - 'Dyson V6' => 'dyson-v6', - 'Dyson V7' => 'dyson-v7', - 'Dyson V8' => 'dyson-v8', - 'Dyson V10' => 'dyson-v10', - 'Dyson V11' => 'dyson-v11', - 'Dyson Vacuum Cleaner' => 'dyson-vacuum-cleaner', - 'e-Reader' => 'ereader', - 'EA' => 'ea', - 'EA Access' => 'ea-access', - 'Earphones' => 'earphones', - 'Earrings' => 'earrings', - 'EA Sports' => 'ea-sports', - 'EA Sports UFC' => 'ufc', - 'Easter Eggs' => 'egg', - 'Eastpak' => 'eastpak', - 'eBook' => 'ebook', - 'Ecovacs' => 'ecovacs', - 'Ecover' => 'ecover', - 'Educational Toys' => 'educational-toys', - 'EE' => 'ee', - 'eFootball PES 2021' => 'pes-2021', - 'ELC Happyland' => 'happyland', - 'Electrical Accessories' => 'electrical-accessories', - 'Electric Bike' => 'electric-bike', - 'Electric Blanket' => 'electric-blanket', - 'Electric Cooker' => 'electric-cooker', - 'Electric Fires' => 'electric-fire', - 'Electric Scooter' => 'electric-scooter', - 'Electric Shower' => 'electric-shower', - 'Electric Toothbrush' => 'electric-toothbrush', - 'Electronic Accessories' => 'electronics-accessories', - 'Electronics' => 'electronics', - 'Elemis' => 'elemis', - 'Elephone' => 'elephone', - 'Elgato' => 'elgato', - 'Elite Dangerous' => 'elite-dangerous', - 'Elizabeth Arden' => 'elizabeth-arden', - 'Emirates' => 'emirates', - 'Endura' => 'endura', - 'Eneloop' => 'eneloop', - 'Energizer' => 'energizer', - 'Energy' => 'energy', - 'Energy, Heating & Gas' => 'energy-heating-gas', - 'Energy Drinks' => 'energy-drinks', - 'Engine Oil' => 'engine-oil', - 'Epilator' => 'epilator', - 'Epson' => 'epson', - 'Epson Printer' => 'epson-printer', - 'Espresso' => 'espresso', - 'Espresso Machine' => 'espresso-machine', - 'Esprit' => 'esprit', - 'Estée Lauder' => 'estee-lauder', - 'Ethernet' => 'ethernet', - 'Etnies' => 'etnies', - 'Eurostar Ticket' => 'eurostar', - 'Eurotunnel' => 'eurotunnel', - 'Everton F. C.' => 'everton', - 'EVGA' => 'evga', - 'Evian' => 'evian', - 'Exercise Equipment' => 'exercise-equipment', - 'Exercise Weights' => 'weight', - 'Extension Lead' => 'extension-lead', - 'External Hard Drive' => 'external-hard-drive', - 'F1' => 'formula-one', - 'F1 2017' => 'f1-2017', - 'F1 2018' => 'f1-2018', - 'F1 2019' => 'f1-2019', - 'F1 2020' => 'f1-2020', - 'Fabric Conditioner' => 'fabric-conditioner', - 'Face Cream' => 'face-cream', - 'Face Mask' => 'face-mask', - 'Fairy' => 'fairy', - 'Fairy Light' => 'fairy-light', - 'Fallout' => 'fallout', - 'Fallout 4' => 'fallout-4', - 'Fallout 76' => 'fallout-76', - 'Family & Kids' => 'kids', - 'Family Break' => 'family-break', - 'Family Guy' => 'family-guy', - 'Famous Grouse' => 'famous-grouse', - 'Fancy Dress' => 'fancy-dress', - 'Fans' => 'fan', - 'Fanta' => 'fanta', - 'Far Cry' => 'far-cry', - 'Far Cry 4' => 'far-cry-4', - 'Far Cry 5' => 'far-cry-5', - 'Far Cry New Dawn' => 'far-cry-new-dawn', - 'Far Cry Primal' => 'far-cry-primal', - 'Farming Simulator' => 'farming-simulator', - 'Fashion & Accessories' => 'fashion', - 'Fashion Accessories' => 'fashion-accessories', - 'Fashion for Men' => 'mens-clothing', - 'Fashion for Women' => 'womens-clothes', - 'Fast and Furious' => 'fast-and-furious', - 'Father's Day' => 'fathers-day', - 'FatMax' => 'fatmax', - 'FC Barcelona' => 'fc-barcelona', - 'Felix' => 'felix', - 'Fence' => 'fence', - 'Fender Guitar' => 'fender', - 'Ferrero Rocher' => 'ferrero-rocher', - 'Ferry' => 'ferry', - 'Festival' => 'festival', - 'Fever Thermometer' => 'thermometer', - 'Fiat' => 'fiat', - 'Fidget Spinner' => 'spinner', - 'FIFA' => 'fifa', - 'FIFA 17' => 'fifa-17', - 'FIFA 18' => 'fifa-18', - 'FIFA 19' => 'fifa-19', - 'FIFA 20' => 'fifa-20', - 'FIFA 21' => 'fifa-21', - 'FightStick' => 'fightstick', - 'Figures' => 'figures', - 'Fila Trainers' => 'fila-trainers', - 'Filing Cabinet' => 'filing-cabinet', - 'Final Fantasy' => 'final-fantasy', - 'Final Fantasy 15' => 'final-fantasy-15', - 'Finance & Insurance' => 'personal-finance', - 'Finish' => 'finish', - 'Finlux' => 'finlux', - 'Fiorelli' => 'fiorelli', - 'Fire Emblem' => 'fire-emblem', - 'Fire Pit' => 'fire-pit', - 'Fireplace' => 'fireplace', - 'Firewall: Zero Hour' => 'firewall-zero-hour', - 'First Aid' => 'first-aid', - 'Fish & Seafood' => 'fish-and-seafood', - 'Fish and Aquatic Pet Supplies' => 'fish', - 'Fisher Price' => 'fisher-price', - 'Fisher Price Imaginext' => 'imaginext', - 'Fisher Price Jumperoo' => 'jumperoo', - 'Fisher Price Little People' => 'little-people', - 'Fishing' => 'fishing', - 'Fiskars' => 'fiskars', - 'Fitbit' => 'fitbit', - 'Fitbit Alta' => 'fitbit-alta', - 'Fitbit Blaze' => 'fitbit-blaze', - 'Fitbit Charge 2' => 'fitbit-charge-2', - 'Fitbit Inspire' => 'fitbit-inspire', - 'Fitbit Versa' => 'fitbit-versa', - 'Fitness & Running' => 'fitness', - 'Fitness App' => 'fitness-app', - 'Fitness Tracker' => 'fitness-tracker', - 'Flamingo Land' => 'flamingo-land', - 'Flea Treatment' => 'flea', - 'Fleece Clothing' => 'fleece', - 'Flights' => 'flight', - 'Flip Flops' => 'flip-flops', - 'Floodlight' => 'floodlight', - 'Flooring' => 'flooring', - 'Flowers' => 'flowers', - 'Flymo' => 'flymo', - 'FM Transmitter' => 'fm-transmitter', - 'Food' => 'food', - 'Food Containers' => 'food-containers', - 'Food Processor' => 'food-processor', - 'Food Server' => 'food-server', - 'Football' => 'football', - 'Football Boots' => 'football-boots', - 'Football Manager' => 'football-manager', - 'Football Matches' => 'football-matches', - 'Football Shirt' => 'football-shirt', - 'Foot Pump' => 'foot-pump', - 'Ford' => 'ford', - 'For Honor' => 'for-honor', - 'Fortnite' => 'fortnite', - 'Fortnite: Darkfire' => 'fortnite-darkfire', - 'Forza' => 'forza', - 'Forza 7' => 'forza-7', - 'Forza Horizon' => 'forza-horizon', - 'Forza Horizon 3' => 'forza-horizon-3', - 'Forza Horizon 4' => 'forza-horizon-4', - 'Forza Motorsport' => 'forza-motorsport', - 'Foscam' => 'foscam', - 'Fossil' => 'fossil', - 'Foster's' => 'fosters', - 'Foundation' => 'foundation', - 'Fountain Pen' => 'fountain-pen', - 'Fred Perry' => 'fred-perry', - 'Freesat' => 'freesat', - 'Freeview' => 'freeview', - 'Freezer' => 'freezer', - 'Fridge' => 'fridge', - 'Fridge Freezer' => 'fridge-freezer', - 'Frontline' => 'frontline', - 'Frozen Food' => 'frozen', - 'Fruit' => 'fruit', - 'Fruit and Vegetables' => 'fruit-and-vegetable', - 'Fruit of the Loom' => 'fruit-of-the-loom', - 'Fryer' => 'fryer', - 'Frying Pan' => 'frying-pan', - 'Fujifilm' => 'fuji', - 'Fujitsu' => 'fujitsu', - 'Funko Pop' => 'funko-pop', - 'Furby' => 'furby', - 'Furniture' => 'furniture', - 'G-Star' => 'g-star', - 'G-Sync Monitor' => 'g-sync', - 'Gaggia' => 'gaggia', - 'Gambling' => 'gambling', - 'Game App' => 'game-app', - 'Game of Thrones' => 'game-of-thrones', - 'Games & Board Games' => 'board-games', - 'Games Consoles' => 'console', - 'Gaming' => 'gaming', - 'Gaming Accessories' => 'gaming-accessories', - 'Gaming Chair' => 'gaming-chair', - 'Gaming Headset' => 'gaming-headset', - 'Gaming Keyboard' => 'gaming-keyboard', - 'Gaming Laptop' => 'gaming-laptop', - 'Gaming Monitor' => 'gaming-monitor', - 'Gaming Mouse' => 'gaming-mouse', - 'Gaming PC' => 'gaming-pc', - 'Gant' => 'gant', - 'Garage' => 'garage', - 'Garage & Service' => 'garage-service', - 'Garden' => 'garden', - 'Garden & Do It Yourself' => 'garden-diy', - 'Garden Furniture' => 'garden-furniture', - 'Gardening' => 'gardening', - 'Garden Storage' => 'garden-storage', - 'Garden Table' => 'table', - 'Garmin' => 'garmin', - 'Garmin Fenix' => 'garmin-fenix', - 'Garmin Fenix 6' => 'garmin-fenix-6', - 'Garmin Fenix 6 Pro' => 'garmin-fenix-6-pro', - 'Garmin Forerunner' => 'garmin-forerunner', - 'Garmin Vivoactive' => 'garmin-vivoactive', - 'Garmin Watch' => 'garmin-watch', - 'Garnier' => 'garnier', - 'Gas' => 'gas', - 'Gas Canister' => 'butane', - 'Gas Cooker' => 'gas-cooker', - 'Gatwick' => 'gatwick', - 'Gazebo' => 'gazebo', - 'GBK' => 'gbk', - 'Gears 5' => 'gears-5', - 'Gears of War' => 'gears-of-war', - 'Gears of War 4' => 'gears-of-war-4', - 'George Foreman' => 'george-foreman', - 'Geox' => 'geox', - 'GHD' => 'ghd', - 'Ghostbusters' => 'ghostbusters', - 'Ghostbusters: The Video Game Remastered' => 'ghostbusters-the-video-game', - 'Ghost of Tsushima' => 'ghost-of-tsushima', - 'Gibson Guitar' => 'gibson', - 'giffgaff' => 'giffgaff', - 'Gift Card' => 'gift-card', - 'Gift Hamper' => 'hamper', - 'Gifts' => 'gifts', - 'Gift Set' => 'gift-set', - 'GIGABYTE' => 'gigabyte', - 'Gigaset' => 'gigaset', - 'Gilet' => 'gilet', - 'Gillette Fusion' => 'fusion', - 'Gillette Mach3' => 'mach-3', - 'Gillette Razor' => 'gillette', - 'Gimbal' => 'gimbal', - 'Gin' => 'gin', - 'Girl's Clothes' => 'girls-clothes', - 'Glasses' => 'glasses', - 'Glassware' => 'glassware', - 'Glenfiddich' => 'glenfiddich', - 'Glenlivet' => 'glenlivet', - 'Glenmorangie' => 'glenmorangie', - 'Gloves' => 'gloves', - 'Glue' => 'glue', - 'Glue Gun' => 'glue-gun', - 'Gluten-Free' => 'gluten-free', - 'God of War' => 'god-of-war', - 'Go Kart' => 'go-kart', - 'Golf' => 'golf', - 'Golf Balls' => 'golf-balls', - 'Golf Clubs' => 'golf-clubs', - 'Goodfellas' => 'goodfellas', - 'Goodmans' => 'goodmans', - 'Goodyear' => 'goodyear', - 'Google' => 'google', - 'Google Home' => 'google-home', - 'Google Home Max' => 'google-home-max', - 'Google Home Mini' => 'google-home-mini', - 'Google Nest' => 'nest', - 'Google Nest Audio' => 'google-nest-audio', - 'Google Nest Hub' => 'google-home-hub', - 'Google Nest Mini' => 'nest-mini', - 'Google Nest Protect' => 'google-nest-protect', - 'Google Nexus' => 'nexus', - 'Google Pixel' => 'google-pixel', - 'Google Pixel 2' => 'google-pixel-2', - 'Google Pixel 2 XL' => 'google-pixel-2-xl', - 'Google Pixel 3' => 'google-pixel-3', - 'Google Pixel 3 XL' => 'google-pixel-3-xl', - 'Google Pixel 3a' => 'google-pixel-3a', - 'Google Pixel 3a XL' => 'google-pixel-3a-xl', - 'Google Pixel 4' => 'google-pixel-4', - 'Google Pixel 4 XL' => 'google-pixel-4-xl', - 'Google Pixel 4a' => 'google-pixel-4a', - 'Google Pixel 4a 5G' => 'google-pixel-4a-5g', - 'Google Pixel 5' => 'google-pixel-5', - 'Google Pixelbook' => 'google-pixelbook', - 'Google Pixel XL' => 'google-pixel-xl', - 'Google Smartphone' => 'google-smartphone', - 'Google Stadia' => 'google-stadia', - 'GoPro' => 'gopro', - 'GoPro HERO 6' => 'gopro-hero-6', - 'GoPro HERO 7' => 'gopro-hero-7', - 'GoPro HERO 8' => 'gopro-hero-8', - 'GoPro HERO 9' => 'gopro-hero-9', - 'Gore-Tex Clothing and Shoes' => 'gore-tex', - 'Graco' => 'graco', - 'Grand National' => 'grand-national', - 'Gran Turismo' => 'gran-turismo', - 'Gran Turismo Sport' => 'gran-turismo-sport', - 'Graphics Card' => 'graphics-card', - 'Gravity Rush' => 'gravity-rush', - 'Graze' => 'graze', - 'GreedFall' => 'greedfall', - 'Greenhouse' => 'greenhouse', - 'Greeting Cards and Wrapping Paper' => 'wrapping-paper-and-cards', - 'Greggs' => 'greggs', - 'Grey Goose' => 'grey-goose', - 'Griffin Technology' => 'griffin', - 'GroBag' => 'grobag', - 'Groceries' => 'groceries', - 'Gruffalo' => 'gruffalo', - 'Grundig' => 'grundig', - 'GTA' => 'gta', - 'GTA V' => 'gta-v', - 'GTX 970' => 'gtx-970', - 'GTX 980' => 'gtx-980', - 'GTX 1060' => 'gtx-1060', - 'GTX 1070' => 'gtx-1070', - 'GTX 1080' => 'gtx-1080', - 'GTX 1080 Ti' => 'gtx-1080-ti', - 'GTX 1660' => 'gtx-1660', - 'GTX 1660 Ti' => 'gtx-1660-ti', - 'Guardians of the Galaxy' => 'guardians-of-the-galaxy', - 'Gucci' => 'gucci', - 'Guinness' => 'guinness', - 'Guitar' => 'guitar', - 'Guitar Amp' => 'guitar-amp', - 'Guitar Hero' => 'guitar-hero', - 'Gulliver's' => 'gullivers', - 'Gym' => 'gym', - 'Gym Membership' => 'gym-membership', - 'H1Z1' => 'h1z1', - 'Häagen Dazs' => 'haagen-dazs', - 'Habitat' => 'habitat', - 'Hacksaw' => 'hacksaw', - 'Hair Brush' => 'hair-brush', - 'Hair Care' => 'hair', - 'Hair Clipper' => 'hair-clipper', - 'Hair Colour' => 'hair-colour', - 'Haircut' => 'haircut', - 'Hair Dryer' => 'hair-dryer', - 'Hair Dye' => 'hair-dye', - 'Hair Removal Devices' => 'hair-removal-devices', - 'Halifax' => 'halifax', - 'Hall' => 'hall', - 'Halloween' => 'halloween', - 'Halo' => 'halo', - 'Halo 5' => 'halo-5', - 'Ham' => 'ham', - 'Hammer' => 'hammer', - 'Hammer Drill' => 'hammer-drill', - 'Hammock' => 'hammock', - 'Handbag' => 'handbag', - 'Hand Blender' => 'hand-blender', - 'Hand Cream' => 'hand-cream', - 'Hand Mixer' => 'hand-mixer', - 'Hand Tools' => 'hand-tools', - 'Handwash' => 'handwash', - 'Hard Drive' => 'hard-drive', - 'Haribo' => 'haribo', - 'Harman Kardon' => 'harman-kardon', - 'Harry Potter' => 'harry-potter', - 'Hasbro' => 'hasbro', - 'Hat' => 'hat', - 'Hatchimals' => 'hatchimals', - 'Hats & Caps' => 'hats-caps', - 'Hauck' => 'hauck', - 'Hayfever Remedies' => 'hayfever', - 'Headboard' => 'headboard', - 'Headphones' => 'headphones', - 'Headset' => 'headset', - 'Health & Beauty' => 'beauty', - 'Healthcare' => 'health-care', - 'Heart Rate Monitor' => 'heart-rate-monitor', - 'Heater' => 'heater', - 'Heating' => 'heating', - 'Heating Appliances' => 'heating-appliances', - 'Hedge Trimmer' => 'hedge-trimmer', - 'Heineken' => 'heineken', - 'Heinz' => 'heinz', - 'Heinz Beanz' => 'heinz-baked-beans', - 'Hello Kitty' => 'hello-kitty', - 'Hello Neighbour' => 'hello-neighbour', - 'Helly Hansen' => 'helly-hansen', - 'Henry Hoover' => 'henry-hoover', - 'Hermes' => 'hermes', - 'High5' => 'high-5', - 'Highchair' => 'highchair', - 'Hiking' => 'hiking', - 'Hilton' => 'hilton', - 'Hisense' => 'hisense', - 'Hisense TVs' => 'hisense-tv', - 'Hitachi' => 'hitachi', - 'Hitman' => 'hitman', - 'Hive' => 'hive', - 'Hive Active Heating' => 'hive-active-heating', - 'Hob' => 'hob', - 'Hobbit' => 'hobbit', - 'Hockey' => 'hockey', - 'Holiday Inn' => 'holiday-inn', - 'Holiday Park' => 'holiday-parks', - 'Holidays and Trips' => 'holidays-and-trips', - 'Hollow Knight' => 'hollow-knight', - 'Home & Living' => 'home', - 'Home Accessories' => 'home-accessories', - 'Home Appliances' => 'home-appliances', - 'Home Care' => 'home-care', - 'Home Cinema' => 'home-cinema', - 'HoMedics' => 'homedics', - 'Homefront' => 'homefront', - 'Home Networking' => 'network', - 'Homeplug' => 'homeplug', - 'Home Security' => 'home-security', - 'Homeware' => 'homeware', - 'Honda' => 'honda', - 'Honey' => 'honey', - 'Honeywell' => 'honeywell', - 'Honor 6X' => 'honor-6x', - 'Honor 7' => 'honor-7', - 'Honor 8S' => 'honor-8s', - 'Honor 8X' => 'honor-8x', - 'Honor 8X Max' => 'honor-8x-max', - 'Honor 9' => 'honor-9', - 'Honor 9X' => 'honor-9x', - 'Honor 10' => 'honor-10', - 'Honor Band 5' => 'honor-band-5', - 'Honor Play' => 'honor-play', - 'Honor Smartphone' => 'honor', - 'Honor View 20' => 'honor-view-20', - 'Hoodie' => 'hoodie', - 'Hoover' => 'hoover', - 'Hori' => 'hori', - 'Horizon: Zero Dawn' => 'horizon-zero-dawn', - 'Hornby' => 'hornby', - 'Horse Races' => 'horse-races', - 'Hose' => 'hose', - 'HOTAS' => 'hotas', - 'Hotel' => 'hotel', - 'Hotpoint' => 'hotpoint', - 'Hotspot' => 'hotspot', - 'Hot Tub' => 'hot-tub', - 'Hot Water Bottle' => 'hot-water-bottle', - 'Hot Wheels' => 'hot-wheels', - 'Hozelock' => 'hozelock', - 'HP' => 'hp', - 'HP Envy' => 'hp-envy', - 'HP Laptop' => 'hp-laptop', - 'HP Omen' => 'hp-omen', - 'HP Printer' => 'hp-printer', - 'HTC' => 'htc', - 'HTC 10' => 'htc-10', - 'HTC Desire' => 'htc-desire', - 'HTC One' => 'htc-one', - 'HTC Smartphone' => 'htc-smartphone', - 'HTC U11' => 'htc-u11', - 'HTC Vive' => 'htc-vive', - 'Huawei' => 'huawei', - 'Huawei Freebuds 3' => 'huawei-freebuds-3', - 'Huawei Headphones' => 'huawei-headphones', - 'Huawei Mate 20' => 'huawei-mate-20', - 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro', - 'Huawei Mate 30' => 'huawei-mate-30', - 'Huawei Mate 30 Lite' => 'huawei-mate-30-lite', - 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro', - 'Huawei Matebook' => 'huawei-matebook', - 'Huawei MediaPad M3' => 'huawei-mediapad-m3', - 'Huawei MediaPad M5' => 'huawei-mediapad-m5', - 'Huawei MediaPad T3' => 'huawei-mediapad-t3', - 'Huawei MediaPad T5' => 'huawei-mediapad-t5', - 'Huawei P9' => 'huawei-p9', - 'Huawei P10' => 'huawei-p10', - 'Huawei P20' => 'huawei-p20', - 'Huawei P20 Lite' => 'huawei-p20-lite', - 'Huawei P20 Pro' => 'huawei-p20-pro', - 'Huawei P30' => 'huawei-p30', - 'Huawei P30 Lite' => 'huawei-p30-lite', - 'Huawei P30 Pro' => 'huawei-p30-pro', - 'Huawei P40' => 'huawei-p40', - 'Huawei P40 Lite' => 'huawei-p40-lite', - 'Huawei P40 Pro' => 'huawei-p40-pro', - 'Huawei P Smart' => 'huawei-p-smart', - 'Huawei Smartphone' => 'huawei-smartphone', - 'Huawei Smartwatch' => 'huawei-smartwatch', - 'Huawei Tablet' => 'huawei-tablet', - 'Huawei Watch 2' => 'huawei-watch-2', - 'Huawei Watch GT' => 'huawei-watch-gt', - 'Huawei Watch GT2' => 'huawei-watch-gt2', - 'Huawei Watch GT 2 Pro' => 'huawei-watch-gt-2-pro', - 'Huawei Y7' => 'huawei-y7', - 'Huggies' => 'huggies', - 'Hulk' => 'hulk', - 'Humax' => 'humax', - 'Humidifier' => 'humidifier', - 'Hunter' => 'hunter', - 'HyperX' => 'hyperx', - 'Hyrule Warriors' => 'hyrule-warriors', - 'Hyundai' => 'hyundai', - 'IAMS' => 'iams', - 'iCandy' => 'icandy', - 'Ice-Watch' => 'ice-watch', - 'Ice Cream' => 'ice-cream', - 'Ice Cream Maker' => 'ice-cream-maker', - 'iMac' => 'apple-imac', - 'iMac 2021' => 'imac-2021', - 'Impact Driver' => 'impact-driver', - 'Indesit' => 'indesit', - 'Inflatable Boats' => 'boat', - 'Inflatable Toys' => 'inflatable', - 'Injustice' => 'injustice', - 'Injustice 2' => 'injustice-2', - 'Ink Cartridge' => 'ink', - 'Inkjet Printer' => 'inkjet-printer', - 'Innocent' => 'innocent', - 'Instant Cameras' => 'instant-cameras', - 'Instant Ink' => 'instant-ink', - 'Instax Mini 9' => 'instax-mini-9', - 'Insulation' => 'insulation', - 'Insurance' => 'insurance', - 'Intel' => 'intel', - 'Intel Atom' => 'atom', - 'Intel i3' => 'i3', - 'Intel i5' => 'i5', - 'Intel i7' => 'i7', - 'Intel i9' => 'intel-i9', - 'Internet' => 'internet', - 'Internet Security' => 'internet-security', - 'In the Night Garden' => 'in-the-night-garden', - 'Intimate Care' => 'intimate-care', - 'Introduce Yourself' => 'introduce-yourself', - 'iOS Apps' => 'ios-apps', - 'iPad' => 'ipad', - 'iPad 2019' => 'ipad-2019', - 'iPad 2020' => 'ipad-2020', - 'iPad Air' => 'ipad-air', - 'iPad Air 2019' => 'ipad-air-2019', - 'iPad Air 2020' => 'ipad-air-2020', - 'iPad Case' => 'ipad-case', - 'iPad mini' => 'ipad-mini', - 'iPad Pro' => 'ipad-pro', - 'iPad Pro 11' => 'ipad-pro-11', - 'iPad Pro 12.9' => 'ipad-pro-12-9', - 'iPad Pro 2020' => 'ipad-pro-2020', - 'iPad Pro 2021' => 'ipad-pro-2021', - 'IP Camera' => 'ip-camera', - 'iPhone' => 'iphone', - 'iPhone 5s' => 'iphone-5s', - 'iPhone 6' => 'iphone-6', - 'iPhone 6 Plus' => 'iphone-6-plus', - 'iPhone 6s' => 'iphone-6s', - 'iPhone 6s Plus' => 'iphone-6s-plus', - 'iPhone 7' => 'iphone-7', - 'iPhone 7 Plus' => 'iphone-7-plus', - 'iPhone 8' => 'iphone-8', - 'iPhone 8 Plus' => 'iphone-8-plus', - 'iPhone 11' => 'iphone-11', - 'iPhone 11 Pro' => 'iphone-11-pro', - 'iPhone 11 Pro Max' => 'iphone-11-pro-max', - 'iPhone 12' => 'iphone-12', - 'iPhone 12 mini' => 'iphone-12-mini', - 'iPhone 12 Pro' => 'iphone-12-pro', - 'iPhone 12 Pro Max' => 'iphone-12-pro-max', - 'iPhone Accessories' => 'iphone-accessories', - 'iPhone Case' => 'iphone-case', - 'iPhone SE' => 'iphone-se', - 'iPhone X' => 'iphone-x', - 'iPhone Xr' => 'iphone-xr', - 'iPhone Xs' => 'iphone-xs', - 'iPhone Xs Max' => 'iphone-xs-max', - 'iPod' => 'ipod', - 'iPod Nano' => 'ipod-nano', - 'iPod Shuffle' => 'ipod-shuffle', - 'iPod Touch' => 'ipod-touch', - 'Irish Whiskey' => 'irish-whisky', - 'Irn Bru' => 'irn-bru', - 'iRobot' => 'irobot', - 'Iron' => 'iron', - 'Ironing' => 'ironing', - 'Ironing Board' => 'ironing-board', - 'Iron Man' => 'iron-man', - 'Issey Miyake' => 'issey-miyake', - 'ITV' => 'itv', - 'Jabra' => 'jabra', - 'Jabra Elite 85h' => 'jabra-elite-85h', - 'Jabra Elite Active 65t' => 'jabra-elite-active-65t', - 'Jabra Elite Active 75t' => 'jabra-elite-active-75t', - 'Jabra Headphones' => 'jabra-headphones', - 'Jack & Jones' => 'jack-and-jones', - 'Jack Daniel's' => 'jack-daniels', - 'Jacket' => 'jacket', - 'Jack Wills' => 'jack-wills', - 'Jack Wolfskin' => 'jack-wolfskin', - 'Jaffa Cakes' => 'jaffa-cakes', - 'Jägermeister' => 'jagermeister', - 'Jameson' => 'jameson', - 'Jamie Oliver' => 'jamie-oliver', - 'Jaybird' => 'jaybird', - 'JBL' => 'jbl', - 'JBL Flip' => 'jbl-flip', - 'JBL GO' => 'jbl-go', - 'JBL Headphones' => 'jbl-headphones', - 'JBL Link' => 'jbl-link', - 'JBL Live' => 'jbl-live', - 'JBL Tune' => 'jbl-tune', - 'JCB' => 'jcb', - 'Jean Paul Gaultier' => 'jean-paul-gautier', - 'Jean Paul Gaultier Le Male' => 'le-male', - 'Jeans' => 'jeans', - 'Jelly Belly' => 'jelly-belly', - 'Jewellery' => 'jewellery', - 'Jigsaw' => 'jigsaw', - 'Jim Beam' => 'jim-beam', - 'Jimmy Choo' => 'jimmy-choo', - 'JML' => 'jml', - 'Jogging Bottoms' => 'jogging-bottoms', - 'Johnnie Walker' => 'johnnie-walker', - 'Johnson's' => 'johnsons', - 'John West' => 'john-west', - 'John Wick' => 'john-wick', - 'JoJo Siwa' => 'jojo', - 'Joop' => 'joop', - 'Joseph Joseph' => 'joseph-joseph', - 'Joules' => 'joules', - 'Juice' => 'juice', - 'Juicer' => 'juicer', - 'Jumper' => 'jumper', - 'Jurassic World' => 'jurassic-world', - 'Jura Whisky' => 'jura', - 'Just Cause' => 'just-cause', - 'Just Cause 3' => 'just-cause-3', - 'Just Cause 4' => 'just-cause-4', - 'Just Dance' => 'just-dance', - 'JVC' => 'jvc', - 'K-Swiss' => 'k-swiss', - 'Karcher' => 'karcher', - 'Karcher Window Vacuum' => 'karcher-window-cleaner', - 'Karen Millen' => 'karen-millen', - 'Karrimor' => 'karrimor', - 'Kaspersky' => 'kaspersky', - 'Kayak' => 'kayak', - 'Keg' => 'keg', - 'Kellogg's' => 'kelloggs', - 'Kellogg's Cornflakes' => 'cornflakes', - 'Kellogg's Crunchy Nut' => 'crunchy-nut', - 'Kenco' => 'kenco', - 'Kenwood' => 'kenwood', - 'Kenwood kMix' => 'kmix', - 'Kenzo' => 'kenzo', - 'Ketchup' => 'ketchup', - 'Keter' => 'keter', - 'Kettle' => 'kettle', - 'Kettlebell' => 'kettlebell', - 'Keyboard' => 'keyboard', - 'KIA' => 'kia', - 'Kickers' => 'kickers', - 'Kid's Bike' => 'kids-bike', - 'Kid's Clothes' => 'kids-clothes', - 'Kid's Room' => 'kids-rooms', - 'Kid's Shoes' => 'kids-shoes', - 'Kidizoom' => 'kidizoom', - 'Killzone' => 'killzone', - 'Kilner' => 'kilner', - 'Kinder' => 'kinder', - 'Kindle' => 'kindle', - 'Kindle Book' => 'kindle-book', - 'Kindle Fire' => 'kindle-fire', - 'Kindle Oasis' => 'kindle-oasis', - 'Kindle Paperwhite' => 'kindle-paperwhite', - 'Kingdom Come: Deliverance' => 'kingdom-come-deliverance', - 'Kingdom Hearts' => 'kingdom-hearts', - 'Kingdom Hearts 3' => 'kingdom-hearts-3', - 'Kingdom Hearts: The Story So Far' => 'kingdom-hearts-the-story-so-far', - 'King Kong' => 'king-kong', - 'King Size Bed' => 'king-size', - 'Kingsmill' => 'kingsmill', - 'Kingston' => 'kingston', - 'Kitchen' => 'kitchen', - 'KitchenAid' => 'kitchenaid', - 'Kitchen Appliances' => 'kitchen-appliances', - 'Kitchen Knife' => 'knife', - 'Kitchen Roll' => 'kitchen-roll', - 'Kitchen Scale' => 'kitchen-scales', - 'Kitchen Tap' => 'kitchen-tap', - 'Kitchen Utensils' => 'kitchen-utensils', - 'Kite' => 'kite', - 'KitSound' => 'kitsound', - 'Knickers' => 'knickers', - 'Kobo' => 'kobo', - 'Kodak' => 'kodak', - 'Kodi' => 'kodi', - 'Kohinoor' => 'kohinoor', - 'Kopparberg' => 'kopparberg', - 'Kraken' => 'kraken', - 'Krispy Kreme' => 'krispy-kreme', - 'Krups' => 'krups', - 'KTC' => 'ktc', - 'Kurt Geiger' => 'kurt-geiger', - 'L'Occitane' => 'loccitane', - 'L.O.L. Surprise!' => 'lol-surprise', - 'Lacoste' => 'lacoste', - 'Ladder' => 'ladder', - 'Lamaze' => 'lamaze', - 'Lamb' => 'lamb', - 'Laminate' => 'laminate', - 'Laminator' => 'laminator', - 'Lamp' => 'lamp', - 'Lancôme' => 'lancome', - 'Landmann' => 'landmann', - 'Lantern' => 'lantern', - 'Laphroaig' => 'laphroaig', - 'Laptop' => 'laptop', - 'Laptop Accessories' => 'laptop-accessories', - 'Laptop Case' => 'laptop-case', - 'Laptop Sleeve' => 'laptop-sleeve', - 'Laser Printer' => 'laser-printer', - 'Last Minute' => 'last-minute', - 'Laundry Basket' => 'laundry-basket', - 'Laura Ashley' => 'laura-ashley', - 'Lavazza' => 'lavazza', - 'Lavender' => 'lavender', - 'Lawnmower' => 'lawnmower', - 'Lay-Z-Spa' => 'lay-z-spa', - 'LeapFrog' => 'leapfrog', - 'Le Creuset' => 'le-creuset', - 'LED Bulb' => 'led-bulbs', - 'LED Light' => 'led-light', - 'LED Strip Lights' => 'led-strip-lights', - 'LED TV' => 'led-tv', - 'Lee Stafford' => 'lee-stafford', - 'Leffe' => 'leffe', - 'Leggings' => 'leggings', - 'Lego' => 'lego', - 'Lego Advent Calendar' => 'lego-advent-calendar', - 'Lego Architecture' => 'lego-architecture', - 'Lego Art' => 'lego-art', - 'Lego Batman' => 'lego-batman', - 'Lego BrickHeadz' => 'lego-brickheadz', - 'Lego City' => 'lego-city', - 'Lego Classic' => 'lego-classic', - 'Lego Creator' => 'lego-creator', - 'Lego Dimensions' => 'lego-dimensions', - 'Lego Disney' => 'lego-disney', - 'Lego Dots' => 'lego-dots', - 'Lego Duplo' => 'lego-duplo', - 'Lego Friends' => 'lego-friends', - 'LEGO Harry Potter' => 'lego-harry-potter', - 'Lego Hidden Side' => 'lego-hidden-side', - 'Legoland' => 'legoland', - 'Lego Marvel' => 'lego-marvel', - 'Lego Mindstorms' => 'lego-mindstorms', - 'Lego Nexo Knights' => 'lego-nexo-knights', - 'Lego Ninjago' => 'lego-ninjago', - 'Lego Porsche' => 'lego-porsche', - 'Lego Simpsons' => 'lego-simpsons', - 'Lego Speed Champions' => 'lego-speed-champions', - 'Lego Star Wars' => 'lego-star-wars', - 'Lego Star Wars Millennium Falcon' => 'lego-star-wars-millennium-falcon', - 'Lego Super Mario' => 'lego-mario', - 'Lego Technic' => 'lego-technic', - 'Lego VIDIYO' => 'lego-vidiyo', - 'Lemonade' => 'lemonade', - 'Lenor' => 'lenor', - 'Lenovo' => 'lenovo', - 'Lenovo IdeaPad' => 'lenovo-ideapad', - 'Lenovo Laptop' => 'lenovo-laptop', - 'Lenovo Tablet' => 'lenovo-tablet', - 'Lenovo Thinkpad' => 'thinkpad', - 'Lenovo Yoga Laptop' => 'lenovo-yoga-laptop', - 'Lenovo Yoga Tablet' => 'lenovo-yoga', - 'Les Paul' => 'les-paul', - 'Levi's' => 'levi', - 'Lexar' => 'lexar', - 'LG' => 'lg', - 'LG G3' => 'lg-g3', - 'LG G5' => 'lg-g5', - 'LG G6' => 'lg-g6', - 'LG G7' => 'lg-g7', - 'LG G8S ThinQ' => 'lg-g8s-thinq', - 'LG OLED TV' => 'lg-oled-tv', - 'LG Smartphone' => 'lg-smartphone', - 'LG TV' => 'lg-tv', - 'LG V30' => 'lg-v30', - 'LG V40 ThinQ' => 'lg-v40-thinq', - 'Life Insurance' => 'life-insurance', - 'Life is Strange' => 'life-is-strange', - 'Light Box' => 'light-box', - 'Lighting' => 'lighting', - 'Lightning Cable' => 'lightning-cable', - 'Lightsaber' => 'lightsaber', - 'Lindor' => 'lindor', - 'Lindt' => 'lindt', - 'Lingerie' => 'lingerie', - 'Linksys' => 'linksys', - 'Linx' => 'linx', - 'Lion King' => 'lion-king', - 'Lipstick' => 'lipstick', - 'Lipsy' => 'lipsy', - 'Little Tikes' => 'little-tikes', - 'Liverpool F. C.' => 'liverpool-fc', - 'Living Room' => 'living-room', - 'Local Traffic' => 'local-traffic', - 'Lodge' => 'lodge', - 'Loft' => 'loft', - 'Logitech' => 'logitech', - 'Logitech G430' => 'logitech-g430', - 'Logitech G703' => 'logitech-g703', - 'Logitech G903' => 'logitech-g903', - 'Logitech Harmony' => 'harmony', - 'Logitech Keyboard' => 'logitech-keyboard', - 'Logitech Mouse' => 'logitech-mouse', - 'Logitech MX Master' => 'logitech-mx-master', - 'Logitech MX Master 2S' => 'logitech-mx-master-2s', - 'London Eye' => 'london-eye', - 'London Zoo' => 'london-zoo', - 'Longleat' => 'longleat', - 'Long Sleeve' => 'long-sleeve', - 'Lord of the Rings' => 'lord-of-the-rings', - 'Lottery' => 'lottery', - 'Lounger' => 'lounger', - 'Lowepro' => 'lowepro', - 'Lucozade' => 'lucozade', - 'Luigi' => 'luigi', - 'Luigi's Mansion' => 'luigis-manison', - 'Luigi's Mansion 3' => 'luigis-mansion-3', - 'Lunch Bag' => 'lunch-bag', - 'Lunch Box' => 'lunch-box', - 'Lurpak' => 'lurpak', - 'Luton' => 'luton', - 'Lyle & Scott' => 'lyle-and-scott', - 'Lynx' => 'lynx', - 'M.2 SSD' => 'm2-ssd', - 'MacBook' => 'macbook', - 'MacBook Air' => 'macbook-air', - 'MacBook Pro' => 'macbook-pro', - 'MacBook Pro 13' => 'macbook-pro-13', - 'MacBook Pro 15' => 'macbook-pro-15', - 'MacBook Pro 16' => 'macbook-pro-16', - 'Maclaren' => 'maclaren', - 'Mac mini' => 'mac-mini', - 'Madame Tussauds' => 'madame-tussauds', - 'Mad Catz' => 'madcatz', - 'Madden NFL' => 'madden', - 'Madden NFL 20' => 'madden-nfl-20', - 'Mad Max' => 'mad-max', - 'Mafia 3' => 'mafia-3', - 'Magazine' => 'magazine', - 'Magimix' => 'magimix', - 'Magners' => 'magners', - 'Magnum' => 'magnum', - 'Make Up' => 'make-up', - 'Makeup Advent Calendar' => 'makeup-advent-calendar', - 'Make Up Brush' => 'make-up-brush', - 'Makita' => 'makita', - 'Makita Drill' => 'makita-drill', - 'Malibu' => 'malibu', - 'Maltesers' => 'maltesers', - 'MAM' => 'mam', - 'Mamas & Papas' => 'mamas-and-papas', - 'Manchester United' => 'manchester-united', - 'Manfrotto' => 'manfrotto', - 'Manga' => 'manga', - 'Manuka Honey' => 'manuka-honey', - 'Marantz' => 'marantz', - 'Marc Jacobs' => 'marc-jacobs', - 'Marc Jacobs Daisy' => 'daisy', - 'Mario & Sonic at the Olympic Games: Tokyo 2020' => 'mario-and-sonic-tokyo-2020', - 'Mario + Rabbids Kingdom Battle' => 'mario-rabbids-kingdom-battle', - 'Mario Kart' => 'mario-kart', - 'Mario Kart 8' => 'mario-kart-8', - 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe', - 'Marmite' => 'marmite', - 'Mars' => 'mars', - 'Marshall' => 'marshall', - 'Marshall Headphones' => 'marshall-headphones', - 'Marvel' => 'marvel', - 'Marvel's Spider-Man (PS4)' => 'spider-man-2018', - 'Marvel's Spider-Man: Miles Morales' => 'spiderman-miles-morales', - 'Mascara' => 'mascara', - 'Massage' => 'massage', - 'Mass Effect' => 'mass-effect', - 'Mass Effect: Andromeda' => 'mass-effect-andromeda', - 'Mastercard' => 'mastercard', - 'Masterplug' => 'masterplug', - 'Maternity & Pregnancy' => 'maternity', - 'Mattress' => 'mattress', - 'Mattress Protector' => 'mattress-protector', - 'Mattress Topper' => 'mattress-topper', - 'Mavic' => 'mavic', - 'Max Factor' => 'max-factor', - 'Maxi Cosi' => 'maxi-cosi', - 'Maximuscle' => 'maximuscle', - 'Maxtor' => 'maxtor', - 'Maybelline' => 'maybelline', - 'Mayo' => 'mayo', - 'Mazda' => 'mazda', - 'McAfee' => 'mcafee', - 'Meat & Sausages' => 'meat', - 'Meccano' => 'meccano', - 'Mechanical Keyboard' => 'mechanical-keyboard', - 'Medal of Honor' => 'medal-of-honor', - 'Medela' => 'medela', - 'Media Player' => 'media-player', - 'Medievil' => 'medievil', - 'Medion' => 'medion', - 'Mega Bloks' => 'mega-bloks', - 'Megathread' => 'megathread', - 'Melissa & Doug' => 'melissa', - 'Memory Cards' => 'memory-cards', - 'Memory Foam Mattress' => 'memory-foam', - 'Men's Boots' => 'mens-boots', - 'Men's Fragrance' => 'mens-fragrance', - 'Men's Shoes' => 'mens-shoes', - 'Men's Suit' => 'suit', - 'Mercedes' => 'mercedes', - 'Meridian' => 'meridian', - 'Merlin' => 'merlin', - 'Merrell' => 'merrell', - 'Messenger Bag' => 'messenger-bag', - 'Metal Gear Solid' => 'metal-gear-solid', - 'Metro Exodus' => 'metro-exodus', - 'Metroid' => 'metroid', - 'Metro Series' => 'metro-series', - 'Michael Kors' => 'michael-kors', - 'Michelin' => 'michelin', - 'Microphone' => 'microphone', - 'Micro SD Card' => 'micro-sd', - 'Micro SDHC' => 'micro-sdhc', - 'Micro SDXC' => 'micro-sdxc', - 'Microserver' => 'microserver', - 'Microsoft' => 'microsoft', - 'Microsoft Flight Simulator' => 'microsoft-flight-simulator', - 'Microsoft Office' => 'microsoft-office', - 'Microsoft Points' => 'microsoft-points', - 'Microsoft Software' => 'microsoft-software', - 'Microsoft Surface Book' => 'surface-book', - 'Microsoft Surface Laptop' => 'surface', - 'Microsoft Surface Pro 6' => 'surface-pro-6', - 'Microsoft Surface Pro 7' => 'surface-pro-7', - 'Microsoft Surface Tablet' => 'microsoft-surface-tablet', - 'Microwave' => 'microwave', - 'Middle Earth' => 'middle-earth', - 'Middle Earth: Shadow of Mordor' => 'shadow-of-mordor', - 'Middle Earth: Shadow of War' => 'middle-earth-shadow-of-war', - 'Miele' => 'miele', - 'Miele Vacuum Cleaner' => 'miele-vacuum-cleaner', - 'Milk' => 'milk', - 'Milk Frother' => 'milk-frother', - 'Milk Tray' => 'milk-tray', - 'Milwaukee' => 'milwaukee', - 'Mince' => 'mince', - 'Minecraft Game' => 'minecraft', - 'Mineral Water' => 'mineral-water', - 'Mini Fridge' => 'mini-fridge', - 'Minions' => 'minions', - 'Mini PC' => 'mini-pc', - 'Minky' => 'minky', - 'Mira' => 'mira', - 'Mirror' => 'mirror', - 'Mirror's Edge' => 'mirrors-edge', - 'Misc' => 'misc', - 'Misfit' => 'misfit', - 'Mitre Saw' => 'mitre-saw', - 'Mitsubishi' => 'mitsubishi', - 'Mixer & Blender' => 'mixer-and-blender', - 'Mobile Contracts' => 'mobile-contract', - 'Mobile Phone' => 'mobile-phone', - 'Model Building' => 'model-building', - 'Moët' => 'moet', - 'Molton Brown' => 'molton-brown', - 'Money Saving Tips and Tricks' => 'money-saving-tips', - 'Monitor' => 'monitor', - 'Monopoly' => 'monopoly', - 'Monsoon' => 'monsoon', - 'Monster Energy' => 'monster-energy', - 'Monster High' => 'monster-high', - 'Monster Hunter' => 'monster-hunter', - 'Monster Hunter World' => 'monster-hunter-world', - 'Mont Blanc' => 'mont-blanc', - 'Mop' => 'mop', - 'Morphy Richards' => 'morphy-richards', - 'Mortal Kombat' => 'mortal-kombat', - 'Mortal Kombat 11' => 'mortal-kombat-11', - 'Mortgage' => 'mortgage', - 'Moschino' => 'moschino', - 'Moses Basket' => 'moses-basket', - 'MOT' => 'mot', - 'Motherboard' => 'motherboard', - 'Moto 360' => 'moto-360', - 'Moto E' => 'moto-e', - 'Moto G' => 'moto-g', - 'Moto G4' => 'moto-g4', - 'Moto G5' => 'moto-g5', - 'Moto G6' => 'moto-g6', - 'Moto G7' => 'moto-g7', - 'Motorcycle' => 'motorcycle', - 'Motorcycle Accessories' => 'motorcycle-accessories', - 'Motorcycle Helmet' => 'motorcycle-helmet', - 'Motorola' => 'motorola', - 'Motorola Smartphone' => 'motorola-smartphone', - 'Moto X' => 'moto-x', - 'Moto Z' => 'moto-z', - 'Mountain Bike' => 'mountain-bike', - 'Mouse & Keyboard Bundles' => 'mouse-and-keyboard-bundle', - 'Mouse Mat' => 'mouse-mat', - 'Mouthwash' => 'mouthwash', - 'Movie and TV Box Set' => 'box-set', - 'Movies & Series' => 'movie', - 'MP3 Player' => 'mp3-player', - 'Mr Kipling' => 'mr-kipling', - 'Mr Men' => 'mr-men', - 'MSI' => 'msi', - 'MSI Laptop' => 'msi-laptop', - 'Muc-Off' => 'muc-off', - 'Mug' => 'mug', - 'Muller' => 'muller', - 'Multi-Room Audio System' => 'multi-room-audio-system', - 'Multitool' => 'multitool', - 'Museums' => 'museums', - 'Music' => 'music', - 'Musical Instruments' => 'musical-instrument', - 'Music App' => 'music-app', - 'Music Streaming' => 'music-streaming', - 'My Little Pony' => 'my-little-pony', - 'Nail Gun' => 'nail-gun', - 'Nail Polish' => 'nail-polish', - 'Nails' => 'nails', - 'Nails Inc.' => 'nails-inc', - 'Nakd' => 'nakd', - 'Nando's' => 'nandos', - 'Nappy' => 'nappy', - 'NAS' => 'nas', - 'National Express Ticket' => 'national-express', - 'National Trust' => 'national-trust', - 'Nature Observation' => 'nature-observation', - 'NatWest' => 'natwest', - 'NBA 2K' => 'nba-2k', - 'NBA Live' => 'nba', - 'Necklace' => 'necklace', - 'Need for Speed' => 'need-for-speed', - 'Need for Speed: Payback' => 'need-for-speed-payback', - 'Need for Speed Heat' => 'need-for-speed-heat', - 'Neff' => 'neff', - 'Nerf Guns' => 'nerf', - 'Nescafé Azera' => 'azera', - 'Nescafé Coffee' => 'nescafe', - 'Nespresso' => 'nespresso', - 'Nespresso Coffee Machine' => 'nespresso-coffee-machine', - 'Nest Hello' => 'nest-hello', - 'Nestlé' => 'nestle', - 'Nest Learning Thermostat' => 'nest-learning-thermostat', - 'Nestlé Cheerios' => 'cheerios', - 'Nestlé Shreddies' => 'shreddies', - 'Netatmo' => 'netatmo', - 'Netflix' => 'netflix', - 'Netgear' => 'netgear', - 'Netgear Arlo' => 'arlo', - 'New Balance' => 'new-balance', - 'New Balance Trainers' => 'new-balance-trainers', - 'New Look' => 'new-look', - 'Newspapers' => 'newspapers', - 'Nextbase' => 'nextbase', - 'NFL' => 'nfl', - 'NHL' => 'nhl', - 'NHL 20' => 'nhl-20', - 'NHS' => 'nhs', - 'NieR: Automata' => 'nier', - 'Night Light' => 'night-light', - 'Nike' => 'nike', - 'Nike Air Max' => 'nike-air-max', - 'Nike Air Max 200' => 'nike-air-max-200', - 'Nike Air Max 270' => 'nike-air-max-270', - 'Nike Air Max 720' => 'nike-air-max-720', - 'Nike Free' => 'nike-free', - 'Nike Huarache' => 'nike-huarache', - 'Nike Jordan' => 'jordan', - 'Nike Presto' => 'nike-presto', - 'Nike Roshe' => 'nike-roshe', - 'Nike Trainers' => 'nike-shoes', - 'Nikon' => 'nikon', - 'Nikon Camera' => 'nikon-camera', - 'Nikon Coolpix' => 'nikon-coolpix', - 'Nikon D3400' => 'nikon-d3400', - 'Nikon Lens' => 'nikon-lens', - 'Nilfisk' => 'nilfisk', - 'Ni No Kuni' => 'ni-no-kuni', - 'Ni No Kuni: Wrath of the White Witch' => 'ni-no-kuni-white-witch', - 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-2', - 'Nintendo' => 'nintendo', - 'Nintendo 2DS' => '2ds', - 'Nintendo 3DS' => '3ds', - 'Nintendo 3DS Game' => '3ds-games', - 'Nintendo 3DS XL' => 'nintendo-3ds-xl', - 'Nintendo Accessories' => 'nintendo-accessories', - 'Nintendo Classic Mini' => 'nintendo-classic-mini', - 'Nintendo DS Game' => 'ds-games', - 'Nintendo Labo' => 'switch-labo', - 'Nintendo Switch' => 'nintendo-switch', - 'Nintendo Switch Accessories' => 'switch-accessories', - 'Nintendo Switch Case' => 'switch-case', - 'Nintendo Switch Controller' => 'switch-controller', - 'Nintendo Switch Game' => 'switch-game', - 'Nintendo Switch Joy-Con' => 'switch-joy-con', - 'Nintendo Switch Lite' => 'nintendo-switch-lite', - 'Nintendo Switch Pro Controller' => 'switch-pro-controller', - 'Nioh' => 'nioh', - 'Nissan' => 'nissan', - 'Nivea' => 'nivea', - 'No7' => 'no7', - 'Noise Cancelling Headphones' => 'noise-cancelling-headphones', - 'Nokia' => 'nokia', - 'Nokia Smartphones' => 'nokia-mobile', - 'No Man's Sky' => 'no-man-s-sky', - 'Noodles' => 'noodles', - 'Norton' => 'norton', - 'Now' => 'now-tv', - 'Numatic' => 'numatic', - 'Nursery' => 'nursery', - 'Nutella' => 'nutella', - 'NutriBullet' => 'nutribullet', - 'Nutri Ninja' => 'nutri-ninja', - 'Nuts' => 'nuts', - 'Nvidia' => 'nvidia', - 'Nvidia GeForce' => 'geforce', - 'Nvidia Shield' => 'nvidia-shield', - 'NYX' => 'nyx', - 'NZXT' => 'nzxt', - 'O2' => 'o2', - 'O2 Refresh' => 'o2-refresh', - 'Oakley' => 'oakley', - 'Octonauts' => 'octonauts', - 'Oculus Game' => 'oculus-game', - 'Oculus Go' => 'oculus-go', - 'Oculus Quest' => 'oculus-quest', - 'Oculus Rift' => 'oculus', - 'Oculus Rift S' => 'oculus-rift-s', - 'Odeon' => 'odeon', - 'Office' => 'office', - 'Office Chair' => 'office-chair', - 'Official Announcements' => 'official-announcements', - 'Olay' => 'olay', - 'OLED TV' => 'oled', - 'Olive Oil' => 'olive-oil', - 'Olympus' => 'olympus', - 'Omega Seamaster' => 'omega-seamaster', - 'Omega Speedmaster' => 'omega-speedmaster', - 'Omega Watches' => 'omega-watch', - 'OnePlus 3' => 'oneplus-3', - 'OnePlus 5' => 'oneplus-5', - 'OnePlus 6' => 'oneplus-6', - 'OnePlus 6T' => 'oneplus-6t', - 'OnePlus 7' => 'oneplus-7', - 'OnePlus 7 Pro' => 'oneplus-7-pro', - 'OnePlus 7T' => 'oneplus-7t', - 'OnePlus 7T Pro' => 'one-plus-7t-pro', - 'OnePlus 8' => 'oneplus-8', - 'OnePlus 8 Pro' => 'oneplus-8-pro', - 'OnePlus 8T' => 'oneplus-8t', - 'OnePlus 9' => 'oneplus-9', - 'OnePlus 9 Pro' => 'oneplus-9-pro', - 'OnePlus Nord' => 'oneplus-nord', - 'OnePlus Nord N10 5G' => 'oneplus-n10', - 'OnePlus Nord N100' => 'oneplus-n100', - 'OnePlus Smartphone' => 'oneplus', - 'Onesie' => 'onesie', - 'Onkyo' => 'onkyo', - 'Online Courses' => 'online-courses', - 'Operating System' => 'operating-system', - 'Oppo Find X2 Lite' => 'oppo-find-x2-lite', - 'Oppo Find X2 Neo' => 'oppo-find-x2-neo', - 'Oppo Find X2 Pro' => 'oppo-find-x2-pro', - 'Oppo Reno' => 'oppo-reno', - 'Oppo Reno4 5G' => 'oppo-reno4', - 'Oppo Reno4 Z 5G' => 'oppo-reno4-z', - 'Oppo Smartphone' => 'oppo-smartphone', - 'Opticians' => 'opticians', - 'Optoma' => 'optoma', - 'Oral-B' => 'oral-b', - 'Oral-B Toothbrush' => 'oral-b-toothbrush', - 'Oreo' => 'oreo', - 'Origin' => 'origin', - 'Original Penguin' => 'penguin', - 'Orla Kiely' => 'orla-kiely', - 'Osprey' => 'osprey', - 'Osram' => 'osram', - 'Other' => 'other-deals', - 'Ottoman' => 'ottoman', - 'Oukitel' => 'oukitel', - 'Outdoor Clothing' => 'outdoor-clothing', - 'Outdoor Lighting' => 'outdoor-lighting', - 'Outdoor Sports & Camping' => 'outdoor', - 'Outdoor Toys' => 'outdoor-toys', - 'Outlast' => 'outlast', - 'Outlet' => 'outlet', - 'Outwell' => 'outwell', - 'Oven' => 'oven', - 'Overcooked' => 'overcooked', - 'Overcooked 2' => 'overcooked-2', - 'Overwatch' => 'overwatch', - 'Oyster Card' => 'oyster', - 'Package Holidays' => 'holiday', - 'Paco Rabanne' => 'paco-rabanne', - 'Paco Rabanne 1 Million' => 'paco-rabanne-1-million', - 'Paco Rabanne Lady Million' => 'lady-million', - 'Paddling Pool' => 'paddling-pool', - 'Padlock' => 'padlock', - 'Paint' => 'paint', - 'Paint Brush' => 'paint-brush', - 'Pampers' => 'pampers', - 'Panasonic' => 'panasonic', - 'Panasonic Camera' => 'panasonic-camera', - 'Panasonic Lumix' => 'lumix', - 'Panasonic TV' => 'panasonic-tv', - 'Pandora' => 'pandora', - 'Panini' => 'panini', - 'Panini Stickers' => 'panini-stickers', - 'Papa Johns' => 'papa-johns', - 'Paper Mario' => 'paper-mario', - 'Parasol' => 'parasol', - 'Parcel and Delivery Services' => 'parcel', - 'Parka' => 'parka', - 'Parking' => 'parking', - 'Parrot' => 'parrot', - 'Paul Smith' => 'paul-smith', - 'PAW Patrol' => 'paw-patrol', - 'Payday' => 'payday', - 'Payday 2' => 'payday-2', - 'PAYG' => 'payg', - 'Pay Monthly' => 'pay-monthly', - 'PC' => 'pc', - 'PC Case' => 'pc-case', - 'PC Game' => 'pc-game', - 'PC Gaming Accessories' => 'pc-gaming-accessories', - 'PC Gaming Systems' => 'pc-gaming-systems', - 'PC Mouse' => 'mouse', - 'PC Parts' => 'pc-parts', - 'Peanut Butter' => 'peanut-butter', - 'Peanuts' => 'peanuts', - 'Pedometer' => 'pedometer', - 'Pentax' => 'pentax', - 'Peppa Pig' => 'peppa-pig', - 'PepperBonus' => 'pepperbonus', - 'Pepsi' => 'pepsi', - 'Perfume' => 'perfume', - 'Persil' => 'persil', - 'Persona' => 'persona', - 'Persona 5' => 'persona-5', - 'Personal Care & Hygiene' => 'personal-care-hygiene', - 'Petrol and Diesel' => 'petrol', - 'Pet Supplies' => 'pets', - 'Peugeot' => 'peugeot', - 'PG Tips' => 'pg-tips', - 'Philips' => 'philips', - 'Philips Alarm Clock' => 'philips-alarm-clock', - 'Philips Avent' => 'avent', - 'Philips Hue' => 'philips-hue', - 'Philips Lumea' => 'lumea', - 'Philips OneBlade' => 'philips-one-blade', - 'Philips Senseo' => 'philips-senseo', - 'Philips Senseo Coffee Machine' => 'philips-senseo-coffee-machine', - 'Philips Shaver' => 'philips-shaver', - 'Philips Sonicare' => 'sonicare', - 'Philips TV' => 'philips-tv', - 'Phone Holder' => 'phone-holder', - 'Phones & Accessories' => 'phone', - 'Photo & Cameras' => 'photo-video', - 'Photo & Video App' => 'photo-video-app', - 'Photo Editing' => 'photo-editing', - 'Photo Frame' => 'photo-frame', - 'Photo Paper' => 'photo-paper', - 'Piano' => 'piano', - 'Picnic & Outdoor Cooking' => 'picnic', - 'Pikmin 3 Deluxe' => 'pikmin-3-deluxe', - 'Pillow' => 'pillow', - 'Pimm's' => 'pimms', - 'Pioneer' => 'pioneer', - 'Pirate Toys' => 'pirates', - 'PIR Lights' => 'pir', - 'Pixel C' => 'pixel-c', - 'Piz Buin' => 'piz-buin', - 'Pizza' => 'pizza', - 'Pizza Stone' => 'pizza-stone', - 'Planer' => 'planer', - 'Planet Earth' => 'planet-earth', - 'Plant' => 'plant', - 'Plant Pot' => 'plant-pots', - 'Plants vs. Zombies: Battle for Neighborville' => 'battle-for-neighborville', - 'Plants vs Zombies' => 'plants-vs-zombies', - 'Play-Doh' => 'play-doh', - 'PlayerUnknown's Battlegrounds' => 'playerunknown-s-battlegrounds', - 'Playhouse' => 'playhouse', - 'Playing Cards' => 'playing-cards', - 'Playmat' => 'playmat', - 'Playmobil' => 'playmobil', - 'Playmobil Advent Calendar' => 'playmobil-advent-calendar', - 'PlayStation' => 'playstation', - 'PlayStation 5 DualSense Controller' => 'ps5-controller', - 'PlayStation Accessories' => 'playstation-accessories', - 'PlayStation Classic' => 'playstation-classic', - 'PlayStation Move' => 'playstation-move', - 'PlayStation Now' => 'playstation-now', - 'PlayStation Plus' => 'playstation-plus', - 'PlayStation VR' => 'playstation-vr', - 'PlayStation VR Aim Controller' => 'aim-controller-ps4', - 'Pliers' => 'pliers', - 'Plumbing & Fittings' => 'plumbing-and-fitting', - 'Plus Size' => 'plus-size', - 'PNY' => 'pny', - 'POCO F2 Pro' => 'poco-f2-pro', - 'POCO F3' => 'poco-f3', - 'Poco M3' => 'poco-m3', - 'POCO X3' => 'poco-x3', - 'POCO X3 Pro' => 'poco-x3-pro', - 'Pokémon' => 'pokemon', - 'Pokémon: Let's Go' => 'pokemon-lets-go', - 'Pokémon Go' => 'pokemon-go', - 'Pokemon Sword and Shield' => 'pokemon-sword-and-shield', - 'Pokémon Ultra Sun and Ultra Moon' => 'pokemon-ultra-sun-ultra-moon', - 'Poker' => 'poker', - 'Pokken Tournament' => 'pokken-tournament', - 'Polaroid' => 'polaroid', - 'Police Toys' => 'police', - 'Polo Shirt' => 'polo-shirt', - 'Pool' => 'pool', - 'Pool & Snooker' => 'pool-table', - 'Popcorn' => 'popcorn', - 'Pork' => 'pork', - 'Porridge & Oats' => 'porridge-and-oats', - 'Portable Wireless Speaker' => 'wireless-speaker', - 'Poster' => 'poster', - 'Pots and Pans' => 'pan', - 'Potty' => 'potty', - 'Power Bank' => 'power-bank', - 'Powerbeats Pro' => 'powerbeats-pro', - 'Power Dental Flosser' => 'floss', - 'Powerline' => 'powerline', - 'Power Rangers' => 'power-rangers', - 'Power Tool' => 'power-tool', - 'Prada' => 'prada', - 'Pram' => 'pram', - 'Pregnancy' => 'pregnancy', - 'Prescription Glasses' => 'prescription-glasses', - 'Pressure Cooker' => 'pressure-cooker', - 'Pressure Washer' => 'pressure-washer', - 'Price Glitch' => 'price-glitch', - 'Prime Gaming' => 'twitch', - 'Pringles' => 'pringles', - 'Printer & Printer Supplies' => 'printer', - 'Printer Supplies' => 'printer-supplies', - 'Productivity App' => 'productivity-app', - 'Pro Evolution Soccer' => 'pro-evolution-soccer', - 'Pro Evolution Soccer 2018' => 'pro-evolution-soccer-2018', - 'Pro Evolution Soccer 2019' => 'pro-evolution-soccer-2019', - 'Pro Evolution Soccer 2020' => 'pes-2020', - 'Project Cars' => 'project-cars', - 'Project Cars 2' => 'project-cars-2', - 'Projector' => 'projector', - 'Protein' => 'protein', - 'Protein Bars' => 'protein-bars', - 'Protein Shaker' => 'shaker', - 'PS4' => 'ps4-slim', - 'PS4 Camera' => 'ps4-camera', - 'PS4 Controller' => 'ps4-controller', - 'PS4 Games' => 'ps4-games', - 'PS4 Headset' => 'ps4-headset', - 'PS4 Pro' => 'ps4-pro', - 'PS5' => 'ps5', - 'PS5 Games' => 'ps5-game', - 'PSU' => 'psu', - 'Public Transport' => 'public-transport', - 'Pukka' => 'pukka', - 'Pulse Light Epilator' => 'pulse-light-epilator', - 'Puma' => 'puma', - 'Puma Trainers' => 'puma-trainers', - 'Puppy Supplies' => 'puppy', - 'Purse' => 'purse', - 'Pushchair' => 'pushchair', - 'Pushchairs and Strollers' => 'baby-transport', - 'Puzzle' => 'puzzle', - 'PVR' => 'pvr', - 'Pyjamas' => 'pyjamas', - 'Pyrex' => 'pyrex', - 'Q Acoustics' => 'q-acoustics', - 'QNAP' => 'qnap', - 'Qualcast' => 'qualcast', - 'Quality Street' => 'quality-street', - 'Quantum Break' => 'quantum-break', - 'Quechua' => 'quechua', - 'Quick Charge' => 'quick-charge', - 'Quiksilver' => 'quiksilver', - 'Quinny' => 'quinny', - 'Quorn' => 'quorn', - 'Rab' => 'rab', - 'Radeon RX 480' => 'rx-480', - 'Radeon RX 5700' => 'radeon-rx-5700', - 'Radeon RX 5700 XT' => 'radeon-rx-5700-xt', - 'Radeon RX 6800' => 'radeon-rx-6800', - 'Radeon RX 6800 XT' => 'radeon-rx-6800-xt', - 'Radeon RX 6900 XT' => 'radeon-rx-6900-xt', - 'Radiator' => 'radiator', - 'Radio' => 'radio', - 'Radley' => 'radley', - 'Rage 2' => 'rage-2', - 'Railcard' => 'railcard', - 'Rainbow Six' => 'rainbow-six', - 'Rake' => 'rake', - 'Ralph Lauren' => 'ralph-lauren', - 'RAM' => 'ram', - 'Raspberry Pi' => 'raspberry-pi', - 'Ratchet' => 'ratchet', - 'Ratchet and Clank' => 'ratchet-and-clank', - 'Rattan Garden Furniture' => 'rattan', - 'RAVPower' => 'ravpower', - 'Ray Ban' => 'ray-ban', - 'Razer' => 'razer', - 'Razor' => 'razor', - 'Razor Blade' => 'razor-blade', - 'Real Madrid' => 'real-madrid', - 'Realme Smartphones' => 'realme-smartphone', - 'Real Techniques' => 'real-techniques', - 'Recliner' => 'recliner', - 'ReCore' => 'recore', - 'Recreational Sports' => 'recreational-sports', - 'Red Bull' => 'red-bull', - 'Red Dead Redemption' => 'red-dead-redemption', - 'Red Dead Redemption 2' => 'red-dead-redemption-2', - 'Redex' => 'redex', - 'Red Kite' => 'red-kite', - 'Reebok' => 'reebok', - 'Reese's' => 'reeses', - 'Regatta' => 'regatta', - 'Regina' => 'regina', - 'Remington' => 'remington', - 'Remote Control Car' => 'remote-control-car', - 'Renault' => 'renault', - 'Resident Evil' => 'resident-evil', - 'Resident Evil 2' => 'resident-evil-2', - 'Resident Evil 7' => 'resident-evil-7', - 'Restaurant, Café & Pub' => 'restaurant', - 'Retailer Offers and Issues' => 'retailer-offers-and-issues', - 'Ribena' => 'ribena', - 'Rice' => 'rice', - 'Rice Cooker' => 'rice-cooker', - 'Rick and Morty' => 'rick-and-morty', - 'Ricoh' => 'ricoh', - 'Ride On' => 'ride-on', - 'Ring' => 'ring', - 'Ring Door View Cam' => 'ring-door-view-cam', - 'Ring Fit Adventures' => 'ring-fit-adventures', - 'Ring Stick Up Cam' => 'ring-stick-up-cam', - 'Ring Video Doorbell' => 'ring-video-doorbell', - 'Ring Video Doorbell 2' => 'ring-video-doorbell-2', - 'Ring Video Doorbell 3' => 'ring-video-doorbell-3', - 'Ring Video Doorbell Pro' => 'ring-video-doorbell-pro', - 'Road Bike' => 'road-bike', - 'Roaming' => 'roaming', - 'Robinsons' => 'robinsons', - 'Robotic Lawnmower' => 'robotic-lawnmower', - 'Robot Vacuum Cleaner' => 'robot-vacuum-cleaner', - 'Rock Band' => 'rock-band', - 'Rocket League' => 'rocket-league', - 'Rocking Horse' => 'rocking-horse', - 'Rogue One: A Star Wars Story' => 'rogue-one', - 'Roku' => 'roku', - 'Rolex' => 'rolex', - 'Rollerskates' => 'skate', - 'Ronseal' => 'ronseal', - 'Roof Box' => 'roof-box', - 'Roses' => 'roses', - 'Rotary' => 'rotary', - 'Router' => 'router', - 'Rowenta' => 'rowenta', - 'RTX 2060' => 'rtx-2060', - 'RTX 2070' => 'rtx-2070', - 'RTX 2080' => 'rtx-2080', - 'RTX 2080 Ti' => 'rtx-2080-ti', - 'RTX 3070' => 'rtx-3070', - 'RTX 3080' => 'rtx-3080', - 'RTX 3090' => 'rtx-3090', - 'Rug' => 'rug', - 'Rugby' => 'rugby', - 'Rum' => 'rum', - 'Running' => 'running', - 'Running Shoes' => 'running-shoes', - 'Russell Hobbs' => 'russell-hobbs', - 'RX 570' => 'rx-570', - 'RX 580' => 'rx-580', - 'RX 590' => 'rx-590', - 'RX Vega 56' => 'rx-vega-56', - 'RX Vega 64' => 'rx-vega-64', - 'Ryanair' => 'ryanair', - 'Ryobi' => 'ryobi', - 'Safari' => 'safari', - 'Safety Boots' => 'safety-boots', - 'Sage by Heston Blumenthal' => 'sage', - 'Saints Row' => 'saints-row', - 'Saitek' => 'saitek', - 'Sale' => 'sale', - 'Salmon' => 'salmon', - 'Salomon' => 'salomon', - 'Salter' => 'salter', - 'Samsonite' => 'samsonite', - 'Samsung' => 'samsung', - 'Samsung Ecobubble' => 'ecobubble', - 'Samsung Fridge' => 'samsung-fridge', - 'Samsung Galaxy' => 'samsung-galaxy', - 'Samsung Galaxy A10' => 'samsung-galaxy-a10', - 'Samsung Galaxy A20e' => 'samsung-galaxy-a20e', - 'Samsung Galaxy A40' => 'samsung-galaxy-a40', - 'Samsung Galaxy A42 5G' => 'samsung-galaxy-a42-5g', - 'Samsung Galaxy A50' => 'samsung-galaxy-a50', - 'Samsung Galaxy A51' => 'samsung-galaxy-a51', - 'Samsung Galaxy A52 5G' => 'samsung-galaxy-a52', - 'Samsung Galaxy A60' => 'samsung-galaxy-a60', - 'Samsung Galaxy A70' => 'samsung-galaxy-a70', - 'Samsung Galaxy A71' => 'samsung-galaxy-a71', - 'Samsung Galaxy A72' => 'samsung-galaxy-a72', - 'Samsung Galaxy A80' => 'samsung-galaxy-a80', - 'Samsung Galaxy A90' => 'samsung-galaxy-a90', - 'Samsung Galaxy Buds' => 'samsung-galaxy-buds', - 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus', - 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live', - 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro', - 'Samsung Galaxy Fold' => 'samsung-galaxy-fold', - 'Samsung Galaxy J5' => 'galaxy-j5', - 'Samsung Galaxy Note' => 'samsung-galaxy-note', - 'Samsung Galaxy Note 8' => 'samsung-galaxy-note-8', - 'Samsung Galaxy Note 9' => 'samsung-galaxy-note-9', - 'Samsung Galaxy Note 10' => 'samsung-galaxy-note-10', - 'Samsung Galaxy Note 10+' => 'samsung-galaxy-note-10-plus', - 'Samsung Galaxy Note20' => 'samsung-galaxy-note20', - 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note20-ultra', - 'Samsung Galaxy S6' => 'samsung-galaxy-s6', - 'Samsung Galaxy S7' => 'samsung-galaxy-s7', - 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge', - 'Samsung Galaxy S8' => 'samsung-galaxy-s8', - 'Samsung Galaxy S8+' => 'samsung-s8-plus', - 'Samsung Galaxy S9' => 'samsung-galaxy-s9', - 'Samsung Galaxy S9 Plus' => 'samsung-s9-plus', - 'Samsung Galaxy S10' => 'samsung-galaxy-s10', - 'Samsung Galaxy S10 Lite' => 'samsung-galaxy-s10-lite', - 'Samsung Galaxy S10 Plus' => 'samsung-galaxy-s10-plus', - 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e', - 'Samsung Galaxy S20' => 'samsung-galaxy-s20', - 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe', - 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra', - 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus', - 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g', - 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g', - 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g', - 'Samsung Galaxy Tab' => 'samsung-galaxy-tab', - 'Samsung Galaxy Tab A' => 'samsung-galaxy-tab-a', - 'Samsung Galaxy Tab A7' => 'samsung-galaxy-tab-a7', - 'Samsung Galaxy Tab S' => 'samsung-galaxy-tab-s', - 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4', - 'Samsung Galaxy Tab S5e' => 'samsung-galaxy-tab-s5e', - 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6', - 'Samsung Galaxy Watch' => 'samsung-galaxy-watch', - 'Samsung Galaxy Watch3' => 'samsung-galaxy-watch3', - 'Samsung Galaxy Watch Active2' => 'samsung-galaxy-watch-active-2', - 'Samsung Gear' => 'samsung-gear', - 'Samsung Gear S3' => 'gear-s3', - 'Samsung Gear VR' => 'samsung-gear-vr', - 'Samsung Headphones' => 'samsung-headphones', - 'Samsung Monitor' => 'samsung-monitor', - 'Samsung QLED TVs' => 'samsung-qled-tv', - 'Samsung Smartphone' => 'samsung-smartphone', - 'Samsung SSD' => 'samsung-ssd', - 'Samsung The Frame TV' => 'samsung-the-frame', - 'Samsung TV' => 'samsung-tv', - 'Samsung Washing Machine' => 'samsung-washing-machine', - 'Samsung Watch' => 'samsung-watch', - 'Sandals' => 'sandals', - 'Sander' => 'sander', - 'SanDisk' => 'sandisk', - 'SanDisk SSD' => 'sandisk-ssd', - 'Sand Pit' => 'sand-pit', - 'Sandwich Maker' => 'sandwich', - 'San Miguel' => 'san-miguel', - 'Santander' => 'santander', - 'Satchel' => 'satchel', - 'Sat Nav' => 'sat-nav', - 'Sauce' => 'sauce', - 'Saw' => 'saw', - 'Scalextric' => 'scalextric', - 'Scanner' => 'scanner', - 'School Bag' => 'school-bag', - 'School Supplies' => 'school', - 'School Uniform' => 'school-uniform', - 'Schwalbe' => 'schwalbe', - 'Scooby Doo' => 'scooby-doo', - 'Scooter' => 'scooter', - 'Scotch Whisky' => 'scotch', - 'Scrabble' => 'scrabble', - 'Screen Protector' => 'screen-protector', - 'Screenwash' => 'screenwash', - 'Screwdriver' => 'screwdriver', - 'Screws' => 'screws', - 'SD Cards' => 'sd-card', - 'SDHC' => 'sdhc', - 'SDXC' => 'sdxc', - 'Seagate' => 'seagate', - 'Sea Life' => 'sea-life', - 'Sea of Thieves' => 'sea-of-thieves', - 'Season Pass' => 'season-pass', - 'Seaworld' => 'seaworld', - 'Security Camera' => 'security-camera', - 'Seeds & Bulbs' => 'seeds-and-bulbs', - 'Sega' => 'sega', - 'SEGA Mega Drive Mini' => 'sega-mega-drive-mini', - 'Segway' => 'segway', - 'Seiko' => 'seiko', - 'Sekiro: Shadows Die Twice' => 'sekiro', - 'Sekonda' => 'sekonda', - 'Selfie Stick' => 'selfie-stick', - 'Sennheiser' => 'sennheiser', - 'Sennheiser Headphones' => 'sennheiser-headphones', - 'Sensodyne' => 'sensodyne', - 'Server' => 'server', - 'Services & Contracts' => 'services-contracts', - 'Services and Subscriptions' => 'service-contract', - 'Sewing' => 'sewing', - 'Sewing Machine' => 'sewing-machine', - 'Sex Toys' => 'sex-toys', - 'Shadow of the Tomb Raider' => 'shadow-of-the-tomb-raider', - 'Shampoo' => 'shampoo', - 'Shark' => 'shark', - 'Shark DuoClean' => 'shark-duoclean', - 'Shark Vacuum Cleaner' => 'shark-vacuum-cleaner', - 'Sharp' => 'sharp', - 'Sharpener' => 'sharpener', - 'Sharpie' => 'sharpie', - 'Shaver' => 'shaver', - 'Shaving & Beard Care' => 'shaving', - 'Shaving, Trimming, & Hair Removal' => 'hair-removal', - 'Shaving Foam' => 'shaving-foam', - 'Shears' => 'shears', - 'Sheba' => 'sheba', - 'Shed' => 'shed', - 'Shelter' => 'shelter', - 'Shelves' => 'shelves', - 'Shenmue I & II' => 'shenmue-one-and-two', - 'Shenmue III' => 'shenmue-3', - 'Shenmue Series' => 'shenmue-series', - 'Shimano' => 'shimano', - 'Shirt' => 'shirt', - 'Shoe Rack' => 'shoe-rack', - 'Shoes' => 'shoe', - 'Shopkins' => 'shopkins', - 'Shortbread' => 'shortbread', - 'Shorts' => 'shorts', - 'Short Trip' => 'break', - 'Shoulder Bag' => 'shoulder-bag', - 'Shovel' => 'shovel', - 'Shower Curtain' => 'shower-curtain', - 'Shower Enclosure' => 'shower-enclosure', - 'Shower Fittings' => 'shower', - 'Shower Gel' => 'shower-gel', - 'Shower Head' => 'shower-head', - 'Shredder' => 'shredder', - 'Side-by-Side-Fridge' => 'side-by-side-fridge', - 'Sideboard' => 'sideboard', - 'Sid Meier's Civilization VI' => 'civilization-vi', - 'Siemens' => 'siemens', - 'Siemens Washing Machine' => 'siemens-washing-machine', - 'Sigma' => 'sigma', - 'Silentnight' => 'silentnight', - 'Silvercrest' => 'silvercrest', - 'Silver Cross' => 'silver-cross', - 'Sim Free' => 'sim-free', - 'Sim Only' => 'sim-only', - 'Simplehuman' => 'simplehuman', - 'Simpsons' => 'simpsons', - 'Single Malt' => 'single-malt', - 'Sink' => 'sink', - 'Sistema' => 'sistema', - 'Skateboard' => 'skateboard', - 'Skating' => 'skating', - 'Skechers' => 'skechers', - 'Skiing' => 'ski', - 'Skin Care' => 'skincare', - 'Skittles' => 'skittles', - 'Skoda' => 'skoda', - 'Skullcandy' => 'skullcandy', - 'Sky' => 'sky', - 'Sky Cinema' => 'sky-cinema', - 'Skylanders' => 'skylanders', - 'Skylanders Battlecast' => 'skylanders-battlecast', - 'Skylanders Imaginators' => 'skylanders-imaginators', - 'Sleeping Bag' => 'sleeping-bag', - 'Sleeping Dogs' => 'sleeping-dogs', - 'Sleepwear' => 'sleepwear', - 'Slide' => 'slide', - 'Slimming World' => 'slimming-world', - 'Slippers' => 'slippers', - 'Slow Cooker' => 'slow-cooker', - 'Smart Clock' => 'clock', - 'Smart Doorbells' => 'smart-doorbell', - 'Smart Home' => 'smart-home', - 'Smart Light' => 'smart-light', - 'Smart Lock' => 'smart-lock', - 'Smartphone Accessories' => 'smartphone-accessories', - 'Smartphone Case' => 'smartphone-case', - 'Smartphone under £200' => 'smartphone-under-200-pounds', - 'Smartphone under £400' => 'smartphone-under-400-pounds', - 'Smart Plugs' => 'smart-plugs', - 'Smart Speaker' => 'smart-speaker', - 'Smart Tech & Gadgets' => 'smart-tech', - 'Smart Thermostat' => 'thermostat', - 'SmartThings' => 'smartthings', - 'Smart TV' => 'smart-tv', - 'Smart Watch' => 'smartwatch', - 'Smeg' => 'smeg', - 'Smirnoff' => 'smirnoff', - 'Smoke Alarm' => 'smoke-alarm', - 'Smoothie' => 'smoothie', - 'Smoothie Maker' => 'smoothie-maker', - 'Snacks' => 'snacks', - 'Sneakers' => 'sneakers', - 'SNES Nintendo Classic Mini' => 'snes-nintendo-classic', - 'Snickers' => 'snickers', - 'Sniper Elite' => 'sniper-elite', - 'Snowboard' => 'snowboard', - 'Snow Boots' => 'snow-boots', - 'Soap' => 'soap', - 'Soap and Glory' => 'soap-and-glory', - 'Socket Set' => 'socket-set', - 'Socks' => 'socks', - 'SodaStream' => 'soda-stream', - 'Sofa' => 'sofa', - 'Soft Drinks' => 'soft-drinks', - 'Soft Toy' => 'soft-toy', - 'Software' => 'software', - 'Software & Apps' => 'software-apps', - 'Solar Lights' => 'solar-lights', - 'Soldering Iron' => 'soldering', - 'Sonic' => 'sonic', - 'Sonos' => 'sonos', - 'Sonos Beam' => 'sonos-beam', - 'Sonos Move' => 'sonos-move', - 'Sonos One' => 'sonos-one', - 'Sonos PLAY:1' => 'sonos-play-1', - 'Sonos PLAY:3' => 'sonos-play-3', - 'Sonos PLAY:5' => 'sonos-play-5', - 'Sonos PLAYBAR' => 'sonos-playbar', - 'Sonos PLAYBASE' => 'sonos-playbase', - 'Sony' => 'sony', - 'Sony Camera' => 'sony-camera', - 'Sony Headphones' => 'sony-headphones', - 'Sony Pulse 3D Wireless Headset' => 'pulse-3d-wireless-headsets', - 'Sony TV' => 'sony-tv', - 'Sony WF-1000XM3' => 'sony-wf1000xm3', - 'Sony WH-1000XM3' => 'sony-wh-1000xm3', - 'Sony WH-1000XM4' => 'sony-wh1000xm4', - 'Sony Xperia' => 'xperia', - 'Sony Xperia 5' => 'sony-xperia-5', - 'Sony Xperia 10' => 'sony-xperia-10', - 'Sony Xperia Xa' => 'sony-xperia-xa', - 'Sony Xperia Z3' => 'xperia-z3', - 'Sony Xperia Z5' => 'xperia-z5', - 'Soulcalibur' => 'soulcalibur', - 'Soundbar' => 'soundbar', - 'Soundbase' => 'soundbase', - 'Sound Card' => 'sound-card', - 'Soundmagic' => 'soundmagic', - 'Soup' => 'soup', - 'Soup Maker' => 'soup-maker', - 'Sous-Vide' => 'sousvide', - 'Southern Comfort' => 'southern-comfort', - 'South Park' => 'south-park', - 'Spa' => 'spa', - 'Spade' => 'spade', - 'Spanner' => 'spanner', - 'Speaker' => 'speakers', - 'Specialized' => 'specialized', - 'Speedo' => 'speedo', - 'Sphero' => 'sphero', - 'Spice Rack' => 'spice-rack', - 'Spiderman' => 'spiderman', - 'Spiralizer' => 'spiralizer', - 'Spirit & Liqueur' => 'spirits', - 'Spirit Level' => 'spirit-level', - 'Splatoon' => 'splatoon', - 'Sports & Outdoors' => 'sports-fitness', - 'Sports Events' => 'sports-events', - 'Sports Nutrition' => 'nutrition', - 'Spreads' => 'spreads', - 'Spyro Reignited Trilogy' => 'spyro-reignited-trilogy', - 'SSD' => 'ssd', - 'SSHD' => 'sshd', - 'Staedtler' => 'staedtler', - 'Stair Gate' => 'stair-gate', - 'Stanley' => 'stanley', - 'Stapler' => 'stapler', - 'Starbucks' => 'starbucks', - 'Starlink: Battle for Atlas' => 'starlink-battle-for-atlas', - 'Star Ocean' => 'star-ocean', - 'Star Trek' => 'star-trek', - 'Star Wars' => 'star-wars', - 'Star Wars: Battlefront' => 'star-wars-battlefront', - 'Star Wars: Battlefront II' => 'star-wars-battlefront-2', - 'Star Wars: Squadrons' => 'star-wars-squadrons', - 'Star Wars Jedi: Fallen Order' => 'star-wars-jedi-fallen-order', - 'Stationery' => 'stationery', - 'Stationery & Office Supplies' => 'stationery-office-supplies', - 'Staycation' => 'staycation', - 'Steak' => 'steak', - 'Steam Cleaner' => 'steam-cleaner', - 'Steam Controller' => 'steam-controller', - 'Steamer' => 'steamer', - 'Steam Gaming' => 'steam', - 'Steam Iron' => 'steam-iron', - 'Steam Link' => 'steam-link', - 'Steam Mop' => 'steam-mop', - 'SteelSeries' => 'steelseries', - 'Steering Wheel' => 'steering-wheel', - 'Stella' => 'stella', - 'Stool' => 'stool', - 'Storage Box' => 'storage-box', - 'Stormtrooper' => 'stormtrooper', - 'Straightener' => 'straightener', - 'Streaming' => 'streaming', - 'Street Fighter' => 'street-fighter', - 'Street Fighter V' => 'street-fighter-v', - 'Streetwear' => 'streetwear', - 'Strimmer' => 'strimmer', - 'Strongbow' => 'strongbow', - 'Student Discount' => 'student-discount', - 'Subwoofer' => 'subwoofer', - 'Suitcase' => 'suitcase', - 'Suncare' => 'suncare', - 'Sun Cream' => 'sun-cream', - 'Sunglasses' => 'sunglasses', - 'Superdry' => 'superdry', - 'Superfast Broadband' => 'superfast-broadband', - 'Superking' => 'superking', - 'Super Mario' => 'mario', - 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars', - 'Super Mario 3D World' => 'super-mario-3d-world', - 'Super Mario Maker 2' => 'super-mario-maker-2', - 'Super Mario Odyssey' => 'super-mario-odyssey', - 'Super Mario Party' => 'mario-party', - 'Supermarket' => 'supermarket', - 'Super Smash Bros.' => 'super-smash-bros', - 'Surf' => 'surf', - 'Swarovski' => 'swarovski', - 'Sweets' => 'sweets', - 'Swimming' => 'swimming', - 'Swimming Goggles' => 'goggles', - 'Swimwear' => 'swimwear', - 'Swing' => 'swing', - 'Swingball' => 'swingball', - 'Syberia' => 'syberia', - 'Sylvanian' => 'sylvanian', - 'Synology' => 'synology', - 'T-Mobile' => 't-mobile', - 'T-Shirt' => 't-shirt', - 'Table Lamp' => 'table-lamp', - 'Tablet' => 'tablet', - 'Tablet Accessories' => 'tablet-accessories', - 'Table Tennis' => 'table-tennis', - 'Tableware' => 'tableware', - 'Tacx' => 'tacx', - 'Tado' => 'tado', - 'Tag Heuer' => 'tag-heuer', - 'Takeaway and Food Delivery' => 'takeaway', - 'Tales of Vesperia: Definitive Edition' => 'tales-of-vesperia-definitive-edition', - 'Talisker' => 'talisker', - 'Talkmobile' => 'talkmobile', - 'Tamron' => 'tamron', - 'Tangle Teezer' => 'tangle-teezer', - 'Tank Top' => 'tank-top', - 'Tannoy' => 'tannoy', - 'Tanqueray' => 'tanqueray', - 'Tape' => 'tape', - 'Tassimo' => 'tassimo', - 'Tassimo Coffee Machine' => 'tassimo-coffee-machine', - 'tastecard' => 'tastecard', - 'Taxi' => 'taxi', - 'Tea' => 'tea', - 'Team Sonic Racing' => 'team-sonic-racing', - 'Team Sports' => 'team-sports', - 'Teapot' => 'teapot', - 'Technika' => 'technika', - 'Techwood' => 'techwood', - 'Ted Baker' => 'ted-baker', - 'Teddy Bear' => 'teddy-bear', - 'Teenage Mutant Ninja Turtles' => 'turtle', - 'Teeth Care' => 'teeth-care', - 'Teeth Whitening' => 'teeth-whitening', - 'Tefal' => 'tefal', - 'Tefal Actifry' => 'actifry', - 'Tefal Pan' => 'tefal-pan', - 'Tekken' => 'tekken', - 'Tekken 7' => 'tekken-7', - 'Telegraph' => 'telegraph', - 'Telescope' => 'telescope', - 'Telltale' => 'telltale', - 'Tennis' => 'tennis', - 'Tent' => 'tent', - 'Tequila' => 'tequila', - 'Tesco Clothing' => 'tesco-clothing', - 'Tesla' => 'tesla', - 'Tetris' => 'tetris', - 'Tetris 99' => 'tetris-99', - 'Theatre & Musical' => 'theatre', - 'The Beatles' => 'beatles', - 'The Big Bang Theory' => 'big-bang-theory', - 'The Crew' => 'the-crew', - 'The Dark Pictures: Anthology Man of Medan' => 'the-dark-pictures-anthology-man-of-medan', - 'The Elder Scrolls' => 'elder-scrolls', - 'The Elder Scrolls V: Skyrim' => 'skyrim', - 'The Evil Within' => 'the-evil-within', - 'The Evil Within 2' => 'the-evil-within-2', - 'The Last Guardian' => 'the-last-guardian', - 'The Last of Us' => 'the-last-of-us', - 'The Last of Us Part II' => 'the-last-of-us-part-2', - 'The Legend of Zelda' => 'zelda', - 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild', - 'The Legend of Zelda: Link's Awakening' => 'the-legend-of-zelda-links-awakening', - 'The Legend of Zelda: Skyward Sword HD' => 'the-legend-of-zelda-skyward-sword-hd', - 'Theme Park' => 'theme-park', - 'The North Face' => 'north-face', - 'The Outer Worlds' => 'the-outer-worlds', - 'Thermos Storage' => 'thermos', - 'The Sims' => 'sims', - 'The Sims 4' => 'the-sims-4', - 'The Sinking City' => 'the-sinking-city', - 'The Sun' => 'the-sun', - 'The Sunday Times' => 'sunday-times', - 'The Walking Dead' => 'walking-dead', - 'The Witcher' => 'witcher', - 'The Witcher 3' => 'the-witcher-3', - 'Thierry Mugler' => 'thierry-mugler', - 'Thomas Sabo' => 'thomas-sabo', - 'Thomas The Tank Engine' => 'thomas-the-tank', - 'Thornton's' => 'thorntons', - 'Thorpe Park' => 'thorpe-park', - 'Throw' => 'throw', - 'Thrustmaster' => 'thrustmaster', - 'Thule' => 'thule', - 'Tickets & Shows' => 'tickets-shows', - 'Tie' => 'tie', - 'Tights' => 'tights', - 'TIGI' => 'tigi', - 'Tilda' => 'tilda', - 'Tile' => 'tile', - 'Timberland' => 'timberland', - 'Timex' => 'timex', - 'Tissot' => 'tissot', - 'Tissues' => 'tissues', - 'Titanfall' => 'titanfall', - 'Titanfall 2' => 'titanfall-2', - 'Toaster' => 'toaster', - 'Toblerone' => 'toblerone', - 'Toddler Bed' => 'toddler-bed', - 'Toilet Brush' => 'brush', - 'Toilet Cleaner' => 'toilet', - 'Toilet Roll' => 'toilet-roll', - 'Toilet Seat' => 'toilet-seat', - 'Tokyo Laundry' => 'tokyo-laundry', - 'Tomb Raider' => 'tomb-raider', - 'Tom Clancy's' => 'tom-clancy', - 'Tom Clancy's: Ghost Recon' => 'ghost-recon', - 'Tom Clancy's Ghost Recon: Wildlands' => 'ghost-recon-wildlands', - 'Tom Clancy's Ghost Recon Breakpoint' => 'tom-clancys-ghost-recon-breakpoint', - 'Tom Clancy's The Division' => 'tom-clancy-the-division', - 'Tom Clancy's The Division 2' => 'tom-clancy-the-division-2', - 'Tom Ford' => 'tom-ford', - 'Tommee Tippee' => 'tommee-tippee', - 'Tommy Hilfiger' => 'tommy-hilfiger', - 'Toms' => 'toms', - 'TomTom' => 'tomtom', - 'Tonic Water' => 'tonic-water', - 'Tony Hawk's Pro Skater 1 + 2' => 'tony-hawks-pro-skater-1-2', - 'Tools' => 'tool', - 'Toothbrush' => 'toothbrush', - 'Toothpaste' => 'toothpaste', - 'Torch' => 'torch', - 'Torque Wrench' => 'torque-wrench', - 'Toshiba' => 'toshiba', - 'Toshiba Laptop' => 'toshiba-laptop', - 'Toshiba TV' => 'toshiba-tv', - 'Total War' => 'total-war', - 'Tottenham Hotspur F. C.' => 'tottenham', - 'Towel' => 'towel', - 'Toy Box' => 'toy-box', - 'Toy Cars' => 'toy-cars', - 'Toy Castle' => 'castle', - 'Toy Digger' => 'digger', - 'Toy Helicopter' => 'helicopter', - 'Toy Kitchen' => 'toy-kitchen', - 'Toy Mask' => 'mask', - 'Toyota' => 'toyota', - 'Toys' => 'toy', - 'Toy Story' => 'toy-story', - 'Toy Tractor' => 'tractor', - 'Toy Train' => 'train', - 'TP-Link' => 'tp-link', - 'TP-Link Archer' => 'archer', - 'TP-Link Router' => 'tp-link-router', - 'Tracksuit' => 'tracksuit', - 'Trainers' => 'trainers', - 'Trains & Buses' => 'train-and-bus-ticket', - 'Train Ticket' => 'train-ticket', - 'Trampoline' => 'trampoline', - 'Transcend' => 'transcend', - 'Transformers' => 'transformers', - 'Travel' => 'travel', - 'Travel App' => 'travel-app', - 'Travel Insurance' => 'travel-insurance', - 'Travelodge' => 'travelodge', - 'Travel System' => 'travel-system', - 'Treadmill' => 'treadmill', - 'TRESemmé' => 'tresemme', - 'Trespass' => 'trespass', - 'Triathlon' => 'triathlon', - 'Trike' => 'trike', - 'Trine 4' => 'trine-4', - 'Tripod' => 'tripod', - 'Tripp' => 'tripp', - 'Triton Shower' => 'triton', - 'Trolley Bag' => 'trolley', - 'Tropico 5' => 'tropico-5', - 'Tropico 6' => 'tropico-6', - 'Tropico Series' => 'tropico-deals', - 'Trousers' => 'trousers', - 'True Wireless Earbuds' => 'wireless-earphones', - 'Trunki' => 'trunki', - 'Tumble Dryer' => 'tumble-dryer', - 'Tuna' => 'tuna', - 'Turbo Trainer' => 'turbo-trainer', - 'Turntable' => 'turntable', - 'Turtle Beach' => 'turtle-beach', - 'TV' => 'tv', - 'TV & Video' => 'tv-video', - 'TV Accessories' => 'tv-accessories', - 'TV Mount' => 'tv-mount', - 'TV Series' => 'tv-series', - 'TV Stand' => 'tv-stand', - 'Twinings' => 'twinings', - 'Twin Peaks' => 'twin-peaks', - 'Twix' => 'twix', - 'Typhoo' => 'typhoo', - 'Tyres' => 'tyres', - 'Ubisoft' => 'ubisoft', - 'UE BOOM' => 'ue-boom', - 'UE Boom 2' => 'ue-boom-2', - 'UEFA' => 'uefa', - 'UE Megablast' => 'ue-megablast', - 'UE Megaboom' => 'ue-megaboom', - 'UGG' => 'ugg', - 'Ulefone' => 'ulefone', - 'Ultrabook' => 'ultrabook', - 'Ultrawide Monitor' => 'ultrawide', - 'Umbrella' => 'umbrella', - 'UMI' => 'umidigi', - 'Uncharted' => 'uncharted', - 'Uncharted 4: A Thief's End' => 'uncharted-4', - 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy', - 'Under Armour' => 'under-armour', - 'Underwear' => 'underwear', - 'Unicorn' => 'unicorn', - 'UNiDAYS' => 'unidays', - 'Universal Remote' => 'universal-remote', - 'Uno' => 'uno', - 'Uplay' => 'uplay', - 'Urban Decay' => 'urban-decay', - 'Urban Sports' => 'urban-sports', - 'USB Cable' => 'usb-cable', - 'USB Hub' => 'usb-hub', - 'USB Memory Stick' => 'flash-drive', - 'USB Type C' => 'usb-type-c', - 'USN' => 'usn', - 'Vacuum Cleaner' => 'vacuum-cleaners', - 'Vacuum Flask' => 'flask', - 'Valkyria Chronicles' => 'valkyria-chronicles', - 'Valkyria Chronicles 4' => 'valkyria-chronicles-4', - 'Vango' => 'vango', - 'Vanish' => 'vanish', - 'Vans' => 'vans', - 'Vans Old Skool' => 'vans-old-skool', - 'Vans Shoes' => 'vans-shoes', - 'Vase' => 'vase', - 'Vaseline' => 'vaseline', - 'Vauxhall' => 'vauxhall', - 'VAX' => 'vax', - 'Vax Blade' => 'vax-blade', - 'Vax Vacuum Cleaner' => 'vax-vacuum', - 'Veet' => 'veet', - 'Vega 7' => 'vega-7', - 'Vegetables' => 'vegetables', - 'Vegetarian' => 'vegetarian', - 'Vehicles' => 'vehicles', - 'Velvet Comfort' => 'velvet', - 'Vera Wang' => 'vera-wang', - 'Verbatim' => 'verbatim', - 'Versace' => 'versace', - 'Vibrator' => 'vibrator', - 'Victorinox' => 'victorinox', - 'Video Games' => 'videogame', - 'Video Streaming' => 'video-streaming', - 'Viktor & Rolf Spicebomb' => 'spicebomb', - 'Vileda' => 'vileda', - 'Villeroy & Boch' => 'villeroy-boch', - 'Viners' => 'viners', - 'Vinyl' => 'vinyl', - 'Virgin' => 'virgin', - 'Vitamins & Supplements' => 'vitamins', - 'Vitamix' => 'vitamix', - 'Vodafone' => 'vodafone', - 'Vodka' => 'vodka', - 'Volvo' => 'volvo', - 'VPN' => 'vpn', - 'VR Headset' => 'vr-headset', - 'VTech' => 'vtech', - 'VTech Toot Toot' => 'toot-toot', - 'Vue' => 'vue', - 'VW' => 'vw', - 'Wacom' => 'wacom', - 'Waffle Maker' => 'waffle-maker', - 'Wahl' => 'wahl', - 'Walkers' => 'walkers', - 'Walking Boots' => 'walking-boots', - 'Wall Art' => 'wall-art', - 'Wallet' => 'wallet', - 'Wallpaper' => 'wallpaper', - 'Wardrobe' => 'wardrobe', - 'Warhammer' => 'warhammer', - 'Washbag' => 'washbag', - 'Washer Dryer' => 'washer-dryer', - 'Washing Machine' => 'washing-machine', - 'Washing Powder' => 'washing-powder', - 'Watch' => 'watch', - 'Watch Dogs' => 'watch-dogs', - 'Watch Dogs 2' => 'watch-dogs-2', - 'Watch Dogs: Legion' => 'watch-dogs-legion', - 'Water Bottle' => 'water-bottle', - 'Water Butt' => 'water-butt', - 'Water Dispenser' => 'water-dispenser', - 'Water Filter' => 'water-filter', - 'Water Gun' => 'water-gun', - 'Waterproof Camera' => 'waterproof-camera', - 'Waterproof Jacket' => 'waterproof-jacket', - 'Watersports' => 'watersport', - 'Water Toys' => 'water-toys', - 'Wayfarer' => 'wayfarer', - 'WD40' => 'wd40', - 'Wearable' => 'wearable', - 'Weather Station' => 'weather-station', - 'Webcam' => 'webcam', - 'Weber' => 'weber', - 'Web Hosting' => 'web-hosting', - 'Wedding' => 'wedding', - 'Weed Killer' => 'weed', - 'Weekend Break' => 'weekend-break', - 'Weetabix' => 'weetabix', - 'Weightlifting' => 'weightlifting', - 'Weight Watchers' => 'weight-watchers', - 'Wellies' => 'wellies', - 'Wellness and Health' => 'wellness-and-health', - 'Wenger' => 'wenger', - 'Western Digital' => 'western-digital', - 'Wetsuit' => 'wetsuit', - 'Wheelbarrow' => 'wheelbarrow', - 'Wheelchair' => 'wheelchair', - 'Whey' => 'whey', - 'Whiskas' => 'whiskas', - 'Whisky' => 'whisky', - 'Whole Home Mesh Wi-Fi System' => 'whole-home-mesh-wifi-system', - 'Wi-Fi Camera' => 'wifi-camera', - 'Wi-Fi Dongle' => 'dongle', - 'Wi-Fi Extender' => 'wifi-extender', - 'Wii' => 'wii', - 'Wii Game' => 'wii-games', - 'Wii U Game' => 'wii-u-game', - 'Wii U Pro Controller' => 'wii-u-pro-controller', - 'Wild Turkey' => 'wild-turkey', - 'Wileyfox' => 'wileyfox', - 'Wilkinson Sword Hydro 5' => 'hydro-5', - 'Wilkinson Sword Razor' => 'wilkinson-sword', - 'Wimbledon Tennis' => 'wimbledon', - 'Window Cleaner' => 'window-cleaner', - 'Windows' => 'windows', - 'Windows 8' => 'windows-8', - 'Windows 10' => 'windows-10', - 'Wine' => 'wine', - 'Wine Advent Calendar' => 'wine-advent-calendar', - 'Wine Glasses' => 'wine-glasses', - 'Winter Jacket' => 'winter-jacket', - 'Wiper Blades' => 'wiper-blades', - 'Wireless Adapter' => 'wireless-adapter', - 'Wireless Charger' => 'wireless-charger', - 'Wireless Controller' => 'wireless-controller', - 'Wireless Headphones' => 'wireless-headphones', - 'Wireless Headset' => 'wireless-headset', - 'Wireless Keyboard' => 'wireless-keyboard', - 'Wireless Mouse' => 'wireless-mouse', - 'Wok' => 'wok', - 'Wolfenstein' => 'wolfenstein', - 'Wolfenstein 2: The New Colossus' => 'wolfenstein-2', - 'Women's Boots' => 'womens-boots', - 'Women's Fragrance' => 'womens-fragrance', - 'Women's Shoes' => 'womens-shoes', - 'Workbench' => 'workbench', - 'World of Warcraft' => 'world-of-warcraft', - 'World War Z' => 'world-war-z', - 'WORX' => 'worx', - 'Wreckfest' => 'wreckfest', - 'Wuaki' => 'wuaki', - 'WWE 2K' => 'wwe', - 'Xbox' => 'xbox', - 'Xbox 360 Game' => 'xbox-360-game', - 'Xbox Accessories' => 'xbox-accessories', - 'Xbox Controller' => 'xbox-controller', - 'Xbox Game Pass' => 'xbox-game-pass', - 'Xbox Gift Card' => 'xbox-gift-card', - 'Xbox Headset' => 'xbox-headset', - 'Xbox Kinect' => 'kinect', - 'Xbox Live' => 'xbox-live', - 'Xbox One Controller' => 'xbox-one-controller', - 'Xbox One Elite Controller' => 'xbox-one-elite-controller', - 'Xbox One Games' => 'xbox-one-games', - 'Xbox One S' => 'xbox-one-s', - 'Xbox One X' => 'xbox-one-x', - 'Xbox Series S' => 'xbox-series-s', - 'Xbox Series X' => 'xbox-series-x', - 'Xbox Series X Controller' => 'xbox-series-x-controller', - 'Xbox Series X Games' => 'xbox-series-x-game', - 'Xbox Wireless Adapter' => 'xbox-wireless-adapter', - 'Xbox Wireless Headset' => 'xbox-wireless-headset', - 'XCOM' => 'xcom', - 'XCOM 2' => 'xcom-2', - 'Xenoblade Chronicles' => 'xenoblade-chronicles', - 'XFX' => 'xfx', - 'Xiaomi' => 'xiaomi', - 'Xiaomi AirDots' => 'xiaomi-airdots', - 'Xiaomi Black Shark' => 'xiaomi-black-shark', - 'Xiaomi Black Shark 2' => 'xiaomi-black-shark-2', - 'Xiaomi Headphones' => 'xiaomi-headphones', - 'Xiaomi Laptop' => 'xiaomi-laptop', - 'Xiaomi Mi 5' => 'xiaomi-mi-5', - 'Xiaomi Mi 6' => 'xiaomi-mi-6', - 'Xiaomi Mi 8' => 'xiaomi-mi-8', - 'Xiaomi Mi 8 Lite' => 'xiaomi-mi-8-lite', - 'Xiaomi Mi 8 Pro' => 'xiaomi-mi-8-pro', - 'Xiaomi Mi 9' => 'xiaomi-mi-9', - 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite', - 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se', - 'Xiaomi Mi 9T' => 'xiaomi-mi-9t', - 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro', - 'Xiaomi Mi 10' => 'xiaomi-mi-10', - 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite', - 'Xiaomi Mi 10T' => 'xiaomi-mi-10t', - 'Xiaomi Mi 10T Lite' => 'xiaomi-mi-10t-lite', - 'Xiaomi Mi 10T Pro' => 'xiaomi-mi-10t-pro', - 'Xiaomi Mi 11' => 'xiaomi-mi-11', - 'Xiaomi Mi 11 Lite 4G' => 'xiaomi-mi-11-lite-4g', - 'Xiaomi Mi 11 Lite 5G' => 'xiaomi-mi-11-lite-5g', - 'Xiaomi Mi 11 Pro' => 'xiaomi-mi-11-pro', - 'Xiaomi Mi 11 Ultra' => 'xiaomi-mi-11-ultra', - 'Xiaomi Mi 11i' => 'xiaomi-mi-11i', - 'Xiaomi Mi A1' => 'xiaomi-mi-a1', - 'Xiaomi Mi A2' => 'mi-a2', - 'Xiaomi Mi A3' => 'xiaomi-mi-a3', - 'Xiaomi Mi Band' => 'xiaomi-mi-band', - 'Xiaomi Mi Band 3' => 'xiaomi-mi-band-3', - 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4', - 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5', - 'Xiaomi Mi Box' => 'xiaomi-mi-box', - 'Xiaomi Mi Max 3' => 'xiaomi-mi-max3', - 'Xiaomi Mi Mix' => 'xiaomi-mi-mix', - 'Xiaomi Mi Mix 2' => 'xiaomi-mi-mix-2', - 'Xiaomi Mi Mix 2S' => 'xiaomi-mi-mix-2s', - 'Xiaomi Mi Mix 3' => 'xiaomi-mi-mix-3', - 'Xiaomi Mi Note' => 'xiaomi-mi-note', - 'Xiaomi Mi Note 10' => 'mi-note-10', - 'Xiaomi Mi Pad 4' => 'xiaomi-mi-pad-4', - 'Xiaomi Pocophone F1' => 'pocophone-f1', - 'Xiaomi Redmi' => 'redmi', - 'Xiaomi Redmi 4' => 'xiaomi-redmi-4', - 'Xiaomi Redmi 5' => 'redmi-5', - 'Xiaomi Redmi 6' => 'redmi-6', - 'Xiaomi Redmi 8' => 'redmi-8', - 'Xiaomi Redmi Note 4' => 'note-4', - 'Xiaomi Redmi Note 5' => 'redmi-note-5', - 'Xiaomi Redmi Note 6' => 'redmi-note-6', - 'Xiaomi Redmi Note 6 Pro' => 'xiaomi-redmi-note-6-pro', - 'Xiaomi Redmi Note 7' => 'redmi-note-7', - 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8', - 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro', - 'Xiaomi Redmi Note 8T' => 'redmi-note-8t', - 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9', - 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro', - 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s', - 'Xiaomi Roborock' => 'xiaomi-roborock', - 'Xiaomi Roborock S5' => 'xiaomi-roborock-s5', - 'Xiaomi Scooter' => 'xiaomi-scooter', - 'Xiaomi Smartphones' => 'xiaomi-smartphone', - 'Xiaomi Tablets' => 'xiaomi-tablet', - 'Yakuza' => 'yakuza', - 'Yale' => 'yale', - 'Yale Smart Lock' => 'yale-smart-lock', - 'Yamaha' => 'yamaha', - 'Yankee Candle' => 'yankee-candle', - 'Yeelight' => 'xiaomi-yeelight', - 'Yoga' => 'yoga', - 'Yoghurt' => 'yoghurt', - 'Yoshi' => 'yoshi', - 'Yoshi's Crafted World' => 'yoshis-crafted-world', - 'YouView' => 'youview', - 'Yves Saint Laurent' => 'yves-saint-laurent', - 'Zanussi' => 'zanussi', - 'Zippo' => 'zippo', - 'Zizzi' => 'zizzi', - 'Zoo' => 'zoo', - 'Zoostorm' => 'zoostorm', - 'ZOTAC' => 'zotac', - 'ZTE' => 'zte', - 'ZTE Smartphone' => 'zte-smartphone', - 'ZyXEL' => 'zyxel', - ) - ), - 'order' => array( - 'name' => 'Order by', - 'type' => 'list', - 'title' => 'Sort order of deals', - 'values' => array( - 'From the most to the least hot deal' => '-hot', - 'From the most recent deal to the oldest' => '-new', - ) - ) - ), - 'Discussion Monitoring' => array( - 'url' => array( - 'name' => 'Discussion URL', - 'type' => 'text', - 'required' => true, - 'title' => 'Discussion URL to monitor. Ex: https://www.hotukdeals.com/discussions/title-123', - 'exampleValue' => 'https://www.hotukdeals.com/discussions/the-hukd-lego-thread-3599357', - ), - 'only_with_url' => array( - 'name' => 'Exclude comments without URL', - 'type' => 'checkbox', - 'title' => 'Exclude comments that does not contains URL in the feed', - 'defaultValue' => false, - ) - ) + ]; - ); - - public $lang = array( - 'bridge-uri' => SELF::URI, - 'bridge-name' => SELF::NAME, - 'context-keyword' => 'Search by keyword(s))', - 'context-group' => 'Deals per group', - 'context-talk' => 'Discussion Monitoring', - 'uri-group' => 'tag/', - 'request-error' => 'Could not request HotUKDeals', - 'thread-error' => 'Unable to determine the thread ID. Check the URL you entered', - 'no-results' => 'Ooops, looks like we could', - 'relative-date-indicator' => array( - 'ago', - ), - 'price' => 'Price', - 'shipping' => 'Shipping', - 'origin' => 'Origin', - 'discount' => 'Discount', - 'title-keyword' => 'Search', - 'title-group' => 'Group', - 'title-talk' => 'Discussion Monitoring', - 'local-months' => array( - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Occ', - 'Nov', - 'Dec', - 'st', - 'nd', - 'rd', - 'th' - ), - 'local-time-relative' => array( - 'Posted ', - 'm', - 'h,', - 'day', - 'days', - 'month', - 'year', - 'and ' - ), - 'date-prefixes' => array( - 'Found ', - 'Refreshed ', - 'Made hot ' - ), - 'relative-date-alt-prefixes' => array( - 'Made hot ', - 'Refreshed ', - 'Last updated ' - ), - 'relative-date-ignore-suffix' => array( - '/by.*$/' - ), - 'localdeal' => array( - 'Local', - 'Expires' - ) - ); - + public $lang = [ + 'bridge-uri' => self::URI, + 'bridge-name' => self::NAME, + 'context-keyword' => 'Search by keyword(s))', + 'context-group' => 'Deals per group', + 'context-talk' => 'Discussion Monitoring', + 'uri-group' => 'tag/', + 'request-error' => 'Could not request HotUKDeals', + 'thread-error' => 'Unable to determine the thread ID. Check the URL you entered', + 'no-results' => 'Ooops, looks like we could', + 'relative-date-indicator' => [ + 'ago', + ], + 'price' => 'Price', + 'shipping' => 'Shipping', + 'origin' => 'Origin', + 'discount' => 'Discount', + 'title-keyword' => 'Search', + 'title-group' => 'Group', + 'title-talk' => 'Discussion Monitoring', + 'local-months' => [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Occ', + 'Nov', + 'Dec', + 'st', + 'nd', + 'rd', + 'th' + ], + 'local-time-relative' => [ + 'Posted ', + 'm', + 'h,', + 'day', + 'days', + 'month', + 'year', + 'and ' + ], + 'date-prefixes' => [ + 'Found ', + 'Refreshed ', + 'Made hot ' + ], + 'relative-date-alt-prefixes' => [ + 'Made hot ', + 'Refreshed ', + 'Last updated ' + ], + 'relative-date-ignore-suffix' => [ + '/by.*$/' + ], + 'localdeal' => [ + 'Local', + 'Expires' + ] + ]; } diff --git a/bridges/IGNBridge.php b/bridges/IGNBridge.php index ef5088f2..d00b6a18 100644 --- a/bridges/IGNBridge.php +++ b/bridges/IGNBridge.php @@ -1,65 +1,68 @@ <?php -class IGNBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'IGN Bridge'; - const URI = 'https://www.ign.com/'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'RSS Feed For IGN'; +class IGNBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'IGN Bridge'; + const URI = 'https://www.ign.com/'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'RSS Feed For IGN'; - public function collectData(){ - $this->collectExpandableDatas('http://feeds.ign.com/ign/all', 15); - } + public function collectData() + { + $this->collectExpandableDatas('http://feeds.ign.com/ign/all', 15); + } - // IGNs feed is both hidden and incomplete. This bridge tries to fix this. + // IGNs feed is both hidden and incomplete. This bridge tries to fix this. - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); - // List of BS elements - $uselessElements = array( - '.wiki-page-tools', - '.feedback-container', - '.paging-container', - '.dropdown-wrapper', - '.mw-editsection', - '.jsx-4115608983', - '.jsx-4213937408', - '.commerce-container', - '.widget-container', - '.newsletter-signup-button' - ); + // List of BS elements + $uselessElements = [ + '.wiki-page-tools', + '.feedback-container', + '.paging-container', + '.dropdown-wrapper', + '.mw-editsection', + '.jsx-4115608983', + '.jsx-4213937408', + '.commerce-container', + '.widget-container', + '.newsletter-signup-button' + ]; - // Remove useless elements - foreach($uselessElements as $uslElement) { - foreach($articlePage->find($uslElement) as $jsWidget) { - $jsWidget->remove(); - } - } + // Remove useless elements + foreach ($uselessElements as $uslElement) { + foreach ($articlePage->find($uslElement) as $jsWidget) { + $jsWidget->remove(); + } + } - /* - * NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism - * for handling them seperately as it just ignores the DOM querys which it does not find. - * (and their scraping) - */ + /* + * NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism + * for handling them seperately as it just ignores the DOM querys which it does not find. + * (and their scraping) + */ - // For Articles - $article = $articlePage->find('section.article-page', 0); - // add in verdicts in articles, reviews etc - foreach($articlePage->find('div.article-section') as $element) { - $article = $article . $element; - } + // For Articles + $article = $articlePage->find('section.article-page', 0); + // add in verdicts in articles, reviews etc + foreach ($articlePage->find('div.article-section') as $element) { + $article = $article . $element; + } - // For Wikis and HowTos - foreach($articlePage->find('.wiki-page') as $wikiContents) { - $article = $article . $wikiContents; - } + // For Wikis and HowTos + foreach ($articlePage->find('.wiki-page') as $wikiContents) { + $article = $article . $wikiContents; + } - // Add content to feed - $item['content'] = $article; - return $item; - } + // Add content to feed + $item['content'] = $article; + return $item; + } } diff --git a/bridges/IKWYDBridge.php b/bridges/IKWYDBridge.php index eed7dc38..344efffe 100644 --- a/bridges/IKWYDBridge.php +++ b/bridges/IKWYDBridge.php @@ -1,114 +1,128 @@ <?php -class IKWYDBridge extends BridgeAbstract { - const MAINTAINER = 'DevonHess'; - const NAME = 'I Know What You Download'; - const URI = 'https://iknowwhatyoudownload.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns torrent downloads and distributions for an IP address'; - const PARAMETERS = array( - array( - 'ip' => array( - 'name' => 'IP Address', - 'exampleValue' => '8.8.8.8', - 'required' => true - ), - 'update' => array( - 'name' => 'Update last seen', - 'type' => 'checkbox', - 'title' => 'Update timestamp every time "last seen" changes' - ) - ) - ); - private $name; - private $uri; - public function detectParameters($url) { - $params = array(); +class IKWYDBridge extends BridgeAbstract +{ + const MAINTAINER = 'DevonHess'; + const NAME = 'I Know What You Download'; + const URI = 'https://iknowwhatyoudownload.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns torrent downloads and distributions for an IP address'; + const PARAMETERS = [ + [ + 'ip' => [ + 'name' => 'IP Address', + 'exampleValue' => '8.8.8.8', + 'required' => true + ], + 'update' => [ + 'name' => 'Update last seen', + 'type' => 'checkbox', + 'title' => 'Update timestamp every time "last seen" changes' + ] + ] + ]; + private $name; + private $uri; - $regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/'; - $regex .= '(?:en|ru)\/peer\/\?ip=(\d+\.\d+\.\d+\.\d+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['ip'] = urldecode($matches[2]); - return $params; - } + public function detectParameters($url) + { + $params = []; - $regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/'; - $regex .= '(?:(?:en|ru)\/peer\/)?/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['ip'] = $_SERVER['REMOTE_ADDR']; - return $params; - } + $regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/'; + $regex .= '(?:en|ru)\/peer\/\?ip=(\d+\.\d+\.\d+\.\d+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['ip'] = urldecode($matches[2]); + return $params; + } - return null; - } + $regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/'; + $regex .= '(?:(?:en|ru)\/peer\/)?/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['ip'] = $_SERVER['REMOTE_ADDR']; + return $params; + } - public function getName() { - if($this->name) { - return $this->name; - } else { - return self::NAME; - } - } + return null; + } - public function getURI() { - if($this->uri) { - return $this->uri; - } else { - return self::URI; - } - } + public function getName() + { + if ($this->name) { + return $this->name; + } else { + return self::NAME; + } + } - public function collectData() { - $ip = $this->getInput('ip'); - $root = self::URI . 'en/peer/?ip=' . $ip; - $html = getSimpleHTMLDOM($root); + public function getURI() + { + if ($this->uri) { + return $this->uri; + } else { + return self::URI; + } + } - $this->name = 'IKWYD: ' . $ip; - $this->uri = $root; + public function collectData() + { + $ip = $this->getInput('ip'); + $root = self::URI . 'en/peer/?ip=' . $ip; + $html = getSimpleHTMLDOM($root); - foreach($html->find('.table > tbody > tr') as $download) { - $download = defaultLinkTo($download, self::URI); - $firstSeen = $download->find('.date-column', - 0)->innertext; - $lastSeen = $download->find('.date-column', - 1)->innertext; - $category = $download->find('.category-column', - 0)->innertext; - $torlink = $download->find('.name-column > div > a', - 0); - $tortitle = strip_tags($torlink); - $size = $download->find('td', 4)->innertext; - $title = $tortitle; - $author = $ip; + $this->name = 'IKWYD: ' . $ip; + $this->uri = $root; - if($this->getInput('update')) { - $timestamp = strtotime($lastSeen); - } else { - $timestamp = strtotime($firstSeen); - } + foreach ($html->find('.table > tbody > tr') as $download) { + $download = defaultLinkTo($download, self::URI); + $firstSeen = $download->find( + '.date-column', + 0 + )->innertext; + $lastSeen = $download->find( + '.date-column', + 1 + )->innertext; + $category = $download->find( + '.category-column', + 0 + )->innertext; + $torlink = $download->find( + '.name-column > div > a', + 0 + ); + $tortitle = strip_tags($torlink); + $size = $download->find('td', 4)->innertext; + $title = $tortitle; + $author = $ip; - $uri = $torlink->href; + if ($this->getInput('update')) { + $timestamp = strtotime($lastSeen); + } else { + $timestamp = strtotime($firstSeen); + } - $content = 'IP address: <a href="' . $root . '">'; - $content .= $ip . '</a><br>'; - $content .= 'First seen: ' . $firstSeen . '<br>'; - $content .= ($this->getInput('update') ? 'Last seen: ' . - $lastSeen . '<br>' : ''); - $content .= ($category ? 'Category: ' . - $category . '<br>' : ''); - $content .= 'Title: ' . $torlink . '<br>'; - $content .= 'Size: ' . $size; + $uri = $torlink->href; - $item = array(); - $item['uri'] = $uri; - $item['title'] = $title; - $item['author'] = $author; - $item['timestamp'] = $timestamp; - $item['content'] = $content; - if($category) { - $item['categories'] = array($category); - } - $this->items[] = $item; - } - } + $content = 'IP address: <a href="' . $root . '">'; + $content .= $ip . '</a><br>'; + $content .= 'First seen: ' . $firstSeen . '<br>'; + $content .= ($this->getInput('update') ? 'Last seen: ' . + $lastSeen . '<br>' : ''); + $content .= ($category ? 'Category: ' . + $category . '<br>' : ''); + $content .= 'Title: ' . $torlink . '<br>'; + $content .= 'Size: ' . $size; + + $item = []; + $item['uri'] = $uri; + $item['title'] = $title; + $item['author'] = $author; + $item['timestamp'] = $timestamp; + $item['content'] = $content; + if ($category) { + $item['categories'] = [$category]; + } + $this->items[] = $item; + } + } } diff --git a/bridges/IPBBridge.php b/bridges/IPBBridge.php index af2ed390..d5db0111 100644 --- a/bridges/IPBBridge.php +++ b/bridges/IPBBridge.php @@ -1,309 +1,325 @@ <?php -class IPBBridge extends FeedExpander { - - const NAME = 'IPB Bridge'; - const URI = 'https://www.invisionpower.com'; - const DESCRIPTION = 'Returns feeds for forums powered by IPB'; - const MAINTAINER = 'logmanoriginal'; - const PARAMETERS = array( - array( - 'uri' => array( - 'name' => 'URI', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert forum, subforum or topic URI', - 'exampleValue' => 'https://invisioncommunity.com/forums/forum/499-feedback-and-ideas/' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Specifies the number of items to return on each request (-1: all)', - 'defaultValue' => 10 - ) - ) - ); - const CACHE_TIMEOUT = 3600; - - // Constants for internal use - const FORUM_TYPE_LIST_FILTER = '.cForumTopicTable'; - const FORUM_TYPE_TABLE_FILTER = '#forum_table'; - - const TOPIC_TYPE_ARTICLE = 'article'; - const TOPIC_TYPE_DIV = 'div.post_block'; - - public function getURI(){ - return $this->getInput('uri') ?: parent::getURI(); - } - - public function collectData(){ - // The URI cannot be the mainpage (or anything related) - switch(parse_url($this->getInput('uri'), PHP_URL_PATH)) { - case null: - case '/index.php': - returnClientError('Provided URI is invalid!'); - break; - default: - break; - } - - // Sanitize the URI (because else it won't work) - $uri = rtrim($this->getInput('uri'), '/'); // No trailing slashes! - - // Forums might provide feeds, though that's optional *facepalm* - // Let's check if there is a valid feed available - $headers = get_headers($uri . '.xml'); - - if($headers[0] === 'HTTP/1.1 200 OK') { // Heureka! It's a valid feed! - return $this->collectExpandableDatas($uri . '.xml'); - } - - // No valid feed, so do it the hard way - $html = getSimpleHTMLDOM($uri); - - $limit = $this->getInput('limit'); - - // Determine if this is a topic or a forum - switch(true) { - case $this->isTopic($html): - $this->collectTopic($html, $limit); - break; - case $this->isForum($html): - $this->collectForum($html); - break; - default: - returnClientError('Unknown type!'); - break; - } - } - - private function isForum($html){ - return !is_null($html->find('div[data-controller*=forums.front.forum.forumPage]', 0)) - || !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)); - } - - private function isTopic($html){ - return !is_null($html->find('div[data-controller*=core.front.core.commentFeed]', 0)) - || !is_null($html->find(static::TOPIC_TYPE_DIV, 0)); - } - - private function collectForum($html){ - // There are multiple forum designs in use (depends on version?) - // 1 - Uses an ordered list (based on https://invisioncommunity.com/forums) - // 2 - Uses a table (based on https://onehallyu.com) - - switch(true) { - case !is_null($html->find(static::FORUM_TYPE_LIST_FILTER, 0)): - $this->collectForumList($html); - break; - case !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)): - $this->collectForumTable($html); - break; - default: - returnClientError('Unknown forum format!'); - break; - } - } - - private function collectForumList($html){ - foreach($html->find(static::FORUM_TYPE_LIST_FILTER, 0)->children() as $row) { - // Columns: Title, Statistics, Last modified - $item = array(); - - $item['uri'] = $row->find('a', 0)->href; - $item['title'] = $row->find('a', 0)->title; - $item['author'] = $row->find('a', 1)->innertext; - $item['timestamp'] = strtotime($row->find('time', 0)->getAttribute('datetime')); - - $this->items[] = $item; - } - } - - private function collectForumTable($html){ - foreach($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)->children() as $row) { - // Columns: Icon, Content, Preview, Statistics, Last modified - $item = array(); - - // Skip header row - if(!is_null($row->find('th', 0))) continue; - - $item['uri'] = $row->find('a', 0)->href; - $item['title'] = $row->find('.title', 0)->plaintext; - $item['timestamp'] = strtotime($row->find('[itemprop=dateCreated]', 0)->plaintext); - - $this->items[] = $item; - } - } - - private function collectTopic($html, $limit){ - // There are multiple topic designs in use (depends on version?) - // 1 - Uses articles (based on https://invisioncommunity.com/forums) - // 2 - Uses divs (based on https://onehallyu.com) - - switch(true) { - case !is_null($html->find(static::TOPIC_TYPE_ARTICLE, 0)): - $this->collectTopicHistory($html, $limit, 'collectTopicArticle'); - break; - case !is_null($html->find(static::TOPIC_TYPE_DIV, 0)): - $this->collectTopicHistory($html, $limit, 'collectTopicDiv'); - break; - default: - returnClientError('Unknown topic format!'); - break; - } - } - - private function collectTopicHistory($html, $limit, $callback){ - // Make sure the callback is valid! - if(!method_exists($this, $callback)) - returnServerError('Unknown function (\'' . $callback . '\')!'); - - $next = null; // Holds the URI of the next page - - while(true) { - $next = $this->$callback($html, is_null($next)); - - if(is_null($next) || ($limit > 0 && count($this->items) >= $limit)) { - break; - } - - $html = getSimpleHTMLDOMCached($next); - } - - // We might have more items than specified, remove excess - $this->items = array_slice($this->items, 0, $limit); - } - - private function collectTopicArticle($html, $firstrun = true){ - $title = $html->find('h1.ipsType_pageTitle', 0)->plaintext; - - // Are we on last page? - if($firstrun && !is_null($html->find('.ipsPagination', 0))) { - $last = $html->find('.ipsPagination_last a', 0)->{'data-page'}; - $active = $html->find('.ipsPagination_active a', 0)->{'data-page'}; - - if($active !== $last) { - // Load last page into memory (cached) - $html = getSimpleHTMLDOMCached($html->find('.ipsPagination_last a', 0)->href); - } - } - - foreach(array_reverse($html->find(static::TOPIC_TYPE_ARTICLE)) as $article) { - $item = array(); - - $item['uri'] = $article->find('time', 0)->parent()->href; - $item['author'] = $article->find('aside a', 0)->plaintext; - $item['title'] = $item['author'] . ' - ' . $title; - $item['timestamp'] = strtotime($article->find('time', 0)->getAttribute('datetime')); - - $content = $article->find('[data-role=commentContent]', 0); - $content = $this->scaleImages($content); - $item['content'] = $this->fixContent($content); - $item['enclosures'] = $this->findImages($article->find('[data-role=commentContent]', 0)) ?: null; - - $this->items[] = $item; - } - - // Return whatever page comes next (previous, as we add in inverse order) - // Do we have a previous page? (inactive means no) - if(!is_null($html->find('li[class=ipsPagination_prev ipsPagination_inactive]', 0))) { - return null; // No, or no more - } elseif(!is_null($html->find('li[class=ipsPagination_prev]', 0))) { - return $html->find('.ipsPagination_prev a', 0)->href; - } - - return null; - } - - private function collectTopicDiv($html, $firstrun = true){ - $title = $html->find('h1.ipsType_pagetitle', 0)->plaintext; - - // Are we on last page? - if($firstrun && !is_null($html->find('.pagination', 0))) { - - $active = $html->find('li[class=page active]', 0)->plaintext; - - // There are two ways the 'last' page is displayed: - // - With a distict 'last' button (only if there are enough pages) - // - With a button for each page (use last button) - if(!is_null($html->find('li.last', 0))) { - $last = $html->find('li.last a', 0); - } else { - $last = $html->find('li[class=page] a', -1); - } - - if($active !== $last->plaintext) { - // Load last page into memory (cached) - $html = getSimpleHTMLDOMCached($last->href); - } - } - - foreach(array_reverse($html->find(static::TOPIC_TYPE_DIV)) as $article) { - $item = array(); - - $item['uri'] = $article->find('a[rel=bookmark]', 0)->href; - $item['author'] = $article->find('.author', 0)->plaintext; - $item['title'] = $item['author'] . ' - ' . $title; - $item['timestamp'] = strtotime($article->find('.published', 0)->getAttribute('title')); - - $content = $article->find('[itemprop=commentText]', 0); - $content = $this->scaleImages($content); - $item['content'] = $this->fixContent($content); - - $item['enclosures'] = $this->findImages($article->find('.post_body', 0)) ?: null; - $this->items[] = $item; - } - - // Return whatever page comes next (previous, as we add in inverse order) - // Do we have a previous page? - if(!is_null($html->find('li.prev', 0))) { - return $html->find('li.prev a', 0)->href; - } - - return null; - } - - /** Returns all images from the provide HTML DOM */ - private function findImages($html){ - $images = array(); - - foreach($html->find('img') as $img) { - $images[] = $img->src; - } - - return $images; - } - - /** Sets the maximum width and height for all images */ - private function scaleImages($html, $width = 400, $height = 400){ - foreach($html->find('img') as $img) { - $img->style = "max-width: {$width}px; max-height: {$height}px;"; - } - - return $html; - } - - /** Removes all unnecessary tags and adds formatting */ - private function fixContent($html){ - - // Restore quote highlighting - foreach($html->find('blockquote') as $quote) { - $quote->style = <<<EOD +class IPBBridge extends FeedExpander +{ + const NAME = 'IPB Bridge'; + const URI = 'https://www.invisionpower.com'; + const DESCRIPTION = 'Returns feeds for forums powered by IPB'; + const MAINTAINER = 'logmanoriginal'; + const PARAMETERS = [ + [ + 'uri' => [ + 'name' => 'URI', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert forum, subforum or topic URI', + 'exampleValue' => 'https://invisioncommunity.com/forums/forum/499-feedback-and-ideas/' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Specifies the number of items to return on each request (-1: all)', + 'defaultValue' => 10 + ] + ] + ]; + const CACHE_TIMEOUT = 3600; + + // Constants for internal use + const FORUM_TYPE_LIST_FILTER = '.cForumTopicTable'; + const FORUM_TYPE_TABLE_FILTER = '#forum_table'; + + const TOPIC_TYPE_ARTICLE = 'article'; + const TOPIC_TYPE_DIV = 'div.post_block'; + + public function getURI() + { + return $this->getInput('uri') ?: parent::getURI(); + } + + public function collectData() + { + // The URI cannot be the mainpage (or anything related) + switch (parse_url($this->getInput('uri'), PHP_URL_PATH)) { + case null: + case '/index.php': + returnClientError('Provided URI is invalid!'); + break; + default: + break; + } + + // Sanitize the URI (because else it won't work) + $uri = rtrim($this->getInput('uri'), '/'); // No trailing slashes! + + // Forums might provide feeds, though that's optional *facepalm* + // Let's check if there is a valid feed available + $headers = get_headers($uri . '.xml'); + + if ($headers[0] === 'HTTP/1.1 200 OK') { // Heureka! It's a valid feed! + return $this->collectExpandableDatas($uri . '.xml'); + } + + // No valid feed, so do it the hard way + $html = getSimpleHTMLDOM($uri); + + $limit = $this->getInput('limit'); + + // Determine if this is a topic or a forum + switch (true) { + case $this->isTopic($html): + $this->collectTopic($html, $limit); + break; + case $this->isForum($html): + $this->collectForum($html); + break; + default: + returnClientError('Unknown type!'); + break; + } + } + + private function isForum($html) + { + return !is_null($html->find('div[data-controller*=forums.front.forum.forumPage]', 0)) + || !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)); + } + + private function isTopic($html) + { + return !is_null($html->find('div[data-controller*=core.front.core.commentFeed]', 0)) + || !is_null($html->find(static::TOPIC_TYPE_DIV, 0)); + } + + private function collectForum($html) + { + // There are multiple forum designs in use (depends on version?) + // 1 - Uses an ordered list (based on https://invisioncommunity.com/forums) + // 2 - Uses a table (based on https://onehallyu.com) + + switch (true) { + case !is_null($html->find(static::FORUM_TYPE_LIST_FILTER, 0)): + $this->collectForumList($html); + break; + case !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)): + $this->collectForumTable($html); + break; + default: + returnClientError('Unknown forum format!'); + break; + } + } + + private function collectForumList($html) + { + foreach ($html->find(static::FORUM_TYPE_LIST_FILTER, 0)->children() as $row) { + // Columns: Title, Statistics, Last modified + $item = []; + + $item['uri'] = $row->find('a', 0)->href; + $item['title'] = $row->find('a', 0)->title; + $item['author'] = $row->find('a', 1)->innertext; + $item['timestamp'] = strtotime($row->find('time', 0)->getAttribute('datetime')); + + $this->items[] = $item; + } + } + + private function collectForumTable($html) + { + foreach ($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)->children() as $row) { + // Columns: Icon, Content, Preview, Statistics, Last modified + $item = []; + + // Skip header row + if (!is_null($row->find('th', 0))) { + continue; + } + + $item['uri'] = $row->find('a', 0)->href; + $item['title'] = $row->find('.title', 0)->plaintext; + $item['timestamp'] = strtotime($row->find('[itemprop=dateCreated]', 0)->plaintext); + + $this->items[] = $item; + } + } + + private function collectTopic($html, $limit) + { + // There are multiple topic designs in use (depends on version?) + // 1 - Uses articles (based on https://invisioncommunity.com/forums) + // 2 - Uses divs (based on https://onehallyu.com) + + switch (true) { + case !is_null($html->find(static::TOPIC_TYPE_ARTICLE, 0)): + $this->collectTopicHistory($html, $limit, 'collectTopicArticle'); + break; + case !is_null($html->find(static::TOPIC_TYPE_DIV, 0)): + $this->collectTopicHistory($html, $limit, 'collectTopicDiv'); + break; + default: + returnClientError('Unknown topic format!'); + break; + } + } + + private function collectTopicHistory($html, $limit, $callback) + { + // Make sure the callback is valid! + if (!method_exists($this, $callback)) { + returnServerError('Unknown function (\'' . $callback . '\')!'); + } + + $next = null; // Holds the URI of the next page + + while (true) { + $next = $this->$callback($html, is_null($next)); + + if (is_null($next) || ($limit > 0 && count($this->items) >= $limit)) { + break; + } + + $html = getSimpleHTMLDOMCached($next); + } + + // We might have more items than specified, remove excess + $this->items = array_slice($this->items, 0, $limit); + } + + private function collectTopicArticle($html, $firstrun = true) + { + $title = $html->find('h1.ipsType_pageTitle', 0)->plaintext; + + // Are we on last page? + if ($firstrun && !is_null($html->find('.ipsPagination', 0))) { + $last = $html->find('.ipsPagination_last a', 0)->{'data-page'}; + $active = $html->find('.ipsPagination_active a', 0)->{'data-page'}; + + if ($active !== $last) { + // Load last page into memory (cached) + $html = getSimpleHTMLDOMCached($html->find('.ipsPagination_last a', 0)->href); + } + } + + foreach (array_reverse($html->find(static::TOPIC_TYPE_ARTICLE)) as $article) { + $item = []; + + $item['uri'] = $article->find('time', 0)->parent()->href; + $item['author'] = $article->find('aside a', 0)->plaintext; + $item['title'] = $item['author'] . ' - ' . $title; + $item['timestamp'] = strtotime($article->find('time', 0)->getAttribute('datetime')); + + $content = $article->find('[data-role=commentContent]', 0); + $content = $this->scaleImages($content); + $item['content'] = $this->fixContent($content); + $item['enclosures'] = $this->findImages($article->find('[data-role=commentContent]', 0)) ?: null; + + $this->items[] = $item; + } + + // Return whatever page comes next (previous, as we add in inverse order) + // Do we have a previous page? (inactive means no) + if (!is_null($html->find('li[class=ipsPagination_prev ipsPagination_inactive]', 0))) { + return null; // No, or no more + } elseif (!is_null($html->find('li[class=ipsPagination_prev]', 0))) { + return $html->find('.ipsPagination_prev a', 0)->href; + } + + return null; + } + + private function collectTopicDiv($html, $firstrun = true) + { + $title = $html->find('h1.ipsType_pagetitle', 0)->plaintext; + + // Are we on last page? + if ($firstrun && !is_null($html->find('.pagination', 0))) { + $active = $html->find('li[class=page active]', 0)->plaintext; + + // There are two ways the 'last' page is displayed: + // - With a distict 'last' button (only if there are enough pages) + // - With a button for each page (use last button) + if (!is_null($html->find('li.last', 0))) { + $last = $html->find('li.last a', 0); + } else { + $last = $html->find('li[class=page] a', -1); + } + + if ($active !== $last->plaintext) { + // Load last page into memory (cached) + $html = getSimpleHTMLDOMCached($last->href); + } + } + + foreach (array_reverse($html->find(static::TOPIC_TYPE_DIV)) as $article) { + $item = []; + + $item['uri'] = $article->find('a[rel=bookmark]', 0)->href; + $item['author'] = $article->find('.author', 0)->plaintext; + $item['title'] = $item['author'] . ' - ' . $title; + $item['timestamp'] = strtotime($article->find('.published', 0)->getAttribute('title')); + + $content = $article->find('[itemprop=commentText]', 0); + $content = $this->scaleImages($content); + $item['content'] = $this->fixContent($content); + + $item['enclosures'] = $this->findImages($article->find('.post_body', 0)) ?: null; + + $this->items[] = $item; + } + + // Return whatever page comes next (previous, as we add in inverse order) + // Do we have a previous page? + if (!is_null($html->find('li.prev', 0))) { + return $html->find('li.prev a', 0)->href; + } + + return null; + } + + /** Returns all images from the provide HTML DOM */ + private function findImages($html) + { + $images = []; + + foreach ($html->find('img') as $img) { + $images[] = $img->src; + } + + return $images; + } + + /** Sets the maximum width and height for all images */ + private function scaleImages($html, $width = 400, $height = 400) + { + foreach ($html->find('img') as $img) { + $img->style = "max-width: {$width}px; max-height: {$height}px;"; + } + + return $html; + } + + /** Removes all unnecessary tags and adds formatting */ + private function fixContent($html) + { + // Restore quote highlighting + foreach ($html->find('blockquote') as $quote) { + $quote->style = <<<EOD padding: 0px 15px; border-width: 1px 1px 1px 2px; border-style: solid; border-color: #ededed #e8e8e8 #dbdbdb #666666; background: #fbfbfb; EOD; - } + } - // Remove unnecessary tags - $content = strip_tags( - $html->innertext, - '<p><a><img><ol><ul><li><table><tr><th><td><strong><blockquote><br><hr><h>' - ); + // Remove unnecessary tags + $content = strip_tags( + $html->innertext, + '<p><a><img><ol><ul><li><table><tr><th><td><strong><blockquote><br><hr><h>' + ); - return $content; - } + return $content; + } } diff --git a/bridges/IdenticaBridge.php b/bridges/IdenticaBridge.php index a9f47d10..6029eea2 100644 --- a/bridges/IdenticaBridge.php +++ b/bridges/IdenticaBridge.php @@ -1,52 +1,56 @@ <?php -class IdenticaBridge extends BridgeAbstract { - - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Identica Bridge'; - const URI = 'https://identi.ca/'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Returns user timelines'; - - const PARAMETERS = array( array( - 'u' => array( - 'name' => 'username', - 'exampleValue' => 'jxself', - 'required' => true - ) - )); - - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); - - foreach($html->find('li.major') as $dent) { - $item = array(); - - // get dent link - $item['uri'] = html_entity_decode($dent->find('a', 0)->href); - - // extract dent timestamp - $item['timestamp'] = strtotime($dent->find('abbr.easydate', 0)->plaintext); - - // extract dent text - $item['content'] = trim($dent->find('div.activity-content', 0)->innertext); - $item['title'] = $this->getInput('u') . ' | ' . $item['content']; - $this->items[] = $item; - } - } - - public function getName(){ - if(!is_null($this->getInput('u'))) { - return $this->getInput('u') . ' - Identica Bridge'; - } - - return parent::getName(); - } - - public function getURI(){ - if(!is_null($this->getInput('u'))) { - return self::URI . urlencode($this->getInput('u')); - } - - return parent::getURI(); - } + +class IdenticaBridge extends BridgeAbstract +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Identica Bridge'; + const URI = 'https://identi.ca/'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Returns user timelines'; + + const PARAMETERS = [ [ + 'u' => [ + 'name' => 'username', + 'exampleValue' => 'jxself', + 'required' => true + ] + ]]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + foreach ($html->find('li.major') as $dent) { + $item = []; + + // get dent link + $item['uri'] = html_entity_decode($dent->find('a', 0)->href); + + // extract dent timestamp + $item['timestamp'] = strtotime($dent->find('abbr.easydate', 0)->plaintext); + + // extract dent text + $item['content'] = trim($dent->find('div.activity-content', 0)->innertext); + $item['title'] = $this->getInput('u') . ' | ' . $item['content']; + $this->items[] = $item; + } + } + + public function getName() + { + if (!is_null($this->getInput('u'))) { + return $this->getInput('u') . ' - Identica Bridge'; + } + + return parent::getName(); + } + + public function getURI() + { + if (!is_null($this->getInput('u'))) { + return self::URI . urlencode($this->getInput('u')); + } + + return parent::getURI(); + } } diff --git a/bridges/IndeedBridge.php b/bridges/IndeedBridge.php index d86430a0..080d232d 100644 --- a/bridges/IndeedBridge.php +++ b/bridges/IndeedBridge.php @@ -1,220 +1,227 @@ <?php -class IndeedBridge extends BridgeAbstract { - - const NAME = 'Indeed'; - const URI = 'https://www.indeed.com/'; - const DESCRIPTION = 'Returns reviews and comments for a company of your choice'; - const MAINTAINER = 'logmanoriginal'; - const CACHE_TIMEOUT = 14400; // 4 hours - - const PARAMETERS = array( - array( - 'c' => array( - 'name' => 'Company', - 'type' => 'text', - 'required' => true, - 'title' => 'Company name', - 'exampleValue' => 'GitHub', - ) - ), - 'global' => array( - 'language' => array( - 'name' => 'Language Code', - 'type' => 'list', - 'title' => 'Choose your language code', - 'defaultValue' => 'en-US', - 'values' => array( - 'es-AR' => 'es-AR', - 'de-AT' => 'de-AT', - 'en-AU' => 'en-AU', - 'nl-BE' => 'nl-BE', - 'fr-BE' => 'fr-BE', - 'pt-BR' => 'pt-BR', - 'en-CA' => 'en-CA', - 'fr-CA' => 'fr-CA', - 'de-CH' => 'de-CH', - 'fr-CH' => 'fr-CH', - 'es-CL' => 'es-CL', - 'zh-CN' => 'zh-CN', - 'es-CO' => 'es-CO', - 'de-DE' => 'de-DE', - 'es-ES' => 'es-ES', - 'fr-FR' => 'fr-FR', - 'en-GB' => 'en-GB', - 'en-HK' => 'en-HK', - 'en-IE' => 'en-IE', - 'en-IN' => 'en-IN', - 'it-IT' => 'it-IT', - 'ja-JP' => 'ja-JP', - 'ko-KR' => 'ko-KR', - 'es-MX' => 'es-MX', - 'nl-NL' => 'nl-NL', - 'pl-PL' => 'pl-PL', - 'en-SG' => 'en-SG', - 'en-US' => 'en-US', - 'en-ZA' => 'en-ZA', - 'en-AE' => 'en-AE', - 'da-DK' => 'da-DK', - 'in-ID' => 'in-ID', - 'en-MY' => 'en-MY', - 'es-PE' => 'es-PE', - 'en-PH' => 'en-PH', - 'en-PK' => 'en-PK', - 'ro-RO' => 'ro-RO', - 'ru-RU' => 'ru-RU', - 'tr-TR' => 'tr-TR', - 'zh-TW' => 'zh-TW', - 'vi-VN' => 'vi-VN', - 'en-VN' => 'en-VN', - 'ar-EG' => 'ar-EG', - 'fr-MA' => 'fr-MA', - 'en-NG' => 'en-NG', - ) - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => true, - 'title' => 'Maximum number of items to return', - 'exampleValue' => 20, - ) - ) - ); - - const SITES = array( - 'es-AR' => 'https://ar.indeed.com/', - 'de-AT' => 'https://at.indeed.com/', - 'en-AU' => 'https://au.indeed.com/', - 'nl-BE' => 'https://be.indeed.com/', - 'fr-BE' => 'https://emplois.be.indeed.com/', - 'pt-BR' => 'https://www.indeed.com.br/', - 'en-CA' => 'https://ca.indeed.com/', - 'fr-CA' => 'https://emplois.ca.indeed.com/', - 'de-CH' => 'https://www.indeed.ch/', - 'fr-CH' => 'https://emplois.indeed.ch/', - 'es-CL' => 'https://www.indeed.cl/', - 'zh-CN' => 'https://cn.indeed.com/', - 'es-CO' => 'https://co.indeed.com/', - 'de-DE' => 'https://de.indeed.com/', - 'es-ES' => 'https://www.indeed.es/', - 'fr-FR' => 'https://www.indeed.fr/', - 'en-GB' => 'https://www.indeed.co.uk/', - 'en-HK' => 'https://www.indeed.hk/', - 'en-IE' => 'https://ie.indeed.com/', - 'en-IN' => 'https://www.indeed.co.in/', - 'it-IT' => 'https://it.indeed.com/', - 'ja-JP' => 'https://jp.indeed.com/', - 'ko-KR' => 'https://kr.indeed.com/', - 'es-MX' => 'https://www.indeed.com.mx/', - 'nl-NL' => 'https://www.indeed.nl/', - 'pl-PL' => 'https://pl.indeed.com/', - 'en-SG' => 'https://www.indeed.com.sg/', - 'en-US' => 'https://www.indeed.com/', - 'en-ZA' => 'https://www.indeed.co.za/', - 'en-AE' => 'https://www.indeed.ae/', - 'da-DK' => 'https://dk.indeed.com/', - 'in-ID' => 'https://id.indeed.com/', - 'en-MY' => 'https://www.indeed.com.my/', - 'es-PE' => 'https://www.indeed.com.pe/', - 'en-PH' => 'https://www.indeed.com.ph/', - 'en-PK' => 'https://www.indeed.com.pk/', - 'ro-RO' => 'https://ro.indeed.com/', - 'ru-RU' => 'https://ru.indeed.com/', - 'tr-TR' => 'https://tr.indeed.com/', - 'zh-TW' => 'https://tw.indeed.com/', - 'vi-VN' => 'https://vn.indeed.com/', - 'en-VN' => 'https://jobs.vn.indeed.com/', - 'ar-EG' => 'https://eg.indeed.com/', - 'fr-MA' => 'https://ma.indeed.com/', - 'en-NG' => 'https://ng.indeed.com/', - ); - - private $title; - - public function collectData() { - - $url = $this->getURI(); - $limit = $this->getInput('limit') ?: 20; - - do { - - $html = getSimpleHTMLDOM($url); - - $html = defaultLinkTo($html, $url); - - $this->title = $html->find('h1', 0)->innertext; - - foreach($html->find('.cmp-ReviewsList div[itemprop="review"]') as $review) { - $item = array(); - - $title = $review->find('h2[data-testid="title"]', 0)->innertext; - $rating = $review->find('meta[itemprop="ratingValue"]', 0)->getAttribute('content'); - $comment = $review->find('span[itemprop="reviewBody"]', 0)->innertext; - - $item['uri'] = $review->find('a[data-tn-element="individualReviewLink"]', 0)->href; - $item['title'] = "$title | ($rating)"; - $item['author'] = $review->find('span > meta[itemprop="name"]', 0)->getAttribute('content'); - $item['content'] = $comment; - - $this->items[] = $item; - - if(count($this->items) >= $limit) { - break; - } - } - } while(count($this->items) < $limit); - } - - public function getURI() { - if($this->getInput('language') - && $this->getInput('c')) { - return self::SITES[$this->getInput('language')] - . 'cmp/' - . urlencode($this->getInput('c')) - . '/reviews'; - } - - return parent::getURI(); - } - - public function getName() { - return $this->title ?: parent::getName(); - } - - public function detectParameters($url) { - /** - * Expected: https://<...>.indeed.<...>/cmp/<company>[/reviews][/...] - * - * Note that most users will be redirected to their localized version - * of the page, which adds the language code to the host. For example, - * "en.indeed.com" or "www.indeed.fr" (see link[rel="alternate"]). At - * least each of the sites have ".indeed." in the name. - */ - - if(filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false - || stristr($url, '.indeed.') === false) { - return null; - } - - $url_components = parse_url($url); - $path_segments = array_values(array_filter(explode('/', $url_components['path']))); - - if(count($path_segments) < 2 || $path_segments[0] !== 'cmp') { - return null; - } - - $language = array_search('https://' . $url_components['host'] . '/', self::SITES); - if($language === false) { - return null; - } - - $limit = self::PARAMETERS['global']['limit']['defaultValue'] ?: 20; - $company = $path_segments[1]; - - return array( - 'c' => $company, - 'language' => $language, - 'limit' => $limit, - ); - } + +class IndeedBridge extends BridgeAbstract +{ + const NAME = 'Indeed'; + const URI = 'https://www.indeed.com/'; + const DESCRIPTION = 'Returns reviews and comments for a company of your choice'; + const MAINTAINER = 'logmanoriginal'; + const CACHE_TIMEOUT = 14400; // 4 hours + + const PARAMETERS = [ + [ + 'c' => [ + 'name' => 'Company', + 'type' => 'text', + 'required' => true, + 'title' => 'Company name', + 'exampleValue' => 'GitHub', + ] + ], + 'global' => [ + 'language' => [ + 'name' => 'Language Code', + 'type' => 'list', + 'title' => 'Choose your language code', + 'defaultValue' => 'en-US', + 'values' => [ + 'es-AR' => 'es-AR', + 'de-AT' => 'de-AT', + 'en-AU' => 'en-AU', + 'nl-BE' => 'nl-BE', + 'fr-BE' => 'fr-BE', + 'pt-BR' => 'pt-BR', + 'en-CA' => 'en-CA', + 'fr-CA' => 'fr-CA', + 'de-CH' => 'de-CH', + 'fr-CH' => 'fr-CH', + 'es-CL' => 'es-CL', + 'zh-CN' => 'zh-CN', + 'es-CO' => 'es-CO', + 'de-DE' => 'de-DE', + 'es-ES' => 'es-ES', + 'fr-FR' => 'fr-FR', + 'en-GB' => 'en-GB', + 'en-HK' => 'en-HK', + 'en-IE' => 'en-IE', + 'en-IN' => 'en-IN', + 'it-IT' => 'it-IT', + 'ja-JP' => 'ja-JP', + 'ko-KR' => 'ko-KR', + 'es-MX' => 'es-MX', + 'nl-NL' => 'nl-NL', + 'pl-PL' => 'pl-PL', + 'en-SG' => 'en-SG', + 'en-US' => 'en-US', + 'en-ZA' => 'en-ZA', + 'en-AE' => 'en-AE', + 'da-DK' => 'da-DK', + 'in-ID' => 'in-ID', + 'en-MY' => 'en-MY', + 'es-PE' => 'es-PE', + 'en-PH' => 'en-PH', + 'en-PK' => 'en-PK', + 'ro-RO' => 'ro-RO', + 'ru-RU' => 'ru-RU', + 'tr-TR' => 'tr-TR', + 'zh-TW' => 'zh-TW', + 'vi-VN' => 'vi-VN', + 'en-VN' => 'en-VN', + 'ar-EG' => 'ar-EG', + 'fr-MA' => 'fr-MA', + 'en-NG' => 'en-NG', + ] + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => true, + 'title' => 'Maximum number of items to return', + 'exampleValue' => 20, + ] + ] + ]; + + const SITES = [ + 'es-AR' => 'https://ar.indeed.com/', + 'de-AT' => 'https://at.indeed.com/', + 'en-AU' => 'https://au.indeed.com/', + 'nl-BE' => 'https://be.indeed.com/', + 'fr-BE' => 'https://emplois.be.indeed.com/', + 'pt-BR' => 'https://www.indeed.com.br/', + 'en-CA' => 'https://ca.indeed.com/', + 'fr-CA' => 'https://emplois.ca.indeed.com/', + 'de-CH' => 'https://www.indeed.ch/', + 'fr-CH' => 'https://emplois.indeed.ch/', + 'es-CL' => 'https://www.indeed.cl/', + 'zh-CN' => 'https://cn.indeed.com/', + 'es-CO' => 'https://co.indeed.com/', + 'de-DE' => 'https://de.indeed.com/', + 'es-ES' => 'https://www.indeed.es/', + 'fr-FR' => 'https://www.indeed.fr/', + 'en-GB' => 'https://www.indeed.co.uk/', + 'en-HK' => 'https://www.indeed.hk/', + 'en-IE' => 'https://ie.indeed.com/', + 'en-IN' => 'https://www.indeed.co.in/', + 'it-IT' => 'https://it.indeed.com/', + 'ja-JP' => 'https://jp.indeed.com/', + 'ko-KR' => 'https://kr.indeed.com/', + 'es-MX' => 'https://www.indeed.com.mx/', + 'nl-NL' => 'https://www.indeed.nl/', + 'pl-PL' => 'https://pl.indeed.com/', + 'en-SG' => 'https://www.indeed.com.sg/', + 'en-US' => 'https://www.indeed.com/', + 'en-ZA' => 'https://www.indeed.co.za/', + 'en-AE' => 'https://www.indeed.ae/', + 'da-DK' => 'https://dk.indeed.com/', + 'in-ID' => 'https://id.indeed.com/', + 'en-MY' => 'https://www.indeed.com.my/', + 'es-PE' => 'https://www.indeed.com.pe/', + 'en-PH' => 'https://www.indeed.com.ph/', + 'en-PK' => 'https://www.indeed.com.pk/', + 'ro-RO' => 'https://ro.indeed.com/', + 'ru-RU' => 'https://ru.indeed.com/', + 'tr-TR' => 'https://tr.indeed.com/', + 'zh-TW' => 'https://tw.indeed.com/', + 'vi-VN' => 'https://vn.indeed.com/', + 'en-VN' => 'https://jobs.vn.indeed.com/', + 'ar-EG' => 'https://eg.indeed.com/', + 'fr-MA' => 'https://ma.indeed.com/', + 'en-NG' => 'https://ng.indeed.com/', + ]; + + private $title; + + public function collectData() + { + $url = $this->getURI(); + $limit = $this->getInput('limit') ?: 20; + + do { + $html = getSimpleHTMLDOM($url); + + $html = defaultLinkTo($html, $url); + + $this->title = $html->find('h1', 0)->innertext; + + foreach ($html->find('.cmp-ReviewsList div[itemprop="review"]') as $review) { + $item = []; + + $title = $review->find('h2[data-testid="title"]', 0)->innertext; + $rating = $review->find('meta[itemprop="ratingValue"]', 0)->getAttribute('content'); + $comment = $review->find('span[itemprop="reviewBody"]', 0)->innertext; + + $item['uri'] = $review->find('a[data-tn-element="individualReviewLink"]', 0)->href; + $item['title'] = "$title | ($rating)"; + $item['author'] = $review->find('span > meta[itemprop="name"]', 0)->getAttribute('content'); + $item['content'] = $comment; + + $this->items[] = $item; + + if (count($this->items) >= $limit) { + break; + } + } + } while (count($this->items) < $limit); + } + + public function getURI() + { + if ( + $this->getInput('language') + && $this->getInput('c') + ) { + return self::SITES[$this->getInput('language')] + . 'cmp/' + . urlencode($this->getInput('c')) + . '/reviews'; + } + + return parent::getURI(); + } + + public function getName() + { + return $this->title ?: parent::getName(); + } + + public function detectParameters($url) + { + /** + * Expected: https://<...>.indeed.<...>/cmp/<company>[/reviews][/...] + * + * Note that most users will be redirected to their localized version + * of the page, which adds the language code to the host. For example, + * "en.indeed.com" or "www.indeed.fr" (see link[rel="alternate"]). At + * least each of the sites have ".indeed." in the name. + */ + + if ( + filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) === false + || stristr($url, '.indeed.') === false + ) { + return null; + } + + $url_components = parse_url($url); + $path_segments = array_values(array_filter(explode('/', $url_components['path']))); + + if (count($path_segments) < 2 || $path_segments[0] !== 'cmp') { + return null; + } + + $language = array_search('https://' . $url_components['host'] . '/', self::SITES); + if ($language === false) { + return null; + } + + $limit = self::PARAMETERS['global']['limit']['defaultValue'] ?: 20; + $company = $path_segments[1]; + + return [ + 'c' => $company, + 'language' => $language, + 'limit' => $limit, + ]; + } } diff --git a/bridges/IndiegogoBridge.php b/bridges/IndiegogoBridge.php index 8e725209..c5e5033f 100644 --- a/bridges/IndiegogoBridge.php +++ b/bridges/IndiegogoBridge.php @@ -1,141 +1,142 @@ <?php -class IndiegogoBridge extends BridgeAbstract { - const NAME = 'Indiegogo'; - const URI = 'https://www.indiegogo.com'; - const DESCRIPTION = 'Fetch projects by category'; - const MAINTAINER = 'bockiii'; - const PARAMETERS = array( - 'global' => array( - 'timing' => array( - 'name' => 'Project Timing', - 'type' => 'list', - 'values' => array( - 'All' => 'all', - 'Launching Soon' => 'launching_soon', - 'Just Launched' => 'just_launched', - 'Ending Soon' => 'ending_soon', - ), - 'defaultValue' => 'Just Launched' - ), - ), - 'All Categories' => array(), - 'Tech & Innovation' => array( - 'tech' => array( - 'name' => 'Tech & Innovation', - 'type' => 'list', - 'values' => array( - 'All' => 'all', - 'Audio' => 'Audio', - 'Camera Gear' => 'Camera Gear', - 'Education' => 'Education', - 'Energy & Green Tech' => 'Energy & Green Tech', - 'Fashion & Wearables' => 'Fashion & Wearables', - 'Food & Beverages' => 'Food & Beverages', - 'Health & Fitness' => 'Health & Fitness', - 'Home' => 'Home', - 'Phones & Accessories' => 'Phones & Accessories', - 'Productivity' => 'Productivity', - 'Transportation' => 'Transportation', - 'Travel & Outdoors' => 'Travel & Outdoors', - ), - ), - ), - 'Creative Works' => array( - 'creative' => array( - 'name' => 'Creative Works', - 'type' => 'list', - 'values' => array( - 'All' => 'all', - 'Comics' => 'Comics', - 'Dance & Theater' => 'Dance & Theater', - 'Film' => 'Film', - 'Music' => 'Music', - 'Photography' => 'Photography', - 'Podcasts, Blogs & Vlogs' => 'Podcasts, Blogs & Vlogs', - 'Tabletop Games' => 'Tabletop Games', - 'Video Games' => 'Video Games', - 'Web Series & TV Shows' => 'Web Series & TV Shows', - 'Writing & Publishing' => 'Writing & Publishing', - ), - ), - ), - 'Community Projects' => array( - 'community' => array( - 'name' => 'Community Projects', - 'type' => 'list', - 'values' => array( - 'All' => 'all', - 'Culture' => 'Culture', - 'Environment' => 'Environment', - 'Human Rights' => 'Human Rights', - 'Local Businesses' => 'Local Businesses', - 'Wellness' => 'Wellness', - ), - ), - ), - ); +class IndiegogoBridge extends BridgeAbstract +{ + const NAME = 'Indiegogo'; + const URI = 'https://www.indiegogo.com'; + const DESCRIPTION = 'Fetch projects by category'; + const MAINTAINER = 'bockiii'; + const PARAMETERS = [ + 'global' => [ + 'timing' => [ + 'name' => 'Project Timing', + 'type' => 'list', + 'values' => [ + 'All' => 'all', + 'Launching Soon' => 'launching_soon', + 'Just Launched' => 'just_launched', + 'Ending Soon' => 'ending_soon', + ], + 'defaultValue' => 'Just Launched' + ], + ], + 'All Categories' => [], + 'Tech & Innovation' => [ + 'tech' => [ + 'name' => 'Tech & Innovation', + 'type' => 'list', + 'values' => [ + 'All' => 'all', + 'Audio' => 'Audio', + 'Camera Gear' => 'Camera Gear', + 'Education' => 'Education', + 'Energy & Green Tech' => 'Energy & Green Tech', + 'Fashion & Wearables' => 'Fashion & Wearables', + 'Food & Beverages' => 'Food & Beverages', + 'Health & Fitness' => 'Health & Fitness', + 'Home' => 'Home', + 'Phones & Accessories' => 'Phones & Accessories', + 'Productivity' => 'Productivity', + 'Transportation' => 'Transportation', + 'Travel & Outdoors' => 'Travel & Outdoors', + ], + ], + ], + 'Creative Works' => [ + 'creative' => [ + 'name' => 'Creative Works', + 'type' => 'list', + 'values' => [ + 'All' => 'all', + 'Comics' => 'Comics', + 'Dance & Theater' => 'Dance & Theater', + 'Film' => 'Film', + 'Music' => 'Music', + 'Photography' => 'Photography', + 'Podcasts, Blogs & Vlogs' => 'Podcasts, Blogs & Vlogs', + 'Tabletop Games' => 'Tabletop Games', + 'Video Games' => 'Video Games', + 'Web Series & TV Shows' => 'Web Series & TV Shows', + 'Writing & Publishing' => 'Writing & Publishing', + ], + ], + ], + 'Community Projects' => [ + 'community' => [ + 'name' => 'Community Projects', + 'type' => 'list', + 'values' => [ + 'All' => 'all', + 'Culture' => 'Culture', + 'Environment' => 'Environment', + 'Human Rights' => 'Human Rights', + 'Local Businesses' => 'Local Businesses', + 'Wellness' => 'Wellness', + ], + ], + ], + ]; - const CACHE_TIMEOUT = 21600; // 6 hours + const CACHE_TIMEOUT = 21600; // 6 hours - public function collectData() { + public function collectData() + { + $url = 'https://www.indiegogo.com/private_api/discover'; + $data_array = self::getCategories(); - $url = 'https://www.indiegogo.com/private_api/discover'; - $data_array = self::getCategories(); + $header = ['Content-type: application/json']; + $opts = [CURLOPT_POSTFIELDS => json_encode($data_array)]; + $html = getContents($url, $header, $opts); + $html_response = json_decode($html, true); - $header = array('Content-type: application/json'); - $opts = array(CURLOPT_POSTFIELDS => json_encode($data_array)); - $html = getContents($url, $header, $opts); - $html_response = json_decode($html, true); + foreach ($html_response['response']['discoverables'] as $obj) { + $this->items[] = [ + 'title' => $obj['title'], + 'uri' => $this->getURI() . $obj['clickthrough_url'], + 'timestamp' => $obj['open_date'], + 'enclosures' => $obj['image_url'], + 'content' => '<a href=' . $this->getURI() . $obj['clickthrough_url'] + . '><img src="' . $obj['image_url'] . '" /></a><br><br><b>' + . $obj['title'] . '</b><br><br><small>' + . $obj['tagline'] . '</small><br>', + ]; + } + } - foreach ($html_response['response']['discoverables'] as $obj) { - $this->items[] = array( - 'title' => $obj['title'], - 'uri' => $this->getURI() . $obj['clickthrough_url'], - 'timestamp' => $obj['open_date'], - 'enclosures' => $obj['image_url'], - 'content' => '<a href=' . $this->getURI() . $obj['clickthrough_url'] - . '><img src="' . $obj['image_url'] . '" /></a><br><br><b>' - . $obj['title'] . '</b><br><br><small>' - . $obj['tagline'] . '</small><br>', - ); - } - } + protected function getCategories() + { + $selection = [ + 'sort' => 'trending', + 'project_type' => 'campaign', + 'project_timing' => $this->getInput('timing'), + 'category_main' => null, + 'category_top_level' => null, + 'page_num' => 1, + 'per_page' => 12, + 'q' => '', + 'tags' => [] + ]; - protected function getCategories() { - - $selection = array( - 'sort' => 'trending', - 'project_type' => 'campaign', - 'project_timing' => $this->getInput('timing'), - 'category_main' => null, - 'category_top_level' => null, - 'page_num' => 1, - 'per_page' => 12, - 'q' => '', - 'tags' => array() - ); - - switch($this->queriedContext) { - case 'Tech & Innovation': - $selection['category_top_level'] = $this->queriedContext; - if ($this->getInput('tech') != 'all') { - $selection['category_main'] = $this->getInput('tech'); - } - break; - case 'Creative Works': - $selection['category_top_level'] = $this->queriedContext; - if ($this->getInput('creative') != 'all') { - $selection['category_main'] = $this->getInput('creative'); - } - break; - case 'Community Projects': - $selection['category_top_level'] = $this->queriedContext; - if ($this->getInput('community') != 'all') { - $selection['category_main'] = $this->getInput('community'); - } - break; - } - return $selection; - } + switch ($this->queriedContext) { + case 'Tech & Innovation': + $selection['category_top_level'] = $this->queriedContext; + if ($this->getInput('tech') != 'all') { + $selection['category_main'] = $this->getInput('tech'); + } + break; + case 'Creative Works': + $selection['category_top_level'] = $this->queriedContext; + if ($this->getInput('creative') != 'all') { + $selection['category_main'] = $this->getInput('creative'); + } + break; + case 'Community Projects': + $selection['category_top_level'] = $this->queriedContext; + if ($this->getInput('community') != 'all') { + $selection['category_main'] = $this->getInput('community'); + } + break; + } + return $selection; + } } diff --git a/bridges/InstagramBridge.php b/bridges/InstagramBridge.php index 4d05e3d4..ce7ff2bf 100644 --- a/bridges/InstagramBridge.php +++ b/bridges/InstagramBridge.php @@ -1,312 +1,330 @@ <?php -class InstagramBridge extends BridgeAbstract { - - // const MAINTAINER = 'pauder'; - const NAME = 'Instagram Bridge'; - const URI = 'https://www.instagram.com/'; - const DESCRIPTION = 'Returns the newest images'; - - const CONFIGURATION = array( - 'session_id' => array( - 'required' => false, - ), - 'cache_timeout' => array( - 'required' => false, - ), - ); - - const PARAMETERS = array( - 'Username' => array( - 'u' => array( - 'name' => 'username', - 'exampleValue' => 'aesoprockwins', - 'required' => true - ) - ), - 'Hashtag' => array( - 'h' => array( - 'name' => 'hashtag', - 'exampleValue' => 'beautifulday', - 'required' => true - ) - ), - 'Location' => array( - 'l' => array( - 'name' => 'location', - 'exampleValue' => 'london', - 'required' => true - ) - ), - 'global' => array( - 'media_type' => array( - 'name' => 'Media type', - 'type' => 'list', - 'required' => false, - 'values' => array( - 'All' => 'all', - 'Video' => 'video', - 'Picture' => 'picture', - 'Multiple' => 'multiple', - ), - 'defaultValue' => 'all' - ), - 'direct_links' => array( - 'name' => 'Use direct media links', - 'type' => 'checkbox', - ) - ) - - ); - - const TEST_DETECT_PARAMETERS = array( - 'https://www.instagram.com/metaverse' => array('u' => 'metaverse'), - 'https://instagram.com/metaverse' => array('u' => 'metaverse'), - 'http://www.instagram.com/metaverse' => array('u' => 'metaverse'), - ); - - const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951'; - const TAG_QUERY_HASH = '9b498c08113f1e09617a1703c22b2f32'; - const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a'; - - public function getCacheTimeout() { - $customTimeout = $this->getOption('cache_timeout'); - if ($customTimeout) { - return $customTimeout; - } - return parent::getCacheTimeout(); - } - - protected function getContents($uri) { - $headers = array(); - $sessionId = $this->getOption('session_id'); - if ($sessionId) { - $headers[] = 'cookie: sessionid=' . $sessionId; - } - return getContents($uri, $headers); - } - - protected function getInstagramUserId($username) { - - if(is_numeric($username)) return $username; - - $cacheFac = new CacheFactory(); - - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope(get_called_class()); - $cache->setKey(array($username)); - $key = $cache->loadData(); - - if($key == null) { - $data = $this->getContents(self::URI . 'web/search/topsearch/?query=' . $username); - foreach(json_decode($data)->users as $user) { - if(strtolower($user->user->username) === strtolower($username)) { - $key = $user->user->pk; - } - } - if($key == null) { - returnServerError('Unable to find username in search result.'); - } - $cache->saveData($key); - } - return $key; - - } - - public function collectData(){ - $directLink = !is_null($this->getInput('direct_links')) && $this->getInput('direct_links'); - - $data = $this->getInstagramJSON($this->getURI()); - - if(!is_null($this->getInput('u'))) { - $userMedia = $data->data->user->edge_owner_to_timeline_media->edges; - } elseif(!is_null($this->getInput('h'))) { - $userMedia = $data->data->hashtag->edge_hashtag_to_media->edges; - } elseif(!is_null($this->getInput('l'))) { - $userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges; - } - - foreach($userMedia as $media) { - $media = $media->node; - - switch($this->getInput('media_type')) { - case 'all': break; - case 'video': - if($media->__typename != 'GraphVideo' || !$media->is_video) continue 2; - break; - case 'picture': - if($media->__typename != 'GraphImage') continue 2; - break; - case 'multiple': - if($media->__typename != 'GraphSidecar') continue 2; - break; - default: break; - } - - $item = array(); - $item['uri'] = self::URI . 'p/' . $media->shortcode . '/'; - - if (isset($media->owner->username)) { - $item['author'] = $media->owner->username; - } - - $textContent = $this->getTextContent($media); - - $item['title'] = ($media->is_video ? '▶ ' : '') . $textContent; - $titleLinePos = strpos(wordwrap($item['title'], 120), "\n"); - if ($titleLinePos != false) { - $item['title'] = substr($item['title'], 0, $titleLinePos) . '...'; - } - - if($directLink) { - $mediaURI = $media->display_url; - } else { - $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l'; - } - - $pattern = array('/\@([\w\.]+)/', '/#([\w\.]+)/'); - $replace = array( - '<a href="https://www.instagram.com/$1">@$1</a>', - '<a href="https://www.instagram.com/explore/tags/$1">#$1</a>'); - - switch($media->__typename) { - case 'GraphSidecar': - $data = $this->getInstagramSidecarData($item['uri'], $item['title'], $media, $textContent); - $item['content'] = $data[0]; - $item['enclosures'] = $data[1]; - break; - case 'GraphImage': - $item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">'; - $item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />'; - $item['content'] .= '</a><br><br>' . nl2br(preg_replace($pattern, $replace, htmlentities($textContent))); - $item['enclosures'] = array($mediaURI); - break; - case 'GraphVideo': - $data = $this->getInstagramVideoData($item['uri'], $mediaURI, $media, $textContent); - $item['content'] = $data[0]; - if($directLink) { - $item['enclosures'] = $data[1]; - } else { - $item['enclosures'] = array($mediaURI); - } - $item['thumbnail'] = $mediaURI; - break; - default: break; - } - $item['timestamp'] = $media->taken_at_timestamp; - - $this->items[] = $item; - } - } - - // returns Sidecar(a post which has multiple media)'s contents and enclosures - protected function getInstagramSidecarData($uri, $postTitle, $mediaInfo, $textContent) { - $enclosures = array(); - $content = ''; - foreach($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) { - $singleMedia = $singleMedia->node; - if($singleMedia->is_video) { - if(in_array($singleMedia->video_url, $enclosures)) continue; // check if not added yet - $content .= '<video controls><source src="' . $singleMedia->video_url . '" type="video/mp4"></video><br>'; - array_push($enclosures, $singleMedia->video_url); - } else { - if(in_array($singleMedia->display_url, $enclosures)) continue; // check if not added yet - $content .= '<a href="' . $singleMedia->display_url . '" target="_blank">'; - $content .= '<img src="' . $singleMedia->display_url . '" alt="' . $postTitle . '" />'; - $content .= '</a><br>'; - array_push($enclosures, $singleMedia->display_url); - } - } - $content .= '<br>' . nl2br(htmlentities($textContent)); - - return array($content, $enclosures); - } - - // returns Video post's contents and enclosures - protected function getInstagramVideoData($uri, $mediaURI, $mediaInfo, $textContent) { - $content = '<video controls>'; - $content .= '<source src="' . $mediaInfo->video_url . '" poster="' . $mediaURI . '" type="video/mp4">'; - $content .= '<img src="' . $mediaURI . '" alt="">'; - $content .= '</video><br>'; - $content .= '<br>' . nl2br(htmlentities($textContent)); - - return array($content, array($mediaInfo->video_url)); - } - - protected function getTextContent($media) { - $textContent = '(no text)'; - //Process the first element, that isn't in the node graph - if (count($media->edge_media_to_caption->edges) > 0) { - $textContent = trim($media->edge_media_to_caption->edges[0]->node->text); - } - return $textContent; - } - - protected function getInstagramJSON($uri) { - - if(!is_null($this->getInput('u'))) { - - $userId = $this->getInstagramUserId($this->getInput('u')); - $data = $this->getContents(self::URI . - 'graphql/query/?query_hash=' . - self::USER_QUERY_HASH . - '&variables={"id"%3A"' . - $userId . - '"%2C"first"%3A10}'); - return json_decode($data); - - } elseif(!is_null($this->getInput('h'))) { - $data = $this->getContents(self::URI . - 'graphql/query/?query_hash=' . - self::TAG_QUERY_HASH . - '&variables={"tag_name"%3A"' . - $this->getInput('h') . - '"%2C"first"%3A10}'); - - return json_decode($data); - - } else { - - $html = getContents($uri); - $scriptRegex = '/window\._sharedData = (.*);<\/script>/'; - - preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0); - - return json_decode($matches[1][0]); - - } - - } - - public function getName(){ - if(!is_null($this->getInput('u'))) { - return $this->getInput('u') . ' - Instagram Bridge'; - } - - return parent::getName(); - } - - public function getURI(){ - if(!is_null($this->getInput('u'))) { - return self::URI . urlencode($this->getInput('u')) . '/'; - } elseif(!is_null($this->getInput('h'))) { - return self::URI . 'explore/tags/' . urlencode($this->getInput('h')); - } elseif(!is_null($this->getInput('l'))) { - return self::URI . 'explore/locations/' . urlencode($this->getInput('l')); - } - return parent::getURI(); - } - - public function detectParameters($url){ - $params = array(); - - // By username - $regex = '/^(https?:\/\/)?(www\.)?instagram\.com\/([^\/?\n]+)/'; - - if(preg_match($regex, $url, $matches) > 0) { - $params['u'] = urldecode($matches[3]); - return $params; - } - - return null; - } + +class InstagramBridge extends BridgeAbstract +{ + // const MAINTAINER = 'pauder'; + const NAME = 'Instagram Bridge'; + const URI = 'https://www.instagram.com/'; + const DESCRIPTION = 'Returns the newest images'; + + const CONFIGURATION = [ + 'session_id' => [ + 'required' => false, + ], + 'cache_timeout' => [ + 'required' => false, + ], + ]; + + const PARAMETERS = [ + 'Username' => [ + 'u' => [ + 'name' => 'username', + 'exampleValue' => 'aesoprockwins', + 'required' => true + ] + ], + 'Hashtag' => [ + 'h' => [ + 'name' => 'hashtag', + 'exampleValue' => 'beautifulday', + 'required' => true + ] + ], + 'Location' => [ + 'l' => [ + 'name' => 'location', + 'exampleValue' => 'london', + 'required' => true + ] + ], + 'global' => [ + 'media_type' => [ + 'name' => 'Media type', + 'type' => 'list', + 'required' => false, + 'values' => [ + 'All' => 'all', + 'Video' => 'video', + 'Picture' => 'picture', + 'Multiple' => 'multiple', + ], + 'defaultValue' => 'all' + ], + 'direct_links' => [ + 'name' => 'Use direct media links', + 'type' => 'checkbox', + ] + ] + + ]; + + const TEST_DETECT_PARAMETERS = [ + 'https://www.instagram.com/metaverse' => ['u' => 'metaverse'], + 'https://instagram.com/metaverse' => ['u' => 'metaverse'], + 'http://www.instagram.com/metaverse' => ['u' => 'metaverse'], + ]; + + const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951'; + const TAG_QUERY_HASH = '9b498c08113f1e09617a1703c22b2f32'; + const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a'; + + public function getCacheTimeout() + { + $customTimeout = $this->getOption('cache_timeout'); + if ($customTimeout) { + return $customTimeout; + } + return parent::getCacheTimeout(); + } + + protected function getContents($uri) + { + $headers = []; + $sessionId = $this->getOption('session_id'); + if ($sessionId) { + $headers[] = 'cookie: sessionid=' . $sessionId; + } + return getContents($uri, $headers); + } + + protected function getInstagramUserId($username) + { + if (is_numeric($username)) { + return $username; + } + + $cacheFac = new CacheFactory(); + + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope(get_called_class()); + $cache->setKey([$username]); + $key = $cache->loadData(); + + if ($key == null) { + $data = $this->getContents(self::URI . 'web/search/topsearch/?query=' . $username); + foreach (json_decode($data)->users as $user) { + if (strtolower($user->user->username) === strtolower($username)) { + $key = $user->user->pk; + } + } + if ($key == null) { + returnServerError('Unable to find username in search result.'); + } + $cache->saveData($key); + } + return $key; + } + + public function collectData() + { + $directLink = !is_null($this->getInput('direct_links')) && $this->getInput('direct_links'); + + $data = $this->getInstagramJSON($this->getURI()); + + if (!is_null($this->getInput('u'))) { + $userMedia = $data->data->user->edge_owner_to_timeline_media->edges; + } elseif (!is_null($this->getInput('h'))) { + $userMedia = $data->data->hashtag->edge_hashtag_to_media->edges; + } elseif (!is_null($this->getInput('l'))) { + $userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges; + } + + foreach ($userMedia as $media) { + $media = $media->node; + + switch ($this->getInput('media_type')) { + case 'all': + break; + case 'video': + if ($media->__typename != 'GraphVideo' || !$media->is_video) { + continue 2; + } + break; + case 'picture': + if ($media->__typename != 'GraphImage') { + continue 2; + } + break; + case 'multiple': + if ($media->__typename != 'GraphSidecar') { + continue 2; + } + break; + default: + break; + } + + $item = []; + $item['uri'] = self::URI . 'p/' . $media->shortcode . '/'; + + if (isset($media->owner->username)) { + $item['author'] = $media->owner->username; + } + + $textContent = $this->getTextContent($media); + + $item['title'] = ($media->is_video ? '▶ ' : '') . $textContent; + $titleLinePos = strpos(wordwrap($item['title'], 120), "\n"); + if ($titleLinePos != false) { + $item['title'] = substr($item['title'], 0, $titleLinePos) . '...'; + } + + if ($directLink) { + $mediaURI = $media->display_url; + } else { + $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l'; + } + + $pattern = ['/\@([\w\.]+)/', '/#([\w\.]+)/']; + $replace = [ + '<a href="https://www.instagram.com/$1">@$1</a>', + '<a href="https://www.instagram.com/explore/tags/$1">#$1</a>']; + + switch ($media->__typename) { + case 'GraphSidecar': + $data = $this->getInstagramSidecarData($item['uri'], $item['title'], $media, $textContent); + $item['content'] = $data[0]; + $item['enclosures'] = $data[1]; + break; + case 'GraphImage': + $item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">'; + $item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />'; + $item['content'] .= '</a><br><br>' . nl2br(preg_replace($pattern, $replace, htmlentities($textContent))); + $item['enclosures'] = [$mediaURI]; + break; + case 'GraphVideo': + $data = $this->getInstagramVideoData($item['uri'], $mediaURI, $media, $textContent); + $item['content'] = $data[0]; + if ($directLink) { + $item['enclosures'] = $data[1]; + } else { + $item['enclosures'] = [$mediaURI]; + } + $item['thumbnail'] = $mediaURI; + break; + default: + break; + } + $item['timestamp'] = $media->taken_at_timestamp; + + $this->items[] = $item; + } + } + + // returns Sidecar(a post which has multiple media)'s contents and enclosures + protected function getInstagramSidecarData($uri, $postTitle, $mediaInfo, $textContent) + { + $enclosures = []; + $content = ''; + foreach ($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) { + $singleMedia = $singleMedia->node; + if ($singleMedia->is_video) { + if (in_array($singleMedia->video_url, $enclosures)) { + continue; // check if not added yet + } + $content .= '<video controls><source src="' . $singleMedia->video_url . '" type="video/mp4"></video><br>'; + array_push($enclosures, $singleMedia->video_url); + } else { + if (in_array($singleMedia->display_url, $enclosures)) { + continue; // check if not added yet + } + $content .= '<a href="' . $singleMedia->display_url . '" target="_blank">'; + $content .= '<img src="' . $singleMedia->display_url . '" alt="' . $postTitle . '" />'; + $content .= '</a><br>'; + array_push($enclosures, $singleMedia->display_url); + } + } + $content .= '<br>' . nl2br(htmlentities($textContent)); + + return [$content, $enclosures]; + } + + // returns Video post's contents and enclosures + protected function getInstagramVideoData($uri, $mediaURI, $mediaInfo, $textContent) + { + $content = '<video controls>'; + $content .= '<source src="' . $mediaInfo->video_url . '" poster="' . $mediaURI . '" type="video/mp4">'; + $content .= '<img src="' . $mediaURI . '" alt="">'; + $content .= '</video><br>'; + $content .= '<br>' . nl2br(htmlentities($textContent)); + + return [$content, [$mediaInfo->video_url]]; + } + + protected function getTextContent($media) + { + $textContent = '(no text)'; + //Process the first element, that isn't in the node graph + if (count($media->edge_media_to_caption->edges) > 0) { + $textContent = trim($media->edge_media_to_caption->edges[0]->node->text); + } + return $textContent; + } + + protected function getInstagramJSON($uri) + { + if (!is_null($this->getInput('u'))) { + $userId = $this->getInstagramUserId($this->getInput('u')); + $data = $this->getContents(self::URI . + 'graphql/query/?query_hash=' . + self::USER_QUERY_HASH . + '&variables={"id"%3A"' . + $userId . + '"%2C"first"%3A10}'); + return json_decode($data); + } elseif (!is_null($this->getInput('h'))) { + $data = $this->getContents(self::URI . + 'graphql/query/?query_hash=' . + self::TAG_QUERY_HASH . + '&variables={"tag_name"%3A"' . + $this->getInput('h') . + '"%2C"first"%3A10}'); + + return json_decode($data); + } else { + $html = getContents($uri); + $scriptRegex = '/window\._sharedData = (.*);<\/script>/'; + + preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0); + + return json_decode($matches[1][0]); + } + } + + public function getName() + { + if (!is_null($this->getInput('u'))) { + return $this->getInput('u') . ' - Instagram Bridge'; + } + + return parent::getName(); + } + + public function getURI() + { + if (!is_null($this->getInput('u'))) { + return self::URI . urlencode($this->getInput('u')) . '/'; + } elseif (!is_null($this->getInput('h'))) { + return self::URI . 'explore/tags/' . urlencode($this->getInput('h')); + } elseif (!is_null($this->getInput('l'))) { + return self::URI . 'explore/locations/' . urlencode($this->getInput('l')); + } + return parent::getURI(); + } + + public function detectParameters($url) + { + $params = []; + + // By username + $regex = '/^(https?:\/\/)?(www\.)?instagram\.com\/([^\/?\n]+)/'; + + if (preg_match($regex, $url, $matches) > 0) { + $params['u'] = urldecode($matches[3]); + return $params; + } + + return null; + } } diff --git a/bridges/InstructablesBridge.php b/bridges/InstructablesBridge.php index 98987944..e1d2ef0b 100644 --- a/bridges/InstructablesBridge.php +++ b/bridges/InstructablesBridge.php @@ -1,359 +1,368 @@ <?php + /** * This class implements a bridge for http://www.instructables.com, supporting * general feeds and feeds by category. * * Remarks: * - For some reason it is very important to have the category URI end with a -* slash, otherwise the site defaults to the main category (i.e. Technology)! -* If you need to update the categories list, enable the 'listCategories' -* function (see comments below) and run the bridge with format=Html (see page -* source) +* slash, otherwise the site defaults to the main category (i.e. Technology)! +* If you need to update the categories list, enable the 'listCategories' +* function (see comments below) and run the bridge with format=Html (see page +* source) */ -class InstructablesBridge extends BridgeAbstract { - const NAME = 'Instructables Bridge'; - const URI = 'https://www.instructables.com'; - const DESCRIPTION = 'Returns general feeds and feeds by category'; - const MAINTAINER = 'logmanoriginal'; - const PARAMETERS = array( - 'Category' => array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'Circuits' => array( - 'All' => '/circuits/', - 'Apple' => '/circuits/apple/projects/', - 'Arduino' => '/circuits/arduino/projects/', - 'Art' => '/circuits/art/projects/', - 'Assistive Tech' => '/circuits/assistive-tech/projects/', - 'Audio' => '/circuits/audio/projects/', - 'Cameras' => '/circuits/cameras/projects/', - 'Clocks' => '/circuits/clocks/projects/', - 'Computers' => '/circuits/computers/projects/', - 'Electronics' => '/circuits/electronics/projects/', - 'Gadgets' => '/circuits/gadgets/projects/', - 'Lasers' => '/circuits/lasers/projects/', - 'LEDs' => '/circuits/leds/projects/', - 'Linux' => '/circuits/linux/projects/', - 'Microcontrollers' => '/circuits/microcontrollers/projects/', - 'Microsoft' => '/circuits/microsoft/projects/', - 'Mobile' => '/circuits/mobile/projects/', - 'Raspberry Pi' => '/circuits/raspberry-pi/projects/', - 'Remote Control' => '/circuits/remote-control/projects/', - 'Reuse' => '/circuits/reuse/projects/', - 'Robots' => '/circuits/robots/projects/', - 'Sensors' => '/circuits/sensors/projects/', - 'Software' => '/circuits/software/projects/', - 'Soldering' => '/circuits/soldering/projects/', - 'Speakers' => '/circuits/speakers/projects/', - 'Tools' => '/circuits/tools/projects/', - 'USB' => '/circuits/usb/projects/', - 'Wearables' => '/circuits/wearables/projects/', - 'Websites' => '/circuits/websites/projects/', - 'Wireless' => '/circuits/wireless/projects/', - ), - 'Workshop' => array( - 'All' => '/workshop/', - '3D Printing' => '/workshop/3d-printing/projects/', - 'Cars' => '/workshop/cars/projects/', - 'CNC' => '/workshop/cnc/projects/', - 'Electric Vehicles' => '/workshop/electric-vehicles/projects/', - 'Energy' => '/workshop/energy/projects/', - 'Furniture' => '/workshop/furniture/projects/', - 'Home Improvement' => '/workshop/home-improvement/projects/', - 'Home Theater' => '/workshop/home-theater/projects/', - 'Hydroponics' => '/workshop/hydroponics/projects/', - 'Knives' => '/workshop/knives/projects/', - 'Laser Cutting' => '/workshop/laser-cutting/projects/', - 'Lighting' => '/workshop/lighting/projects/', - 'Metalworking' => '/workshop/metalworking/projects/', - 'Molds & Casting' => '/workshop/molds-and-casting/projects/', - 'Motorcycles' => '/workshop/motorcycles/projects/', - 'Organizing' => '/workshop/organizing/projects/', - 'Pallets' => '/workshop/pallets/projects/', - 'Repair' => '/workshop/repair/projects/', - 'Science' => '/workshop/science/projects/', - 'Shelves' => '/workshop/shelves/projects/', - 'Solar' => '/workshop/solar/projects/', - 'Tools' => '/workshop/tools/projects/', - 'Woodworking' => '/workshop/woodworking/projects/', - 'Workbenches' => '/workshop/workbenches/projects/', - ), - 'Craft' => array( - 'All' => '/craft/', - 'Art' => '/craft/art/projects/', - 'Books & Journals' => '/craft/books-and-journals/projects/', - 'Cardboard' => '/craft/cardboard/projects/', - 'Cards' => '/craft/cards/projects/', - 'Clay' => '/craft/clay/projects/', - 'Costumes & Cosplay' => '/craft/costumes-and-cosplay/projects/', - 'Digital Graphics' => '/craft/digital-graphics/projects/', - 'Duct Tape' => '/craft/duct-tape/projects/', - 'Embroidery' => '/craft/embroidery/projects/', - 'Fashion' => '/craft/fashion/projects/', - 'Felt' => '/craft/felt/projects/', - 'Fiber Arts' => '/craft/fiber-arts/projects/', - 'Gift Wrapping' => '/craft/gift-wrapping/projects/', - 'Jewelry' => '/craft/jewelry/projects/', - 'Knitting & Crochet' => '/craft/knitting-and-crochet/projects/', - 'Leather' => '/craft/leather/projects/', - 'Mason Jars' => '/craft/mason-jars/projects/', - 'No-Sew' => '/craft/no-sew/projects/', - 'Paper' => '/craft/paper/projects/', - 'Parties & Weddings' => '/craft/parties-and-weddings/projects/', - 'Photography' => '/craft/photography/projects/', - 'Printmaking' => '/craft/printmaking/projects/', - 'Reuse' => '/craft/reuse/projects/', - 'Sewing' => '/craft/sewing/projects/', - 'Soapmaking' => '/craft/soapmaking/projects/', - 'Wallets' => '/craft/wallets/projects/', - ), - 'Cooking' => array( - 'All' => '/cooking/', - 'Bacon' => '/cooking/bacon/projects/', - 'BBQ & Grilling' => '/cooking/bbq-and-grilling/projects/', - 'Beverages' => '/cooking/beverages/projects/', - 'Bread' => '/cooking/bread/projects/', - 'Breakfast' => '/cooking/breakfast/projects/', - 'Cake' => '/cooking/cake/projects/', - 'Candy' => '/cooking/candy/projects/', - 'Canning & Preserving' => '/cooking/canning-and-preserving/projects/', - 'Cocktails & Mocktails' => '/cooking/cocktails-and-mocktails/projects/', - 'Coffee' => '/cooking/coffee/projects/', - 'Cookies' => '/cooking/cookies/projects/', - 'Cupcakes' => '/cooking/cupcakes/projects/', - 'Dessert' => '/cooking/dessert/projects/', - 'Homebrew' => '/cooking/homebrew/projects/', - 'Main Course' => '/cooking/main-course/projects/', - 'Pasta' => '/cooking/pasta/projects/', - 'Pie' => '/cooking/pie/projects/', - 'Pizza' => '/cooking/pizza/projects/', - 'Salad' => '/cooking/salad/projects/', - 'Sandwiches' => '/cooking/sandwiches/projects/', - 'Snacks & Appetizers' => '/cooking/snacks-and-appetizers/projects/', - 'Soups & Stews' => '/cooking/soups-and-stews/projects/', - 'Vegetarian & Vegan' => '/cooking/vegetarian-and-vegan/projects/', - ), - 'Living' => array( - 'All' => '/living/', - 'Beauty' => '/living/beauty/projects/', - 'Christmas' => '/living/christmas/projects/', - 'Cleaning' => '/living/cleaning/projects/', - 'Decorating' => '/living/decorating/projects/', - 'Education' => '/living/education/projects/', - 'Gardening' => '/living/gardening/projects/', - 'Halloween' => '/living/halloween/projects/', - 'Health' => '/living/health/projects/', - 'Hiding Places' => '/living/hiding-places/projects/', - 'Holidays' => '/living/holidays/projects/', - 'Homesteading' => '/living/homesteading/projects/', - 'Kids' => '/living/kids/projects/', - 'Kitchen' => '/living/kitchen/projects/', - 'LEGO & KNEX' => '/living/lego-and-knex/projects/', - 'Life Hacks' => '/living/life-hacks/projects/', - 'Music' => '/living/music/projects/', - 'Office Supply Hacks' => '/living/office-supply-hacks/projects/', - 'Organizing' => '/living/organizing/projects/', - 'Pest Control' => '/living/pest-control/projects/', - 'Pets' => '/living/pets/projects/', - 'Pranks, Tricks, & Humor' => '/living/pranks-tricks-and-humor/projects/', - 'Relationships' => '/living/relationships/projects/', - 'Toys & Games' => '/living/toys-and-games/projects/', - 'Travel' => '/living/travel/projects/', - 'Video Games' => '/living/video-games/projects/', - ), - 'Outside' => array( - 'All' => '/outside/', - 'Backyard' => '/outside/backyard/projects/', - 'Beach' => '/outside/beach/projects/', - 'Bikes' => '/outside/bikes/projects/', - 'Birding' => '/outside/birding/projects/', - 'Boats' => '/outside/boats/projects/', - 'Camping' => '/outside/camping/projects/', - 'Climbing' => '/outside/climbing/projects/', - 'Fire' => '/outside/fire/projects/', - 'Fishing' => '/outside/fishing/projects/', - 'Hunting' => '/outside/hunting/projects/', - 'Kites' => '/outside/kites/projects/', - 'Knots' => '/outside/knots/projects/', - 'Launchers' => '/outside/launchers/projects/', - 'Paracord' => '/outside/paracord/projects/', - 'Rockets' => '/outside/rockets/projects/', - 'Siege Engines' => '/outside/siege-engines/projects/', - 'Skateboarding' => '/outside/skateboarding/projects/', - 'Snow' => '/outside/snow/projects/', - 'Sports' => '/outside/sports/projects/', - 'Survival' => '/outside/survival/projects/', - 'Water' => '/outside/water/projects/', - ), - 'Makeymakey' => array( - 'All' => '/makeymakey/', - 'Makey Makey on Instructables' => '/makeymakey/', - ), - 'Teachers' => array( - 'All' => '/teachers/', - 'ELA' => '/teachers/ela/projects/', - 'Math' => '/teachers/math/projects/', - 'Science' => '/teachers/science/projects/', - 'Social Studies' => '/teachers/social-studies/projects/', - 'Engineering' => '/teachers/engineering/projects/', - 'Coding' => '/teachers/coding/projects/', - 'Electronics' => '/teachers/electronics/projects/', - 'Robotics' => '/teachers/robotics/projects/', - 'Arduino' => '/teachers/arduino/projects/', - 'CNC' => '/teachers/cnc/projects/', - 'Laser Cutting' => '/teachers/laser-cutting/projects/', - '3D Printing' => '/teachers/3d-printing/projects/', - '3D Design' => '/teachers/3d-design/projects/', - 'Art' => '/teachers/art/projects/', - 'Music' => '/teachers/music/projects/', - 'Theatre' => '/teachers/theatre/projects/', - 'Wood Shop' => '/teachers/wood-shop/projects/', - 'Metal Shop' => '/teachers/metal-shop/projects/', - 'Resources' => '/teachers/resources/projects/', - ), - ), - 'title' => 'Select your category (required)', - 'defaultValue' => 'Circuits' - ), - 'filter' => array( - 'name' => 'Filter', - 'type' => 'list', - 'values' => array( - 'Featured' => ' ', - 'Recent' => 'recent/', - 'Popular' => 'popular/', - 'Views' => 'views/', - 'Contest Winners' => 'winners/' - ), - 'title' => 'Select a filter', - 'defaultValue' => 'Featured' - ) - ) - ); +class InstructablesBridge extends BridgeAbstract +{ + const NAME = 'Instructables Bridge'; + const URI = 'https://www.instructables.com'; + const DESCRIPTION = 'Returns general feeds and feeds by category'; + const MAINTAINER = 'logmanoriginal'; + const PARAMETERS = [ + 'Category' => [ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'Circuits' => [ + 'All' => '/circuits/', + 'Apple' => '/circuits/apple/projects/', + 'Arduino' => '/circuits/arduino/projects/', + 'Art' => '/circuits/art/projects/', + 'Assistive Tech' => '/circuits/assistive-tech/projects/', + 'Audio' => '/circuits/audio/projects/', + 'Cameras' => '/circuits/cameras/projects/', + 'Clocks' => '/circuits/clocks/projects/', + 'Computers' => '/circuits/computers/projects/', + 'Electronics' => '/circuits/electronics/projects/', + 'Gadgets' => '/circuits/gadgets/projects/', + 'Lasers' => '/circuits/lasers/projects/', + 'LEDs' => '/circuits/leds/projects/', + 'Linux' => '/circuits/linux/projects/', + 'Microcontrollers' => '/circuits/microcontrollers/projects/', + 'Microsoft' => '/circuits/microsoft/projects/', + 'Mobile' => '/circuits/mobile/projects/', + 'Raspberry Pi' => '/circuits/raspberry-pi/projects/', + 'Remote Control' => '/circuits/remote-control/projects/', + 'Reuse' => '/circuits/reuse/projects/', + 'Robots' => '/circuits/robots/projects/', + 'Sensors' => '/circuits/sensors/projects/', + 'Software' => '/circuits/software/projects/', + 'Soldering' => '/circuits/soldering/projects/', + 'Speakers' => '/circuits/speakers/projects/', + 'Tools' => '/circuits/tools/projects/', + 'USB' => '/circuits/usb/projects/', + 'Wearables' => '/circuits/wearables/projects/', + 'Websites' => '/circuits/websites/projects/', + 'Wireless' => '/circuits/wireless/projects/', + ], + 'Workshop' => [ + 'All' => '/workshop/', + '3D Printing' => '/workshop/3d-printing/projects/', + 'Cars' => '/workshop/cars/projects/', + 'CNC' => '/workshop/cnc/projects/', + 'Electric Vehicles' => '/workshop/electric-vehicles/projects/', + 'Energy' => '/workshop/energy/projects/', + 'Furniture' => '/workshop/furniture/projects/', + 'Home Improvement' => '/workshop/home-improvement/projects/', + 'Home Theater' => '/workshop/home-theater/projects/', + 'Hydroponics' => '/workshop/hydroponics/projects/', + 'Knives' => '/workshop/knives/projects/', + 'Laser Cutting' => '/workshop/laser-cutting/projects/', + 'Lighting' => '/workshop/lighting/projects/', + 'Metalworking' => '/workshop/metalworking/projects/', + 'Molds & Casting' => '/workshop/molds-and-casting/projects/', + 'Motorcycles' => '/workshop/motorcycles/projects/', + 'Organizing' => '/workshop/organizing/projects/', + 'Pallets' => '/workshop/pallets/projects/', + 'Repair' => '/workshop/repair/projects/', + 'Science' => '/workshop/science/projects/', + 'Shelves' => '/workshop/shelves/projects/', + 'Solar' => '/workshop/solar/projects/', + 'Tools' => '/workshop/tools/projects/', + 'Woodworking' => '/workshop/woodworking/projects/', + 'Workbenches' => '/workshop/workbenches/projects/', + ], + 'Craft' => [ + 'All' => '/craft/', + 'Art' => '/craft/art/projects/', + 'Books & Journals' => '/craft/books-and-journals/projects/', + 'Cardboard' => '/craft/cardboard/projects/', + 'Cards' => '/craft/cards/projects/', + 'Clay' => '/craft/clay/projects/', + 'Costumes & Cosplay' => '/craft/costumes-and-cosplay/projects/', + 'Digital Graphics' => '/craft/digital-graphics/projects/', + 'Duct Tape' => '/craft/duct-tape/projects/', + 'Embroidery' => '/craft/embroidery/projects/', + 'Fashion' => '/craft/fashion/projects/', + 'Felt' => '/craft/felt/projects/', + 'Fiber Arts' => '/craft/fiber-arts/projects/', + 'Gift Wrapping' => '/craft/gift-wrapping/projects/', + 'Jewelry' => '/craft/jewelry/projects/', + 'Knitting & Crochet' => '/craft/knitting-and-crochet/projects/', + 'Leather' => '/craft/leather/projects/', + 'Mason Jars' => '/craft/mason-jars/projects/', + 'No-Sew' => '/craft/no-sew/projects/', + 'Paper' => '/craft/paper/projects/', + 'Parties & Weddings' => '/craft/parties-and-weddings/projects/', + 'Photography' => '/craft/photography/projects/', + 'Printmaking' => '/craft/printmaking/projects/', + 'Reuse' => '/craft/reuse/projects/', + 'Sewing' => '/craft/sewing/projects/', + 'Soapmaking' => '/craft/soapmaking/projects/', + 'Wallets' => '/craft/wallets/projects/', + ], + 'Cooking' => [ + 'All' => '/cooking/', + 'Bacon' => '/cooking/bacon/projects/', + 'BBQ & Grilling' => '/cooking/bbq-and-grilling/projects/', + 'Beverages' => '/cooking/beverages/projects/', + 'Bread' => '/cooking/bread/projects/', + 'Breakfast' => '/cooking/breakfast/projects/', + 'Cake' => '/cooking/cake/projects/', + 'Candy' => '/cooking/candy/projects/', + 'Canning & Preserving' => '/cooking/canning-and-preserving/projects/', + 'Cocktails & Mocktails' => '/cooking/cocktails-and-mocktails/projects/', + 'Coffee' => '/cooking/coffee/projects/', + 'Cookies' => '/cooking/cookies/projects/', + 'Cupcakes' => '/cooking/cupcakes/projects/', + 'Dessert' => '/cooking/dessert/projects/', + 'Homebrew' => '/cooking/homebrew/projects/', + 'Main Course' => '/cooking/main-course/projects/', + 'Pasta' => '/cooking/pasta/projects/', + 'Pie' => '/cooking/pie/projects/', + 'Pizza' => '/cooking/pizza/projects/', + 'Salad' => '/cooking/salad/projects/', + 'Sandwiches' => '/cooking/sandwiches/projects/', + 'Snacks & Appetizers' => '/cooking/snacks-and-appetizers/projects/', + 'Soups & Stews' => '/cooking/soups-and-stews/projects/', + 'Vegetarian & Vegan' => '/cooking/vegetarian-and-vegan/projects/', + ], + 'Living' => [ + 'All' => '/living/', + 'Beauty' => '/living/beauty/projects/', + 'Christmas' => '/living/christmas/projects/', + 'Cleaning' => '/living/cleaning/projects/', + 'Decorating' => '/living/decorating/projects/', + 'Education' => '/living/education/projects/', + 'Gardening' => '/living/gardening/projects/', + 'Halloween' => '/living/halloween/projects/', + 'Health' => '/living/health/projects/', + 'Hiding Places' => '/living/hiding-places/projects/', + 'Holidays' => '/living/holidays/projects/', + 'Homesteading' => '/living/homesteading/projects/', + 'Kids' => '/living/kids/projects/', + 'Kitchen' => '/living/kitchen/projects/', + 'LEGO & KNEX' => '/living/lego-and-knex/projects/', + 'Life Hacks' => '/living/life-hacks/projects/', + 'Music' => '/living/music/projects/', + 'Office Supply Hacks' => '/living/office-supply-hacks/projects/', + 'Organizing' => '/living/organizing/projects/', + 'Pest Control' => '/living/pest-control/projects/', + 'Pets' => '/living/pets/projects/', + 'Pranks, Tricks, & Humor' => '/living/pranks-tricks-and-humor/projects/', + 'Relationships' => '/living/relationships/projects/', + 'Toys & Games' => '/living/toys-and-games/projects/', + 'Travel' => '/living/travel/projects/', + 'Video Games' => '/living/video-games/projects/', + ], + 'Outside' => [ + 'All' => '/outside/', + 'Backyard' => '/outside/backyard/projects/', + 'Beach' => '/outside/beach/projects/', + 'Bikes' => '/outside/bikes/projects/', + 'Birding' => '/outside/birding/projects/', + 'Boats' => '/outside/boats/projects/', + 'Camping' => '/outside/camping/projects/', + 'Climbing' => '/outside/climbing/projects/', + 'Fire' => '/outside/fire/projects/', + 'Fishing' => '/outside/fishing/projects/', + 'Hunting' => '/outside/hunting/projects/', + 'Kites' => '/outside/kites/projects/', + 'Knots' => '/outside/knots/projects/', + 'Launchers' => '/outside/launchers/projects/', + 'Paracord' => '/outside/paracord/projects/', + 'Rockets' => '/outside/rockets/projects/', + 'Siege Engines' => '/outside/siege-engines/projects/', + 'Skateboarding' => '/outside/skateboarding/projects/', + 'Snow' => '/outside/snow/projects/', + 'Sports' => '/outside/sports/projects/', + 'Survival' => '/outside/survival/projects/', + 'Water' => '/outside/water/projects/', + ], + 'Makeymakey' => [ + 'All' => '/makeymakey/', + 'Makey Makey on Instructables' => '/makeymakey/', + ], + 'Teachers' => [ + 'All' => '/teachers/', + 'ELA' => '/teachers/ela/projects/', + 'Math' => '/teachers/math/projects/', + 'Science' => '/teachers/science/projects/', + 'Social Studies' => '/teachers/social-studies/projects/', + 'Engineering' => '/teachers/engineering/projects/', + 'Coding' => '/teachers/coding/projects/', + 'Electronics' => '/teachers/electronics/projects/', + 'Robotics' => '/teachers/robotics/projects/', + 'Arduino' => '/teachers/arduino/projects/', + 'CNC' => '/teachers/cnc/projects/', + 'Laser Cutting' => '/teachers/laser-cutting/projects/', + '3D Printing' => '/teachers/3d-printing/projects/', + '3D Design' => '/teachers/3d-design/projects/', + 'Art' => '/teachers/art/projects/', + 'Music' => '/teachers/music/projects/', + 'Theatre' => '/teachers/theatre/projects/', + 'Wood Shop' => '/teachers/wood-shop/projects/', + 'Metal Shop' => '/teachers/metal-shop/projects/', + 'Resources' => '/teachers/resources/projects/', + ], + ], + 'title' => 'Select your category (required)', + 'defaultValue' => 'Circuits' + ], + 'filter' => [ + 'name' => 'Filter', + 'type' => 'list', + 'values' => [ + 'Featured' => ' ', + 'Recent' => 'recent/', + 'Popular' => 'popular/', + 'Views' => 'views/', + 'Contest Winners' => 'winners/' + ], + 'title' => 'Select a filter', + 'defaultValue' => 'Featured' + ] + ] + ]; - public function collectData() { - // Enable the following line to get the category list (dev mode) - // $this->listCategories(); + public function collectData() + { + // Enable the following line to get the category list (dev mode) + // $this->listCategories(); - $html = getSimpleHTMLDOM($this->getURI()); - $html = defaultLinkTo($html, $this->getURI()); + $html = getSimpleHTMLDOM($this->getURI()); + $html = defaultLinkTo($html, $this->getURI()); - $covers = $html->find(' + $covers = $html->find(' .category-projects-list > div, .category-landing-projects-list > div, '); - foreach($covers as $cover) { - $item = array(); - - $item['uri'] = $cover->find('a.ible-title', 0)->href; - $item['title'] = $cover->find('a.ible-title', 0)->innertext; - $item['author'] = $this->getCategoryAuthor($cover); - $item['content'] = '<a href=' - . $item['uri'] - . '><img src=' - . $cover->find('img', 0)->getAttribute('data-src') - . '></a>'; - - $item['enclosures'][] = str_replace( - '.RECTANGLE1', - '.LARGE', - $cover->find('img', 0)->getAttribute('data-src') - ); + foreach ($covers as $cover) { + $item = []; - $this->items[] = $item; - } - } + $item['uri'] = $cover->find('a.ible-title', 0)->href; + $item['title'] = $cover->find('a.ible-title', 0)->innertext; + $item['author'] = $this->getCategoryAuthor($cover); + $item['content'] = '<a href=' + . $item['uri'] + . '><img src=' + . $cover->find('img', 0)->getAttribute('data-src') + . '></a>'; - public function getName() { - switch($this->queriedContext) { - case 'Category': - foreach(self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) { - $subcategory = array_search($this->getInput('category'), $value); + $item['enclosures'][] = str_replace( + '.RECTANGLE1', + '.LARGE', + $cover->find('img', 0)->getAttribute('data-src') + ); - if($subcategory !== false) - break; - } + $this->items[] = $item; + } + } - $filter = array_search( - $this->getInput('filter'), - self::PARAMETERS[$this->queriedContext]['filter']['values'] - ); + public function getName() + { + switch ($this->queriedContext) { + case 'Category': + foreach (self::PARAMETERS[$this->queriedContext]['category']['values'] as $key => $value) { + $subcategory = array_search($this->getInput('category'), $value); - return $subcategory . ' (' . $filter . ') - ' . static::NAME; - } + if ($subcategory !== false) { + break; + } + } - return parent::getName(); - } + $filter = array_search( + $this->getInput('filter'), + self::PARAMETERS[$this->queriedContext]['filter']['values'] + ); - public function getURI() { - switch($this->queriedContext) { - case 'Category': - return self::URI - . $this->getInput('category') - . $this->getInput('filter'); - } + return $subcategory . ' (' . $filter . ') - ' . static::NAME; + } - return parent::getURI(); - } + return parent::getName(); + } - /** - * Returns a list of categories for development purposes (used to build the - * parameters list) - */ - private function listCategories(){ + public function getURI() + { + switch ($this->queriedContext) { + case 'Category': + return self::URI + . $this->getInput('category') + . $this->getInput('filter'); + } - // Use home page to acquire main categories - $html = getSimpleHTMLDOM(self::URI); - $html = defaultLinkTo($html, self::URI); + return parent::getURI(); + } - foreach($html->find('.home-content-explore-link') as $category) { + /** + * Returns a list of categories for development purposes (used to build the + * parameters list) + */ + private function listCategories() + { + // Use home page to acquire main categories + $html = getSimpleHTMLDOM(self::URI); + $html = defaultLinkTo($html, self::URI); - // Use arbitrary category to receive full list - $html = getSimpleHTMLDOM($category->href); + foreach ($html->find('.home-content-explore-link') as $category) { + // Use arbitrary category to receive full list + $html = getSimpleHTMLDOM($category->href); - foreach($html->find('.channel-thumbnail a') as $channel) { - $name = html_entity_decode(trim($channel->title)); + foreach ($html->find('.channel-thumbnail a') as $channel) { + $name = html_entity_decode(trim($channel->title)); - // Remove unwanted entities - $name = str_replace("'", '', $name); - $name = str_replace(''', '', $name); + // Remove unwanted entities + $name = str_replace("'", '', $name); + $name = str_replace(''', '', $name); - $uri = $channel->href; + $uri = $channel->href; - $category_name = explode('/', $uri)[1]; + $category_name = explode('/', $uri)[1]; - if(!isset($categories) - || !array_key_exists($category_name, $categories) - || !in_array($uri, $categories[$category_name])) - $categories[$category_name][$name] = $uri; - } - } + if ( + !isset($categories) + || !array_key_exists($category_name, $categories) + || !in_array($uri, $categories[$category_name]) + ) { + $categories[$category_name][$name] = $uri; + } + } + } - // Build PHP array manually - foreach($categories as $key => $value) { - $name = ucfirst($key); - echo "'{$name}' => array(\n"; - echo "\t'All' => '/{$key}/',\n"; - foreach($value as $name => $uri) { - echo "\t'{$name}' => '{$uri}',\n"; - } - echo "),\n"; - } + // Build PHP array manually + foreach ($categories as $key => $value) { + $name = ucfirst($key); + echo "'{$name}' => array(\n"; + echo "\t'All' => '/{$key}/',\n"; + foreach ($value as $name => $uri) { + echo "\t'{$name}' => '{$uri}',\n"; + } + echo "),\n"; + } - die; - } + die; + } - /** - * Returns the author as anchor for a given cover. - */ - private function getCategoryAuthor($cover) { - return '<a href=' - . $cover->find('.ible-author a', 0)->href - . '>' - . $cover->find('.ible-author a', 0)->innertext - . '</a>'; - } + /** + * Returns the author as anchor for a given cover. + */ + private function getCategoryAuthor($cover) + { + return '<a href=' + . $cover->find('.ible-author a', 0)->href + . '>' + . $cover->find('.ible-author a', 0)->innertext + . '</a>'; + } } diff --git a/bridges/InternetArchiveBridge.php b/bridges/InternetArchiveBridge.php index b9f9d274..7175cde8 100644 --- a/bridges/InternetArchiveBridge.php +++ b/bridges/InternetArchiveBridge.php @@ -1,319 +1,326 @@ <?php -class InternetArchiveBridge extends BridgeAbstract { - const NAME = 'Internet Archive Bridge'; - const URI = 'https://archive.org'; - const DESCRIPTION = 'Returns newest uploads, posts and more from an account'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - 'Account' => array( - 'username' => array( - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '@verifiedjoseph', - ), - 'content' => array( - 'name' => 'Content', - 'type' => 'list', - 'values' => array( - 'Uploads' => 'uploads', - 'Posts' => 'posts', - 'Reviews' => 'reviews', - 'Collections' => 'collections', - 'Web Archives' => 'web-archive', - ), - 'defaultValue' => 'uploads', - ), - 'limit' => self::LIMIT, - ) - ); - - const CACHE_TIMEOUT = 900; // 15 mins - - const TEST_DETECT_PARAMETERS = array( - 'https://archive.org/details/@verifiedjoseph' => array( - 'context' => 'Account', 'username' => 'verifiedjoseph', 'content' => 'uploads' - ), - 'https://archive.org/details/@verifiedjoseph?tab=collections' => array( - 'context' => 'Account', 'username' => 'verifiedjoseph', 'content' => 'collections' - ), - ); - - private $skipClasses = array( - 'item-ia mobile-header hidden-tiles', - 'item-ia account-ia' - ); - - private $detectParamsRegex = '/https?:\/\/archive\.org\/details\/@([\w]+)(?:\?tab=([a-z-]+))?/'; - - public function detectParameters($url) { - $params = array(); - - if(preg_match($this->detectParamsRegex, $url, $matches) > 0) { - $params['context'] = 'Account'; - $params['username'] = $matches[1]; - $params['content'] = 'uploads'; - - if (isset($matches[2])) { - $params['content'] = $matches[2]; - } - - return $params; - } - - return null; - } - - public function collectData() { - - $html = getSimpleHTMLDOM($this->getURI()); - - $html = defaultLinkTo($html, $this->getURI()); - - if ($this->getInput('content') !== 'posts') { - $detailsDivNumber = 0; - - $results = $html->find('div.results > div[data-id]'); - foreach ($results as $index => $result) { - $item = array(); - - if (in_array($result->class, $this->skipClasses)) { - continue; - } - - switch($result->class) { - case 'item-ia': - switch($this->getInput('content')) { - case 'reviews': - $item = $this->processReview($result); - break; - case 'uploads': - $item = $this->processUpload($result); - break; - } - - break; - case 'item-ia url-item': - $item = $this->processWebArchives($result); - break; - case 'item-ia collection-ia': - $item = $this->processCollection($result); - break; - } - - if ($this->getInput('content') !== 'reviews') { - $hiddenDetails = $this->processHiddenDetails($html, $detailsDivNumber, $item); - - $this->items[] = array_merge($item, $hiddenDetails); - } else { - - $this->items[] = $item; - - } - - $detailsDivNumber++; - $limit = $this->getInput('limit') ?? 10; - if (count($this->items) >= $limit) { - break; - } - } - } - - if ($this->getInput('content') === 'posts') { - $this->items = $this->processPosts($html); - } - } - - public function getURI() { - - if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) { - return self::URI . '/details/' . $this->processUsername() . '&tab=' . $this->getInput('content'); - } - - return parent::getURI(); - } - - public function getName() { - - if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) { - $contentValues = array_flip(self::PARAMETERS['Account']['content']['values']); - - return $contentValues[$this->getInput('content')] . ' - ' - . $this->processUsername() . ' - Internet Archive'; - } - - return parent::getName(); - } - - private function processUsername() { - - if (substr($this->getInput('username'), 0, 1) !== '@') { - return '@' . $this->getInput('username'); - } - - return $this->getInput('username'); - } - - private function processUpload($result) { - $item = array(); - - $collection = $result->find('a.stealth', 0); - $collectionLink = $collection->href; - $collectionTitle = $collection->find('div.item-parent-ttl', 0)->plaintext; - - $item['title'] = trim($result->find('div.ttl', 0)->innertext); - $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext); - $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href; - - if ($result->find('div.by.C.C4', 0)->children(2)) { - $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext; - } - - $item['content'] = <<<EOD +class InternetArchiveBridge extends BridgeAbstract +{ + const NAME = 'Internet Archive Bridge'; + const URI = 'https://archive.org'; + const DESCRIPTION = 'Returns newest uploads, posts and more from an account'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + 'Account' => [ + 'username' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '@verifiedjoseph', + ], + 'content' => [ + 'name' => 'Content', + 'type' => 'list', + 'values' => [ + 'Uploads' => 'uploads', + 'Posts' => 'posts', + 'Reviews' => 'reviews', + 'Collections' => 'collections', + 'Web Archives' => 'web-archive', + ], + 'defaultValue' => 'uploads', + ], + 'limit' => self::LIMIT, + ] + ]; + + const CACHE_TIMEOUT = 900; // 15 mins + + const TEST_DETECT_PARAMETERS = [ + 'https://archive.org/details/@verifiedjoseph' => [ + 'context' => 'Account', 'username' => 'verifiedjoseph', 'content' => 'uploads' + ], + 'https://archive.org/details/@verifiedjoseph?tab=collections' => [ + 'context' => 'Account', 'username' => 'verifiedjoseph', 'content' => 'collections' + ], + ]; + + private $skipClasses = [ + 'item-ia mobile-header hidden-tiles', + 'item-ia account-ia' + ]; + + private $detectParamsRegex = '/https?:\/\/archive\.org\/details\/@([\w]+)(?:\?tab=([a-z-]+))?/'; + + public function detectParameters($url) + { + $params = []; + + if (preg_match($this->detectParamsRegex, $url, $matches) > 0) { + $params['context'] = 'Account'; + $params['username'] = $matches[1]; + $params['content'] = 'uploads'; + + if (isset($matches[2])) { + $params['content'] = $matches[2]; + } + + return $params; + } + + return null; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $html = defaultLinkTo($html, $this->getURI()); + + if ($this->getInput('content') !== 'posts') { + $detailsDivNumber = 0; + + $results = $html->find('div.results > div[data-id]'); + foreach ($results as $index => $result) { + $item = []; + + if (in_array($result->class, $this->skipClasses)) { + continue; + } + + switch ($result->class) { + case 'item-ia': + switch ($this->getInput('content')) { + case 'reviews': + $item = $this->processReview($result); + break; + case 'uploads': + $item = $this->processUpload($result); + break; + } + + break; + case 'item-ia url-item': + $item = $this->processWebArchives($result); + break; + case 'item-ia collection-ia': + $item = $this->processCollection($result); + break; + } + + if ($this->getInput('content') !== 'reviews') { + $hiddenDetails = $this->processHiddenDetails($html, $detailsDivNumber, $item); + + $this->items[] = array_merge($item, $hiddenDetails); + } else { + $this->items[] = $item; + } + + $detailsDivNumber++; + + $limit = $this->getInput('limit') ?? 10; + if (count($this->items) >= $limit) { + break; + } + } + } + + if ($this->getInput('content') === 'posts') { + $this->items = $this->processPosts($html); + } + } + + public function getURI() + { + if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) { + return self::URI . '/details/' . $this->processUsername() . '&tab=' . $this->getInput('content'); + } + + return parent::getURI(); + } + + public function getName() + { + if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) { + $contentValues = array_flip(self::PARAMETERS['Account']['content']['values']); + + return $contentValues[$this->getInput('content')] . ' - ' + . $this->processUsername() . ' - Internet Archive'; + } + + return parent::getName(); + } + + private function processUsername() + { + if (substr($this->getInput('username'), 0, 1) !== '@') { + return '@' . $this->getInput('username'); + } + + return $this->getInput('username'); + } + + private function processUpload($result) + { + $item = []; + + $collection = $result->find('a.stealth', 0); + $collectionLink = $collection->href; + $collectionTitle = $collection->find('div.item-parent-ttl', 0)->plaintext; + + $item['title'] = trim($result->find('div.ttl', 0)->innertext); + $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext); + $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href; + + if ($result->find('div.by.C.C4', 0)->children(2)) { + $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext; + } + + $item['content'] = <<<EOD <p>Media Type: {$result->attr['data-mediatype']}<br> Collection: <a href="{$collectionLink}">{$collectionTitle}</a></p> EOD; - $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; + $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; - return $item; - } + return $item; + } - private function processReview($result) { - $item = array(); + private function processReview($result) + { + $item = []; - $item['title'] = trim($result->find('div.ttl', 0)->innertext); - $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext); - $item['uri'] = $result->find('div.review-title', 0)->children(0)->href; + $item['title'] = trim($result->find('div.ttl', 0)->innertext); + $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext); + $item['uri'] = $result->find('div.review-title', 0)->children(0)->href; - if ($result->find('div.by.C.C4', 0)->children(2)) { - $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext; - } + if ($result->find('div.by.C.C4', 0)->children(2)) { + $item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext; + } - $item['content'] = <<<EOD + $item['content'] = <<<EOD <p><strong>Subject: {$result->find('div.review-title', 0)->plaintext}</strong></p> <p>{$result->find('div.hidden-lists.review' , 0)->children(1)->plaintext}</p> EOD; - $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; + $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; - return $item; - } + return $item; + } - private function processWebArchives($result) { - $item = array(); + private function processWebArchives($result) + { + $item = []; - $item['title'] = trim($result->find('div.ttl', 0)->plaintext); - $item['timestamp'] = strtotime($result->find('div.hidden-lists', 0)->children(0)->plaintext); - $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href; + $item['title'] = trim($result->find('div.ttl', 0)->plaintext); + $item['timestamp'] = strtotime($result->find('div.hidden-lists', 0)->children(0)->plaintext); + $item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href; - $item['content'] = <<<EOD + $item['content'] = <<<EOD {$this->processUsername()} archived <a href="{$item['uri']}">{$result->find('div.ttl', 0)->plaintext}</a> EOD; - $item['enclosures'][] = $result->find('img.item-img', 0)->source; + $item['enclosures'][] = $result->find('img.item-img', 0)->source; - return $item; - } + return $item; + } - private function processCollection($result) { - $item = array(); + private function processCollection($result) + { + $item = []; - $title = trim($result->find('div.collection-title.C.C2', 0)->children(0)->plaintext); - $itemCount = strtolower(trim($result->find('div.num-items.topinblock', 0)->plaintext)); + $title = trim($result->find('div.collection-title.C.C2', 0)->children(0)->plaintext); + $itemCount = strtolower(trim($result->find('div.num-items.topinblock', 0)->plaintext)); - $item['title'] = $title . ' (' . $itemCount . ')'; - $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext); - $item['uri'] = $result->find('div.collection-title.C.C2 > a', 0)->href; + $item['title'] = $title . ' (' . $itemCount . ')'; + $item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext); + $item['uri'] = $result->find('div.collection-title.C.C2 > a', 0)->href; - $item['content'] = ''; + $item['content'] = ''; - if ($result->find('img.item-img', 0)) { - $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; - } + if ($result->find('img.item-img', 0)) { + $item['enclosures'][] = self::URI . $result->find('img.item-img', 0)->source; + } - return $item; - } + return $item; + } - private function processHiddenDetails($html, $detailsDivNumber, $item) { - $description = ''; + private function processHiddenDetails($html, $detailsDivNumber, $item) + { + $description = ''; - if ($html->find('div.details-ia.hidden-tiles', $detailsDivNumber)) { - $detailsDiv = $html->find('div.details-ia.hidden-tiles', $detailsDivNumber); + if ($html->find('div.details-ia.hidden-tiles', $detailsDivNumber)) { + $detailsDiv = $html->find('div.details-ia.hidden-tiles', $detailsDivNumber); - if ($detailsDiv->find('div.C234', 0)->children(0)) { - $description = $detailsDiv->find('div.C234', 0)->children(0)->plaintext; + if ($detailsDiv->find('div.C234', 0)->children(0)) { + $description = $detailsDiv->find('div.C234', 0)->children(0)->plaintext; - $detailsDiv->find('div.C234', 0)->children(0)->innertext = ''; - } + $detailsDiv->find('div.C234', 0)->children(0)->innertext = ''; + } - $topics = trim($detailsDiv->find('div.C234', 0)->plaintext); + $topics = trim($detailsDiv->find('div.C234', 0)->plaintext); - if (!empty($topics)) { - $topics = trim($detailsDiv->find('div.C234', 0)->plaintext); - $topics = trim(substr($topics, 7)); + if (!empty($topics)) { + $topics = trim($detailsDiv->find('div.C234', 0)->plaintext); + $topics = trim(substr($topics, 7)); - $item['categories'] = explode(',', $topics); - } + $item['categories'] = explode(',', $topics); + } - $item['content'] = '<p>' . $description . '</p>' . $item['content']; - } + $item['content'] = '<p>' . $description . '</p>' . $item['content']; + } - return $item; - } + return $item; + } - private function processPosts($html) { - $items = array(); + private function processPosts($html) + { + $items = []; - foreach ($html->find('table.forumTable > tr') as $index => $tr) { - $item = array(); + foreach ($html->find('table.forumTable > tr') as $index => $tr) { + $item = []; - if ($index === 0) { - continue; - } + if ($index === 0) { + continue; + } - $item['title'] = $tr->find('td', 0)->plaintext; - $item['timestamp'] = strtotime($tr->find('td', 4)->children(0)->plaintext); - $item['uri'] = $tr->find('td', 0)->children(0)->href; + $item['title'] = $tr->find('td', 0)->plaintext; + $item['timestamp'] = strtotime($tr->find('td', 4)->children(0)->plaintext); + $item['uri'] = $tr->find('td', 0)->children(0)->href; - $formLink = <<<EOD + $formLink = <<<EOD <a href="{$tr->find('td', 2)->children(0)->href}">{$tr->find('td', 2)->children(0)->plaintext}</a> EOD; - $postDate = $tr->find('td', 4)->children(0)->plaintext; + $postDate = $tr->find('td', 4)->children(0)->plaintext; - $postPageHtml = getSimpleHTMLDOMCached($item['uri'], 3600); + $postPageHtml = getSimpleHTMLDOMCached($item['uri'], 3600); - $postPageHtml = defaultLinkTo($postPageHtml, $this->getURI()); + $postPageHtml = defaultLinkTo($postPageHtml, $this->getURI()); - $post = $postPageHtml->find('div.box.well.well-sm', 0); + $post = $postPageHtml->find('div.box.well.well-sm', 0); - $parentLink = ''; - $replyLink = <<<EOD + $parentLink = ''; + $replyLink = <<<EOD <a href="{$post->find('a', 0)->href}">Reply</a> EOD; - if ($post->find('a', 1)->innertext = 'See parent post') { - $parentLink = <<<EOD + if ($post->find('a', 1)->innertext = 'See parent post') { + $parentLink = <<<EOD <a href="{$post->find('a', 1)->href}">View parent post</a> EOD; - } + } - $post->find('h1', 0)->outertext = ''; - $post->find('h2', 0)->outertext = ''; + $post->find('h1', 0)->outertext = ''; + $post->find('h2', 0)->outertext = ''; - $item['content'] = <<<EOD + $item['content'] = <<<EOD <p>{$post->innertext}</p>{$replyLink} - {$parentLink} - Posted in {$formLink} on {$postDate} EOD; - $items[] = $item; + $items[] = $item; - if (count($items) >= $this->getInput('limit') ?? 10) { - break; - } - } + if (count($items) >= $this->getInput('limit') ?? 10) { + break; + } + } - return $items; - } + return $items; + } } diff --git a/bridges/ItchioBridge.php b/bridges/ItchioBridge.php index 3dcbd6bf..e7892306 100644 --- a/bridges/ItchioBridge.php +++ b/bridges/ItchioBridge.php @@ -1,46 +1,48 @@ <?php -class ItchioBridge extends BridgeAbstract { - const NAME = 'itch.io'; - const URI = 'https://itch.io'; - const DESCRIPTION = 'Fetches the file uploads for a product'; - const MAINTAINER = 'jacquesh'; - const PARAMETERS = array(array( - 'url' => array( - 'name' => 'Product URL', - 'exampleValue' => 'https://remedybg.itch.io/remedybg', - 'required' => true, - ) - )); - const CACHE_TIMEOUT = 21600; // 6 hours +class ItchioBridge extends BridgeAbstract +{ + const NAME = 'itch.io'; + const URI = 'https://itch.io'; + const DESCRIPTION = 'Fetches the file uploads for a product'; + const MAINTAINER = 'jacquesh'; + const PARAMETERS = [[ + 'url' => [ + 'name' => 'Product URL', + 'exampleValue' => 'https://remedybg.itch.io/remedybg', + 'required' => true, + ] + ]]; + const CACHE_TIMEOUT = 21600; // 6 hours - public function collectData() { - $url = $this->getInput('url'); - $html = getSimpleHTMLDOM($url); + public function collectData() + { + $url = $this->getInput('url'); + $html = getSimpleHTMLDOM($url); - $title = $html->find('.game_title', 0)->innertext; + $title = $html->find('.game_title', 0)->innertext; - $content = 'The following files are available to download:<br/>'; - foreach ($html->find('div.upload') as $element) { - $filename = $element->find('strong.name', 0)->innertext; - $filesize = $element->find('span.file_size', 0)->first_child()->innertext; - $content = $content . $filename . ' (' . $filesize . ')<br/>'; - } + $content = 'The following files are available to download:<br/>'; + foreach ($html->find('div.upload') as $element) { + $filename = $element->find('strong.name', 0)->innertext; + $filesize = $element->find('span.file_size', 0)->first_child()->innertext; + $content = $content . $filename . ' (' . $filesize . ')<br/>'; + } - // On 2021-04-28/29, itch.io changed their project page format so that the - // 'last updated' timestamp is only shown to logged-in users. - // Since we can't use the last-updated date to identify a post, we include - // the description text in the input for the UID hash so that if the - // project posts an update that changes the description but does not add - // or rename any files, we'll still flag it as an update. - $project_description = $html->find('div.formatted_description', 0)->plaintext; - $uidContent = $project_description . $content; + // On 2021-04-28/29, itch.io changed their project page format so that the + // 'last updated' timestamp is only shown to logged-in users. + // Since we can't use the last-updated date to identify a post, we include + // the description text in the input for the UID hash so that if the + // project posts an update that changes the description but does not add + // or rename any files, we'll still flag it as an update. + $project_description = $html->find('div.formatted_description', 0)->plaintext; + $uidContent = $project_description . $content; - $item = array(); - $item['uri'] = $url; - $item['uid'] = $uidContent; - $item['title'] = 'Update for ' . $title; - $item['content'] = $content; - $this->items[] = $item; - } + $item = []; + $item['uri'] = $url; + $item['uid'] = $uidContent; + $item['title'] = 'Update for ' . $title; + $item['content'] = $content; + $this->items[] = $item; + } } diff --git a/bridges/IvooxBridge.php b/bridges/IvooxBridge.php index d48be598..971c4632 100644 --- a/bridges/IvooxBridge.php +++ b/bridges/IvooxBridge.php @@ -1,128 +1,132 @@ <?php + /** * IvooxRssBridge * Returns the latest search result * TODO: support podcast episodes list */ -class IvooxBridge extends BridgeAbstract { - const NAME = 'Ivoox Bridge'; - const URI = 'https://www.ivoox.com/'; - const CACHE_TIMEOUT = 10800; // 3h - const DESCRIPTION = 'Returns the 10 newest episodes by keyword search'; - const MAINTAINER = 'xurxof'; // based on YoutubeBridge by mitsukarenai - const PARAMETERS = array( - 'Search result' => array( - 's' => array( - 'name' => 'keyword', - 'required' => true, - 'exampleValue' => 'car' - ) - ) - ); - - private function ivBridgeAddItem( - $episode_link, - $podcast_name, - $episode_title, - $author_name, - $episode_description, - $publication_date, - $episode_duration) { - $item = array(); - $item['title'] = htmlspecialchars_decode($podcast_name . ': ' . $episode_title); - $item['author'] = $author_name; - $item['timestamp'] = $publication_date; - $item['uri'] = $episode_link; - $item['content'] = '<a href="' . $episode_link . '">' . $podcast_name . ': ' . $episode_title - . '</a><br />Duration: ' . $episode_duration - . '<br />Description:<br />' . $episode_description; - $this->items[] = $item; - } - - private function ivBridgeParseHtmlListing($html) { - $limit = 4; - $count = 0; - - foreach($html->find('div.flip-container') as $flipper) { - $linkcount = 0; - if(!empty($flipper->find( 'div.modulo-type-banner' ))) { - // ad - continue; - } - - if($count < $limit) { - foreach($flipper->find('div.header-modulo') as $element) { - foreach($element->find('a') as $link) { - if ($linkcount == 0) { - $episode_link = $link->href; - $episode_title = $link->title; - } elseif ($linkcount == 1) { - $author_link = $link->href; - $author_name = $link->title; - } elseif ($linkcount == 2) { - $podcast_link = $link->href; - $podcast_name = $link->title; - } - - $linkcount++; - } - } - - $episode_description = $flipper->find('button.btn-link', 0)->getAttribute('data-content'); - $episode_duration = $flipper->find('p.time', 0)->innertext; - $publication_date = $flipper->find('li.date', 0)->getAttribute('title'); - - // alternative date_parse_from_format - // or DateTime::createFromFormat('G:i - d \d\e M \d\e Y', $publication); - // TODO: month name translations, due function doesn't support locale - - $a = strptime($publication_date, '%H:%M - %d de %b. de %Y'); // obsolete function, uses c libraries - $publication_date = mktime(0, 0, 0, $a['tm_mon'] + 1, $a['tm_mday'], $a['tm_year'] + 1900); - - $this->ivBridgeAddItem( - $episode_link, - $podcast_name, - $episode_title, - $author_name, - $episode_description, - $publication_date, - $episode_duration - ); - $count++; - } - } - } - - public function collectData() { - - // store locale, change to spanish - $originalLocales = explode(';', setlocale(LC_ALL, 0)); - setlocale(LC_ALL, 'es_ES.utf8'); - - $xml = ''; - $html = ''; - $url_feed = ''; - if($this->getInput('s')) { /* Search modes */ - $this->request = str_replace(' ', '-', $this->getInput('s')); - $url_feed = self::URI . urlencode($this->request) . '_sb_f_1.html?o=uploaddate'; - } else { - returnClientError('Not valid mode at IvooxBridge'); - } - - $dom = getSimpleHTMLDOM($url_feed); - $this->ivBridgeParseHtmlListing($dom); - - // restore locale - - foreach($originalLocales as $localeSetting) { - if(strpos($localeSetting, '=') !== false) { - list($category, $locale) = explode('=', $localeSetting); - } else { - $category = LC_ALL; - $locale = $localeSetting; - } - - setlocale($category, $locale); - } - } +class IvooxBridge extends BridgeAbstract +{ + const NAME = 'Ivoox Bridge'; + const URI = 'https://www.ivoox.com/'; + const CACHE_TIMEOUT = 10800; // 3h + const DESCRIPTION = 'Returns the 10 newest episodes by keyword search'; + const MAINTAINER = 'xurxof'; // based on YoutubeBridge by mitsukarenai + const PARAMETERS = [ + 'Search result' => [ + 's' => [ + 'name' => 'keyword', + 'required' => true, + 'exampleValue' => 'car' + ] + ] + ]; + + private function ivBridgeAddItem( + $episode_link, + $podcast_name, + $episode_title, + $author_name, + $episode_description, + $publication_date, + $episode_duration + ) { + $item = []; + $item['title'] = htmlspecialchars_decode($podcast_name . ': ' . $episode_title); + $item['author'] = $author_name; + $item['timestamp'] = $publication_date; + $item['uri'] = $episode_link; + $item['content'] = '<a href="' . $episode_link . '">' . $podcast_name . ': ' . $episode_title + . '</a><br />Duration: ' . $episode_duration + . '<br />Description:<br />' . $episode_description; + $this->items[] = $item; + } + + private function ivBridgeParseHtmlListing($html) + { + $limit = 4; + $count = 0; + + foreach ($html->find('div.flip-container') as $flipper) { + $linkcount = 0; + if (!empty($flipper->find('div.modulo-type-banner'))) { + // ad + continue; + } + + if ($count < $limit) { + foreach ($flipper->find('div.header-modulo') as $element) { + foreach ($element->find('a') as $link) { + if ($linkcount == 0) { + $episode_link = $link->href; + $episode_title = $link->title; + } elseif ($linkcount == 1) { + $author_link = $link->href; + $author_name = $link->title; + } elseif ($linkcount == 2) { + $podcast_link = $link->href; + $podcast_name = $link->title; + } + + $linkcount++; + } + } + + $episode_description = $flipper->find('button.btn-link', 0)->getAttribute('data-content'); + $episode_duration = $flipper->find('p.time', 0)->innertext; + $publication_date = $flipper->find('li.date', 0)->getAttribute('title'); + + // alternative date_parse_from_format + // or DateTime::createFromFormat('G:i - d \d\e M \d\e Y', $publication); + // TODO: month name translations, due function doesn't support locale + + $a = strptime($publication_date, '%H:%M - %d de %b. de %Y'); // obsolete function, uses c libraries + $publication_date = mktime(0, 0, 0, $a['tm_mon'] + 1, $a['tm_mday'], $a['tm_year'] + 1900); + + $this->ivBridgeAddItem( + $episode_link, + $podcast_name, + $episode_title, + $author_name, + $episode_description, + $publication_date, + $episode_duration + ); + $count++; + } + } + } + + public function collectData() + { + // store locale, change to spanish + $originalLocales = explode(';', setlocale(LC_ALL, 0)); + setlocale(LC_ALL, 'es_ES.utf8'); + + $xml = ''; + $html = ''; + $url_feed = ''; + if ($this->getInput('s')) { /* Search modes */ + $this->request = str_replace(' ', '-', $this->getInput('s')); + $url_feed = self::URI . urlencode($this->request) . '_sb_f_1.html?o=uploaddate'; + } else { + returnClientError('Not valid mode at IvooxBridge'); + } + + $dom = getSimpleHTMLDOM($url_feed); + $this->ivBridgeParseHtmlListing($dom); + + // restore locale + + foreach ($originalLocales as $localeSetting) { + if (strpos($localeSetting, '=') !== false) { + list($category, $locale) = explode('=', $localeSetting); + } else { + $category = LC_ALL; + $locale = $localeSetting; + } + + setlocale($category, $locale); + } + } } diff --git a/bridges/JapanExpoBridge.php b/bridges/JapanExpoBridge.php index c56999fe..0d02e753 100644 --- a/bridges/JapanExpoBridge.php +++ b/bridges/JapanExpoBridge.php @@ -1,104 +1,108 @@ <?php -class JapanExpoBridge extends BridgeAbstract { - const MAINTAINER = 'Ginko'; - const NAME = 'Japan Expo Actualités'; - const URI = 'https://www.japan-expo-paris.com/fr/actualites'; - const CACHE_TIMEOUT = 14400; // 4h - const DESCRIPTION = 'Returns most recent entries from Japan Expo actualités.'; - const PARAMETERS = array( array( - 'mode' => array( - 'name' => 'Show full contents', - 'type' => 'checkbox', - ) - )); +class JapanExpoBridge extends BridgeAbstract +{ + const MAINTAINER = 'Ginko'; + const NAME = 'Japan Expo Actualités'; + const URI = 'https://www.japan-expo-paris.com/fr/actualites'; + const CACHE_TIMEOUT = 14400; // 4h + const DESCRIPTION = 'Returns most recent entries from Japan Expo actualités.'; + const PARAMETERS = [ [ + 'mode' => [ + 'name' => 'Show full contents', + 'type' => 'checkbox', + ] + ]]; - public function getIcon() { - return 'https://s.japan-expo.com/katana/images/JES073/favicons/paris.png'; - } + public function getIcon() + { + return 'https://s.japan-expo.com/katana/images/JES073/favicons/paris.png'; + } - public function collectData(){ + public function collectData() + { + $convert_article_images = function ($matches) { + if (is_array($matches) && count($matches) > 1) { + return '<img src="' . $matches[1] . '" />'; + } + }; - $convert_article_images = function($matches){ - if(is_array($matches) && count($matches) > 1) { - return '<img src="' . $matches[1] . '" />'; - } - }; + $html = getSimpleHTMLDOM(self::URI); + $fullcontent = $this->getInput('mode'); + $count = 0; - $html = getSimpleHTMLDOM(self::URI); - $fullcontent = $this->getInput('mode'); - $count = 0; + foreach ($html->find('a._tile2') as $element) { + $url = $element->href; + $thumbnail = 'https://s.japan-expo.com/katana/images/JES049/paris.png'; + preg_match('/url\(([^)]+)\)/', $element->find('img.rspvimgset', 0)->style, $img_search_result); - foreach($html->find('a._tile2') as $element) { + if (count($img_search_result) >= 2) { + $thumbnail = trim($img_search_result[1], "'"); + } - $url = $element->href; - $thumbnail = 'https://s.japan-expo.com/katana/images/JES049/paris.png'; - preg_match('/url\(([^)]+)\)/', $element->find('img.rspvimgset', 0)->style, $img_search_result); + if ($fullcontent) { + if ($count >= 5) { + break; + } - if(count($img_search_result) >= 2) - $thumbnail = trim($img_search_result[1], "'"); + $article_html = getSimpleHTMLDOMCached($url); + $header = $article_html->find('header.pageHeadBox', 0); + $timestamp = strtotime($header->find('time', 0)->datetime); + $title_html = $header->find('div.section', 0)->next_sibling(); + $title = $title_html->plaintext; + $headings = $title_html->next_sibling()->outertext; + $article = $article_html->find('div.content', 0)->innertext; + $article = preg_replace_callback( + '/<img [^>]+ style="[^\(]+\(\'([^\']+)\'[^>]+>/i', + $convert_article_images, + $article + ); - if($fullcontent) { - if($count >= 5) { - break; - } + $content = $headings . $article; + } else { + $date_text = $element->find('span.date', 0)->plaintext; + $timestamp = $this->frenchPubDateToTimestamp($date_text); + $title = trim($element->find('span._title', 0)->plaintext); + $content = '<img src="' + . $thumbnail + . '"></img><br />' + . $date_text + . '<br /><a href="' + . $url + . '">Lire l\'article</a>'; + } - $article_html = getSimpleHTMLDOMCached($url); - $header = $article_html->find('header.pageHeadBox', 0); - $timestamp = strtotime($header->find('time', 0)->datetime); - $title_html = $header->find('div.section', 0)->next_sibling(); - $title = $title_html->plaintext; - $headings = $title_html->next_sibling()->outertext; - $article = $article_html->find('div.content', 0)->innertext; - $article = preg_replace_callback( - '/<img [^>]+ style="[^\(]+\(\'([^\']+)\'[^>]+>/i', - $convert_article_images, - $article); + $item = []; + $item['uri'] = $url; + $item['title'] = $title; + $item['timestamp'] = $timestamp; + $item['enclosures'] = [$thumbnail]; + $item['content'] = $content; + $this->items[] = $item; + $count++; + } + } - $content = $headings . $article; - } else { - $date_text = $element->find('span.date', 0)->plaintext; - $timestamp = $this->frenchPubDateToTimestamp($date_text); - $title = trim($element->find('span._title', 0)->plaintext); - $content = '<img src="' - . $thumbnail - . '"></img><br />' - . $date_text - . '<br /><a href="' - . $url - . '">Lire l\'article</a>'; - } - - $item = array(); - $item['uri'] = $url; - $item['title'] = $title; - $item['timestamp'] = $timestamp; - $item['enclosures'] = array($thumbnail); - $item['content'] = $content; - $this->items[] = $item; - $count++; - } - } - - private function frenchPubDateToTimestamp($date_to_parse) { - return strtotime( - strtr( - strtolower(str_replace('Publié le ', '', $date_to_parse)), - array( - 'janvier' => 'jan', - 'février' => 'feb', - 'mars' => 'march', - 'avril' => 'apr', - 'mai' => 'may', - 'juin' => 'jun', - 'juillet' => 'jul', - 'août' => 'aug', - 'septembre' => 'sep', - 'octobre' => 'oct', - 'novembre' => 'nov', - 'décembre' => 'dec' - ) - ) - ); - } + private function frenchPubDateToTimestamp($date_to_parse) + { + return strtotime( + strtr( + strtolower(str_replace('Publié le ', '', $date_to_parse)), + [ + 'janvier' => 'jan', + 'février' => 'feb', + 'mars' => 'march', + 'avril' => 'apr', + 'mai' => 'may', + 'juin' => 'jun', + 'juillet' => 'jul', + 'août' => 'aug', + 'septembre' => 'sep', + 'octobre' => 'oct', + 'novembre' => 'nov', + 'décembre' => 'dec' + ] + ) + ); + } } diff --git a/bridges/JornalDeNoticiasBridge.php b/bridges/JornalDeNoticiasBridge.php index e61a7a42..a9c2031e 100644 --- a/bridges/JornalDeNoticiasBridge.php +++ b/bridges/JornalDeNoticiasBridge.php @@ -1,54 +1,59 @@ <?php -class JornalDeNoticiasBridge extends BridgeAbstract { - const NAME = 'Jornal de Notícias (PT)'; - const URI = 'https://jn.pt'; - const DESCRIPTION = 'Jornal de Notícias (JN.PT)'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - 'URL' => array( - 'url' => array( - 'name' => 'URL (relative)', - 'exampleValue' => 'opiniao/catia-domingues.html', - ) - ) - ); - - public function getIcon() { - return 'https://static.globalnoticias.pt/jn/common/images/favicons/favicon-128.png'; - } - - public function getURI() { - switch($this->queriedContext) { - case 'URL': - $url = self::URI . '/' . $this->getInput('url'); - break; - default: - $url = self::URI; - } - return $url; - } - - public function collectData() { - $archives = self::getURI(); - $html = getSimpleHTMLDOMCached($archives); - - foreach($html->find('article') as $element) { - $item = array(); - - $title = $element->find('h2 a', 0); - $link = $element->find('h2 a', 0); - $auth = $element->find('h3 a', 0); - - $item['title'] = $title->plaintext; - $item['uri'] = self::URI . $link->href; - $item['author'] = $auth->plaintext; - - $snippet = $element->find('h4 a', 0); - if ($snippet) { - $item['content'] = $snippet->plaintext; - } - - $this->items[] = $item; - } - } + +class JornalDeNoticiasBridge extends BridgeAbstract +{ + const NAME = 'Jornal de Notícias (PT)'; + const URI = 'https://jn.pt'; + const DESCRIPTION = 'Jornal de Notícias (JN.PT)'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + 'URL' => [ + 'url' => [ + 'name' => 'URL (relative)', + 'exampleValue' => 'opiniao/catia-domingues.html', + ] + ] + ]; + + public function getIcon() + { + return 'https://static.globalnoticias.pt/jn/common/images/favicons/favicon-128.png'; + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'URL': + $url = self::URI . '/' . $this->getInput('url'); + break; + default: + $url = self::URI; + } + return $url; + } + + public function collectData() + { + $archives = self::getURI(); + $html = getSimpleHTMLDOMCached($archives); + + foreach ($html->find('article') as $element) { + $item = []; + + $title = $element->find('h2 a', 0); + $link = $element->find('h2 a', 0); + $auth = $element->find('h3 a', 0); + + $item['title'] = $title->plaintext; + $item['uri'] = self::URI . $link->href; + $item['author'] = $auth->plaintext; + + $snippet = $element->find('h4 a', 0); + if ($snippet) { + $item['content'] = $snippet->plaintext; + } + + $this->items[] = $item; + } + } } diff --git a/bridges/JustETFBridge.php b/bridges/JustETFBridge.php index 2f322789..88920133 100644 --- a/bridges/JustETFBridge.php +++ b/bridges/JustETFBridge.php @@ -1,352 +1,368 @@ <?php -class JustETFBridge extends BridgeAbstract { - const NAME = 'justETF Bridge'; - const URI = 'https://www.justetf.com'; - const DESCRIPTION = 'Currently only supports the news feed'; - const MAINTAINER = 'logmanoriginal'; - const PARAMETERS = array( - 'News' => array( - 'full' => array( - 'name' => 'Full Article', - 'type' => 'checkbox', - 'title' => 'Enable to load full articles' - ) - ), - 'Profile' => array( - 'isin' => array( - 'name' => 'ISIN', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'IE00B4X9L533', - 'pattern' => '[a-zA-Z]{2}[a-zA-Z0-9]{10}', - 'title' => 'ISIN, consisting of 2-letter country code, 9-character identifier, check character' - ), - 'strategy' => array( - 'name' => 'Include Strategy', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'description' => array( - 'name' => 'Include Description', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ) - ), - 'global' => array( - 'lang' => array( - 'name' => 'Language', - 'type' => 'list', - 'values' => array( - 'Englisch' => 'en', - 'Deutsch' => 'de', - 'Italiano' => 'it' - ), - 'defaultValue' => 'Englisch' - ) - ) - ); - - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); - - defaultLinkTo($html, static::URI); - - switch($this->queriedContext) { - case 'News': - $this->collectNews($html); - break; - case 'Profile': - $this->collectProfile($html); - break; - } - } - - public function getURI() { - $uri = static::URI; - - if($this->getInput('lang')) { - $uri .= '/' . $this->getInput('lang'); - } - - switch($this->queriedContext) { - case 'News': - $uri .= '/news'; - break; - case 'Profile': - $uri .= '/etf-profile.html?' . http_build_query(array( - 'isin' => strtoupper($this->getInput('isin')) - )); - break; - } - - return $uri; - } - - public function getName() { - $name = static::NAME; - - $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : ''; - - switch($this->queriedContext) { - case 'News': break; - case 'Profile': - if($this->getInput('isin')) { - $name .= ' ISIN ' . strtoupper($this->getInput('isin')); - } - } - - if($this->getInput('lang')) { - $name .= ' (' . strtoupper($this->getInput('lang')) . ')'; - } - - return $name; - } - - #region Common - - /** - * Fixes dates depending on the choosen language: - * - * de : dd.mm.yy - * en : dd.mm.yy - * it : dd/mm/yy - * - * Basically strtotime doesn't convert dates correctly due to formats - * being hard to interpret. So we use the DateTime object, manually - * fixing dates and times (set to 00:00:00.000). - * - * We don't know the timezone, so just assume +00:00 (or whatever - * DateTime chooses) - */ - private function fixDate($date) { - switch($this->getInput('lang')) { - case 'en': - case 'de': - $df = date_create_from_format('d.m.y', $date); - break; - case 'it': - $df = date_create_from_format('d/m/y', $date); - break; - } - date_time_set($df, 0, 0); +class JustETFBridge extends BridgeAbstract +{ + const NAME = 'justETF Bridge'; + const URI = 'https://www.justetf.com'; + const DESCRIPTION = 'Currently only supports the news feed'; + const MAINTAINER = 'logmanoriginal'; + const PARAMETERS = [ + 'News' => [ + 'full' => [ + 'name' => 'Full Article', + 'type' => 'checkbox', + 'title' => 'Enable to load full articles' + ] + ], + 'Profile' => [ + 'isin' => [ + 'name' => 'ISIN', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'IE00B4X9L533', + 'pattern' => '[a-zA-Z]{2}[a-zA-Z0-9]{10}', + 'title' => 'ISIN, consisting of 2-letter country code, 9-character identifier, check character' + ], + 'strategy' => [ + 'name' => 'Include Strategy', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'description' => [ + 'name' => 'Include Description', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ] + ], + 'global' => [ + 'lang' => [ + 'name' => 'Language', + 'type' => 'list', + 'values' => [ + 'Englisch' => 'en', + 'Deutsch' => 'de', + 'Italiano' => 'it' + ], + 'defaultValue' => 'Englisch' + ] + ] + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + defaultLinkTo($html, static::URI); + + switch ($this->queriedContext) { + case 'News': + $this->collectNews($html); + break; + case 'Profile': + $this->collectProfile($html); + break; + } + } + + public function getURI() + { + $uri = static::URI; + + if ($this->getInput('lang')) { + $uri .= '/' . $this->getInput('lang'); + } + + switch ($this->queriedContext) { + case 'News': + $uri .= '/news'; + break; + case 'Profile': + $uri .= '/etf-profile.html?' . http_build_query([ + 'isin' => strtoupper($this->getInput('isin')) + ]); + break; + } + + return $uri; + } + + public function getName() + { + $name = static::NAME; + + $name .= ($this->queriedContext) ? ' - ' . $this->queriedContext : ''; + + switch ($this->queriedContext) { + case 'News': + break; + case 'Profile': + if ($this->getInput('isin')) { + $name .= ' ISIN ' . strtoupper($this->getInput('isin')); + } + } + + if ($this->getInput('lang')) { + $name .= ' (' . strtoupper($this->getInput('lang')) . ')'; + } + + return $name; + } + + #region Common + + /** + * Fixes dates depending on the choosen language: + * + * de : dd.mm.yy + * en : dd.mm.yy + * it : dd/mm/yy + * + * Basically strtotime doesn't convert dates correctly due to formats + * being hard to interpret. So we use the DateTime object, manually + * fixing dates and times (set to 00:00:00.000). + * + * We don't know the timezone, so just assume +00:00 (or whatever + * DateTime chooses) + */ + private function fixDate($date) + { + switch ($this->getInput('lang')) { + case 'en': + case 'de': + $df = date_create_from_format('d.m.y', $date); + break; + case 'it': + $df = date_create_from_format('d/m/y', $date); + break; + } + + date_time_set($df, 0, 0); + + // Debug::log(date_format($df, 'U')); + + return date_format($df, 'U'); + } + + private function extractImages($article) + { + // Notice: We can have zero or more images (though it should mostly be 1) + $elements = $article->find('img'); + + $images = []; + + foreach ($elements as $img) { + // Skip the logo (mostly provided part of a hidden div) + if (substr($img->src, strrpos($img->src, '/') + 1) === 'logo.png') { + continue; + } + + $images[] = $img->src; + } + + return $images; + } + + #endregion + + #region News + + private function collectNews($html) + { + $articles = $html->find('div.newsTopArticle') + or returnServerError('No articles found! Layout might have changed!'); + + foreach ($articles as $article) { + $item = []; + + // Common data + + $item['uri'] = $this->extractNewsUri($article); + $item['timestamp'] = $this->extractNewsDate($article); + $item['title'] = $this->extractNewsTitle($article); - // Debug::log(date_format($df, 'U')); + if ($this->getInput('full')) { + $uri = $this->extractNewsUri($article); - return date_format($df, 'U'); - } + $html = getSimpleHTMLDOMCached($uri) + or returnServerError('Failed loading full article from ' . $uri); - private function extractImages($article) { - // Notice: We can have zero or more images (though it should mostly be 1) - $elements = $article->find('img'); + $fullArticle = $html->find('div.article', 0) + or returnServerError('No content found! Layout might have changed!'); - $images = array(); + defaultLinkTo($fullArticle, static::URI); + + $item['author'] = $this->extractFullArticleAuthor($fullArticle); + $item['content'] = $this->extractFullArticleContent($fullArticle); + $item['enclosures'] = $this->extractImages($fullArticle); + } else { + $item['content'] = $this->extractNewsDescription($article); + $item['enclosures'] = $this->extractImages($article); + } - foreach($elements as $img) { - // Skip the logo (mostly provided part of a hidden div) - if(substr($img->src, strrpos($img->src, '/') + 1) === 'logo.png') - continue; + $this->items[] = $item; + } + } - $images[] = $img->src; - } + private function extractNewsUri($article) + { + $element = $article->find('a', 0) + or returnServerError('Anchor not found!'); - return $images; - } + return $element->href; + } - #endregion + private function extractNewsDate($article) + { + $element = $article->find('div.subheadline', 0) + or returnServerError('Date not found!'); - #region News + // Debug::log($element->plaintext); - private function collectNews($html) { - $articles = $html->find('div.newsTopArticle') - or returnServerError('No articles found! Layout might have changed!'); + $date = trim(explode('|', $element->plaintext)[0]); - foreach($articles as $article) { + return $this->fixDate($date); + } - $item = array(); + private function extractNewsDescription($article) + { + $element = $article->find('span.newsText', 0) + or returnServerError('Description not found!'); - // Common data + $element->find('a', 0)->onclick = ''; - $item['uri'] = $this->extractNewsUri($article); - $item['timestamp'] = $this->extractNewsDate($article); - $item['title'] = $this->extractNewsTitle($article); + // Debug::log($element->innertext); - if($this->getInput('full')) { + return $element->innertext; + } - $uri = $this->extractNewsUri($article); + private function extractNewsTitle($article) + { + $element = $article->find('h3', 0) + or returnServerError('Title not found!'); - $html = getSimpleHTMLDOMCached($uri) - or returnServerError('Failed loading full article from ' . $uri); + return $element->plaintext; + } - $fullArticle = $html->find('div.article', 0) - or returnServerError('No content found! Layout might have changed!'); + private function extractFullArticleContent($article) + { + $element = $article->find('div.article_body', 0) + or returnServerError('Article body not found!'); - defaultLinkTo($fullArticle, static::URI); + // Remove teaser image + $element->find('img.teaser-img', 0)->outertext = ''; - $item['author'] = $this->extractFullArticleAuthor($fullArticle); - $item['content'] = $this->extractFullArticleContent($fullArticle); - $item['enclosures'] = $this->extractImages($fullArticle); + // Remove self advertisements + foreach ($element->find('.call-action') as $adv) { + $adv->outertext = ''; + } - } else { + // Remove tips + foreach ($element->find('.panel-edu') as $tip) { + $tip->outertext = ''; + } - $item['content'] = $this->extractNewsDescription($article); - $item['enclosures'] = $this->extractImages($article); + // Remove inline scripts (used for i.e. interactive graphs) as they are + // rendered as a long series of strings + foreach ($element->find('script') as $script) { + $script->outertext = '[Content removed! Visit site to see full contents!]'; + } - } + return $element->innertext; + } - $this->items[] = $item; - } - } + private function extractFullArticleAuthor($article) + { + $element = $article->find('span[itemprop=name]', 0) + or returnServerError('Author not found!'); - private function extractNewsUri($article) { - $element = $article->find('a', 0) - or returnServerError('Anchor not found!'); + return $element->plaintext; + } - return $element->href; - } + #endregion - private function extractNewsDate($article) { - $element = $article->find('div.subheadline', 0) - or returnServerError('Date not found!'); + #region Profile - // Debug::log($element->plaintext); + private function collectProfile($html) + { + $item = []; - $date = trim(explode('|', $element->plaintext)[0]); + $item['uri'] = $this->getURI(); + $item['timestamp'] = $this->extractProfileDate($html); + $item['title'] = $this->extractProfiletitle($html); + $item['author'] = $this->extractProfileAuthor($html); + $item['content'] = $this->extractProfileContent($html); - return $this->fixDate($date); - } + $this->items[] = $item; + } - private function extractNewsDescription($article) { - $element = $article->find('span.newsText', 0) - or returnServerError('Description not found!'); + private function extractProfileDate($html) + { + $element = $html->find('div.infobox div.vallabel', 0) + or returnServerError('Date not found!'); - $element->find('a', 0)->onclick = ''; + // Debug::log($element->plaintext); - // Debug::log($element->innertext); + $date = trim(explode("\r\n", $element->plaintext)[1]); - return $element->innertext; - } + return $this->fixDate($date); + } - private function extractNewsTitle($article) { - $element = $article->find('h3', 0) - or returnServerError('Title not found!'); + private function extractProfileTitle($html) + { + $element = $html->find('span.h1', 0) + or returnServerError('Title not found!'); - return $element->plaintext; - } + return $element->plaintext; + } - private function extractFullArticleContent($article) { - $element = $article->find('div.article_body', 0) - or returnServerError('Article body not found!'); + private function extractProfileContent($html) + { + // There are a few thins we are interested: + // - Investment Strategy + // - Description + // - Quote - // Remove teaser image - $element->find('img.teaser-img', 0)->outertext = ''; + $strategy = $html->find('div.tab-container div.col-sm-6 p', 0) + or returnServerError('Investment Strategy not found!'); - // Remove self advertisements - foreach($element->find('.call-action') as $adv) { - $adv->outertext = ''; - } + // Description requires a bit of cleanup due to lack of propper identification - // Remove tips - foreach($element->find('.panel-edu') as $tip) { - $tip->outertext = ''; - } + $description = $html->find('div.headline', 5) + or returnServerError('Description container not found!'); - // Remove inline scripts (used for i.e. interactive graphs) as they are - // rendered as a long series of strings - foreach($element->find('script') as $script) { - $script->outertext = '[Content removed! Visit site to see full contents!]'; - } + $description = $description->parent(); - return $element->innertext; - } + foreach ($description->find('div') as $div) { + $div->outertext = ''; + } - private function extractFullArticleAuthor($article) { - $element = $article->find('span[itemprop=name]', 0) - or returnServerError('Author not found!'); + $quote = $html->find('div.infobox div.val', 0) + or returnServerError('Quote not found!'); - return $element->plaintext; - } + $quote_html = '<strong>Quote</strong><br><p>' . $quote . '</p>'; + $strategy_html = ''; + $description_html = ''; - #endregion + if ($this->getInput('strategy') === true) { + $strategy_html = '<strong>Strategy</strong><br><p>' . $strategy . '</p><br>'; + } - #region Profile + if ($this->getInput('description') === true) { + $description_html = '<strong>Description</strong><br><p>' . $description . '</p><br>'; + } - private function collectProfile($html) { - $item = array(); + return $strategy_html . $description_html . $quote_html; + } - $item['uri'] = $this->getURI(); - $item['timestamp'] = $this->extractProfileDate($html); - $item['title'] = $this->extractProfiletitle($html); - $item['author'] = $this->extractProfileAuthor($html); - $item['content'] = $this->extractProfileContent($html); + private function extractProfileAuthor($html) + { + // Use ISIN + WKN as author + // Notice: "identfier" is not a typo [sic]! + $element = $html->find('span.identfier', 0) + or returnServerError('Author not found!'); - $this->items[] = $item; - } + return $element->plaintext; + } - private function extractProfileDate($html) { - $element = $html->find('div.infobox div.vallabel', 0) - or returnServerError('Date not found!'); - - // Debug::log($element->plaintext); - - $date = trim(explode("\r\n", $element->plaintext)[1]); - - return $this->fixDate($date); - } - - private function extractProfileTitle($html) { - $element = $html->find('span.h1', 0) - or returnServerError('Title not found!'); - - return $element->plaintext; - } - - private function extractProfileContent($html) { - // There are a few thins we are interested: - // - Investment Strategy - // - Description - // - Quote - - $strategy = $html->find('div.tab-container div.col-sm-6 p', 0) - or returnServerError('Investment Strategy not found!'); - - // Description requires a bit of cleanup due to lack of propper identification - - $description = $html->find('div.headline', 5) - or returnServerError('Description container not found!'); - - $description = $description->parent(); - - foreach($description->find('div') as $div) { - $div->outertext = ''; - } - - $quote = $html->find('div.infobox div.val', 0) - or returnServerError('Quote not found!'); - - $quote_html = '<strong>Quote</strong><br><p>' . $quote . '</p>'; - $strategy_html = ''; - $description_html = ''; - - if($this->getInput('strategy') === true) { - $strategy_html = '<strong>Strategy</strong><br><p>' . $strategy . '</p><br>'; - } - - if($this->getInput('description') === true) { - $description_html = '<strong>Description</strong><br><p>' . $description . '</p><br>'; - } - - return $strategy_html . $description_html . $quote_html; - } - - private function extractProfileAuthor($html) { - // Use ISIN + WKN as author - // Notice: "identfier" is not a typo [sic]! - $element = $html->find('span.identfier', 0) - or returnServerError('Author not found!'); - - return $element->plaintext; - } - - #endregion + #endregion } diff --git a/bridges/Kanali6Bridge.php b/bridges/Kanali6Bridge.php index 267c7d5e..e3b7998d 100644 --- a/bridges/Kanali6Bridge.php +++ b/bridges/Kanali6Bridge.php @@ -1,20 +1,22 @@ <?php -class Kanali6Bridge extends XPathAbstract { - const NAME = 'Kanali6 Latest Podcasts'; - const DESCRIPTION = 'Returns the latest podcasts'; - const URI = 'https://kanali6.com.cy/mp3/TOC.html'; +class Kanali6Bridge extends XPathAbstract +{ + const NAME = 'Kanali6 Latest Podcasts'; + const DESCRIPTION = 'Returns the latest podcasts'; + const URI = 'https://kanali6.com.cy/mp3/TOC.html'; - const FEED_SOURCE_URL = 'https://kanali6.com.cy/mp3/TOC.xml'; - const XPATH_EXPRESSION_ITEM = '//recording[position() <= 50]'; - const XPATH_EXPRESSION_ITEM_TITLE = './title'; - const XPATH_EXPRESSION_ITEM_CONTENT = './durationvisual'; - const XPATH_EXPRESSION_ITEM_URI = './filename'; - const XPATH_EXPRESSION_ITEM_AUTHOR = './/producersname'; - const XPATH_EXPRESSION_ITEM_TIMESTAMP = './recfinisheddatetime'; - const XPATH_EXPRESSION_ITEM_ENCLOSURES = './filename'; + const FEED_SOURCE_URL = 'https://kanali6.com.cy/mp3/TOC.xml'; + const XPATH_EXPRESSION_ITEM = '//recording[position() <= 50]'; + const XPATH_EXPRESSION_ITEM_TITLE = './title'; + const XPATH_EXPRESSION_ITEM_CONTENT = './durationvisual'; + const XPATH_EXPRESSION_ITEM_URI = './filename'; + const XPATH_EXPRESSION_ITEM_AUTHOR = './/producersname'; + const XPATH_EXPRESSION_ITEM_TIMESTAMP = './recfinisheddatetime'; + const XPATH_EXPRESSION_ITEM_ENCLOSURES = './filename'; - public function getURI() { - return self::URI; - } + public function getURI() + { + return self::URI; + } } diff --git a/bridges/KernelBugTrackerBridge.php b/bridges/KernelBugTrackerBridge.php index 2677d717..02d31cff 100644 --- a/bridges/KernelBugTrackerBridge.php +++ b/bridges/KernelBugTrackerBridge.php @@ -1,147 +1,158 @@ <?php -class KernelBugTrackerBridge extends BridgeAbstract { - const NAME = 'Kernel Bug Tracker'; - const URI = 'https://bugzilla.kernel.org'; - const DESCRIPTION = 'DEPRECATED: Use BugzillaBridge instead. +class KernelBugTrackerBridge extends BridgeAbstract +{ + const NAME = 'Kernel Bug Tracker'; + const URI = 'https://bugzilla.kernel.org'; + const DESCRIPTION = 'DEPRECATED: Use BugzillaBridge instead. Returns feeds for bug comments'; - const MAINTAINER = 'logmanoriginal'; - const PARAMETERS = array( - 'Bug comments' => array( - 'id' => array( - 'name' => 'Bug tracking ID', - 'type' => 'number', - 'required' => true, - 'title' => 'Insert bug tracking ID', - 'exampleValue' => 121241 - ), - 'limit' => array( - 'name' => 'Number of comments to return', - 'type' => 'number', - 'required' => false, - 'title' => 'Specify number of comments to return', - 'defaultValue' => -1 - ), - 'sorting' => array( - 'name' => 'Sorting', - 'type' => 'list', - 'required' => false, - 'title' => 'Defines the sorting order of the comments returned', - 'defaultValue' => 'of', - 'values' => array( - 'Oldest first' => 'of', - 'Latest first' => 'lf' - ) - ) - ) - ); - - private $bugid = ''; - private $bugdesc = ''; - - public function getIcon() { - return self::URI . '/images/favicon.ico'; - } - - public function collectData(){ - $limit = $this->getInput('limit'); - $sorting = $this->getInput('sorting'); - - // We use the print preview page for simplicity - $html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple', - 86400, - null, - null, - true, - true, - DEFAULT_TARGET_CHARSET, - false, // Do NOT remove line breaks - DEFAULT_BR_TEXT, - DEFAULT_SPAN_TEXT); - - if($html === false) - returnServerError('Failed to load page!'); - - $html = defaultLinkTo($html, self::URI); - - // Store header information into private members - $this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext; - $this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext; - - // Get and limit comments - $comments = $html->find('div.bz_comment'); - - if($limit > 0 && count($comments) > $limit) { - $comments = array_slice($comments, count($comments) - $limit, $limit); - } - - // Order comments - switch($sorting) { - case 'lf': $comments = array_reverse($comments, true); - // fall-through - case 'of': - // fall-through - default: // Nothing to do, keep original order - } - - foreach($comments as $comment) { - $comment = $this->inlineStyles($comment); - - $item = array(); - $item['uri'] = $this->getURI() . '#' . $comment->id; - $item['author'] = $comment->find('span.bz_comment_user', 0)->innertext; - $item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext; - $item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext); - $item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext; - - // Fix line breaks (they use LF) - $item['content'] = str_replace("\n", '<br>', $item['content']); - - // Fix relative URIs - $item['content'] = $item['content']; - - $this->items[] = $item; - } - - } - - public function getURI(){ - switch($this->queriedContext) { - case 'Bug comments': - return parent::getURI() - . '/show_bug.cgi?id=' - . $this->getInput('id'); - break; - default: return parent::getURI(); - } - } - - public function getName(){ - switch($this->queriedContext) { - case 'Bug comments': - return 'Bug ' - . $this->bugid - . ' tracker for ' - . $this->bugdesc - . ' - ' - . parent::getName(); - break; - default: return parent::getName(); - } - } - - /** - * Adds styles as attributes to tags with known classes - * - * @param object $html A simplehtmldom object - * @return object Returns the original object with styles added as - * attributes. - */ - private function inlineStyles($html){ - foreach($html->find('.bz_obsolete') as $element) { - $element->style = 'text-decoration:line-through;'; - } - - return $html; - } + const MAINTAINER = 'logmanoriginal'; + const PARAMETERS = [ + 'Bug comments' => [ + 'id' => [ + 'name' => 'Bug tracking ID', + 'type' => 'number', + 'required' => true, + 'title' => 'Insert bug tracking ID', + 'exampleValue' => 121241 + ], + 'limit' => [ + 'name' => 'Number of comments to return', + 'type' => 'number', + 'required' => false, + 'title' => 'Specify number of comments to return', + 'defaultValue' => -1 + ], + 'sorting' => [ + 'name' => 'Sorting', + 'type' => 'list', + 'required' => false, + 'title' => 'Defines the sorting order of the comments returned', + 'defaultValue' => 'of', + 'values' => [ + 'Oldest first' => 'of', + 'Latest first' => 'lf' + ] + ] + ] + ]; + + private $bugid = ''; + private $bugdesc = ''; + + public function getIcon() + { + return self::URI . '/images/favicon.ico'; + } + + public function collectData() + { + $limit = $this->getInput('limit'); + $sorting = $this->getInput('sorting'); + + // We use the print preview page for simplicity + $html = getSimpleHTMLDOMCached( + $this->getURI() . '&format=multiple', + 86400, + null, + null, + true, + true, + DEFAULT_TARGET_CHARSET, + false, // Do NOT remove line breaks + DEFAULT_BR_TEXT, + DEFAULT_SPAN_TEXT + ); + + if ($html === false) { + returnServerError('Failed to load page!'); + } + + $html = defaultLinkTo($html, self::URI); + + // Store header information into private members + $this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext; + $this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext; + + // Get and limit comments + $comments = $html->find('div.bz_comment'); + + if ($limit > 0 && count($comments) > $limit) { + $comments = array_slice($comments, count($comments) - $limit, $limit); + } + + // Order comments + switch ($sorting) { + case 'lf': + $comments = array_reverse($comments, true); + // fall-through + case 'of': + // fall-through + default: // Nothing to do, keep original order + } + + foreach ($comments as $comment) { + $comment = $this->inlineStyles($comment); + + $item = []; + $item['uri'] = $this->getURI() . '#' . $comment->id; + $item['author'] = $comment->find('span.bz_comment_user', 0)->innertext; + $item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext; + $item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext); + $item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext; + + // Fix line breaks (they use LF) + $item['content'] = str_replace("\n", '<br>', $item['content']); + + // Fix relative URIs + $item['content'] = $item['content']; + + $this->items[] = $item; + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Bug comments': + return parent::getURI() + . '/show_bug.cgi?id=' + . $this->getInput('id'); + break; + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Bug comments': + return 'Bug ' + . $this->bugid + . ' tracker for ' + . $this->bugdesc + . ' - ' + . parent::getName(); + break; + default: + return parent::getName(); + } + } + + /** + * Adds styles as attributes to tags with known classes + * + * @param object $html A simplehtmldom object + * @return object Returns the original object with styles added as + * attributes. + */ + private function inlineStyles($html) + { + foreach ($html->find('.bz_obsolete') as $element) { + $element->style = 'text-decoration:line-through;'; + } + + return $html; + } } diff --git a/bridges/KhinsiderBridge.php b/bridges/KhinsiderBridge.php index 73c297cd..8493eadc 100644 --- a/bridges/KhinsiderBridge.php +++ b/bridges/KhinsiderBridge.php @@ -2,40 +2,40 @@ class KhinsiderBridge extends BridgeAbstract { - const MAINTAINER = 'Chouchenos'; - const NAME = 'Khinsider'; - const URI = 'https://downloads.khinsider.com/'; - const CACHE_TIMEOUT = 14400; // 4 h - const DESCRIPTION = 'Fetch daily game OST from Khinsider'; + const MAINTAINER = 'Chouchenos'; + const NAME = 'Khinsider'; + const URI = 'https://downloads.khinsider.com/'; + const CACHE_TIMEOUT = 14400; // 4 h + const DESCRIPTION = 'Fetch daily game OST from Khinsider'; - public function collectData() - { - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $dates = $html->find('.latestSoundtrackHeading'); - $tables = $html->find('.albumList'); - // $dates is empty - foreach ($dates as $i => $date) { - $item = array(); - $item['uri'] = self::URI; - $item['timestamp'] = DateTime::createFromFormat('F jS, Y', $date->plaintext)->setTime(1, 1)->format('U'); - $item['title'] = sprintf('OST for %s', $date->plaintext); - $item['author'] = 'Khinsider'; - $trs = $tables[$i]->find('tr'); - $content = '<ul>'; - foreach ($trs as $tr) { - $td = $tr->find('td', 1); - if (null !== $td) { - $link = $td->find('a', 0); - $content .= sprintf('<li><a href="%s">%s</a></li>', $link->href, $link->plaintext); - } - } - $content .= '</ul>'; - $item['content'] = $content; - $item['uid'] = $item['timestamp']; - $item['categories'] = array('Video games', 'Music', 'OST', 'download'); + $dates = $html->find('.latestSoundtrackHeading'); + $tables = $html->find('.albumList'); + // $dates is empty + foreach ($dates as $i => $date) { + $item = []; + $item['uri'] = self::URI; + $item['timestamp'] = DateTime::createFromFormat('F jS, Y', $date->plaintext)->setTime(1, 1)->format('U'); + $item['title'] = sprintf('OST for %s', $date->plaintext); + $item['author'] = 'Khinsider'; + $trs = $tables[$i]->find('tr'); + $content = '<ul>'; + foreach ($trs as $tr) { + $td = $tr->find('td', 1); + if (null !== $td) { + $link = $td->find('a', 0); + $content .= sprintf('<li><a href="%s">%s</a></li>', $link->href, $link->plaintext); + } + } + $content .= '</ul>'; + $item['content'] = $content; + $item['uid'] = $item['timestamp']; + $item['categories'] = ['Video games', 'Music', 'OST', 'download']; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/KilledbyGoogleBridge.php b/bridges/KilledbyGoogleBridge.php index dc9d33f7..54c5b59f 100644 --- a/bridges/KilledbyGoogleBridge.php +++ b/bridges/KilledbyGoogleBridge.php @@ -1,78 +1,81 @@ <?php -class KilledbyGoogleBridge extends BridgeAbstract { - const NAME = 'Killed by Google Bridge'; - const URI = 'https://killedbygoogle.com'; - const DESCRIPTION = 'Returns list of recently discontinued Google services, products, devices, and apps.'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(); - const CACHE_TIMEOUT = 3600; - - public function collectData() { - - $json = getContents(self::URI . '/graveyard.json') - or returnServerError('Could not request: ' . self::URI . '/graveyard.json'); - - $this->handleJson($json); - $this->orderItems(); - $this->limitItems(); - } - - /** - * Handle JSON - */ - private function handleJson($json) { - - $graveyard = json_decode($json, true); - - foreach($graveyard as $tombstone) { - $item = array(); - - $openDate = new DateTime($tombstone['dateOpen']); - $closeDate = new DateTime($tombstone['dateClose']); - $currentDate = new DateTime(); - - $yearOpened = $openDate->format('Y'); - $yearClosed = $closeDate->format('Y'); - - if ($closeDate > $currentDate) { - continue; - } - - $item['title'] = $tombstone['name'] . ' (' . $yearOpened . ' - ' . $yearClosed . ')'; - $item['uid'] = $tombstone['slug']; - $item['uri'] = $tombstone['link']; - $item['timestamp'] = strtotime($tombstone['dateClose']); - - $item['content'] = <<<EOD +class KilledbyGoogleBridge extends BridgeAbstract +{ + const NAME = 'Killed by Google Bridge'; + const URI = 'https://killedbygoogle.com'; + const DESCRIPTION = 'Returns list of recently discontinued Google services, products, devices, and apps.'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = []; + + const CACHE_TIMEOUT = 3600; + + public function collectData() + { + $json = getContents(self::URI . '/graveyard.json') + or returnServerError('Could not request: ' . self::URI . '/graveyard.json'); + + $this->handleJson($json); + $this->orderItems(); + $this->limitItems(); + } + + /** + * Handle JSON + */ + private function handleJson($json) + { + $graveyard = json_decode($json, true); + + foreach ($graveyard as $tombstone) { + $item = []; + + $openDate = new DateTime($tombstone['dateOpen']); + $closeDate = new DateTime($tombstone['dateClose']); + $currentDate = new DateTime(); + + $yearOpened = $openDate->format('Y'); + $yearClosed = $closeDate->format('Y'); + + if ($closeDate > $currentDate) { + continue; + } + + $item['title'] = $tombstone['name'] . ' (' . $yearOpened . ' - ' . $yearClosed . ')'; + $item['uid'] = $tombstone['slug']; + $item['uri'] = $tombstone['link']; + $item['timestamp'] = strtotime($tombstone['dateClose']); + + $item['content'] = <<<EOD <p>{$tombstone['description']}</p><p><a href="{$tombstone['link']}">{$tombstone['link']}</a></p> EOD; - $item['enclosures'][] = 'https://static.killedbygoogle.com/com/tombstone.svg'; - - $this->items[] = $item; - } - } - - /** - * Order items by timestamp - */ - private function orderItems() { - - $sort = array(); - - foreach ($this->items as $key => $item) { - $sort[$key] = $item['timestamp']; - } - - array_multisort($sort, SORT_DESC, $this->items); - $this->items = array_slice($this->items, 0, 15); - } - - /** - * Limit items to 15 - */ - private function limitItems() { - $this->items = array_slice($this->items, 0, 15); - } + $item['enclosures'][] = 'https://static.killedbygoogle.com/com/tombstone.svg'; + + $this->items[] = $item; + } + } + + /** + * Order items by timestamp + */ + private function orderItems() + { + $sort = []; + + foreach ($this->items as $key => $item) { + $sort[$key] = $item['timestamp']; + } + + array_multisort($sort, SORT_DESC, $this->items); + $this->items = array_slice($this->items, 0, 15); + } + + /** + * Limit items to 15 + */ + private function limitItems() + { + $this->items = array_slice($this->items, 0, 15); + } } diff --git a/bridges/KonachanBridge.php b/bridges/KonachanBridge.php index db6c0767..afddf9ca 100644 --- a/bridges/KonachanBridge.php +++ b/bridges/KonachanBridge.php @@ -1,10 +1,9 @@ <?php -class KonachanBridge extends MoebooruBridge { - - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Konachan'; - const URI = 'https://konachan.com/'; - const DESCRIPTION = 'Returns images from given page'; - +class KonachanBridge extends MoebooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Konachan'; + const URI = 'https://konachan.com/'; + const DESCRIPTION = 'Returns images from given page'; } diff --git a/bridges/KoreusBridge.php b/bridges/KoreusBridge.php index 4cfb8c21..874c2c92 100644 --- a/bridges/KoreusBridge.php +++ b/bridges/KoreusBridge.php @@ -1,22 +1,25 @@ <?php -class KoreusBridge extends FeedExpander { - const MAINTAINER = 'pit-fgfjiudghdf'; - const NAME = 'Koreus'; - const URI = 'https://www.koreus.com/'; - const DESCRIPTION = 'Returns the newest posts from Koreus (full text)'; +class KoreusBridge extends FeedExpander +{ + const MAINTAINER = 'pit-fgfjiudghdf'; + const NAME = 'Koreus'; + const URI = 'https://www.koreus.com/'; + const DESCRIPTION = 'Returns the newest posts from Koreus (full text)'; - protected function parseItem($item){ - $item = parent::parseItem($item); + protected function parseItem($item) + { + $item = parent::parseItem($item); - $html = getSimpleHTMLDOMCached($item['uri']); - $text = $html->find('p.itemText', 0)->innertext; - $item['content'] = utf8_encode($text); + $html = getSimpleHTMLDOMCached($item['uri']); + $text = $html->find('p.itemText', 0)->innertext; + $item['content'] = utf8_encode($text); - return $item; - } + return $item; + } - public function collectData(){ - $this->collectExpandableDatas('https://feeds.feedburner.com/Koreus-articles'); - } + public function collectData() + { + $this->collectExpandableDatas('https://feeds.feedburner.com/Koreus-articles'); + } } diff --git a/bridges/KununuBridge.php b/bridges/KununuBridge.php index 4352ab26..e1b228dc 100644 --- a/bridges/KununuBridge.php +++ b/bridges/KununuBridge.php @@ -1,147 +1,156 @@ <?php -class KununuBridge extends BridgeAbstract { - const MAINTAINER = 'logmanoriginal'; - const NAME = 'Kununu Bridge'; - const URI = 'https://www.kununu.com/'; - const CACHE_TIMEOUT = 86400; // 24h - const DESCRIPTION = 'Returns the latest reviews for a company and site of your choice.'; - - const PARAMETERS = array( - 'global' => array( - 'site' => array( - 'name' => 'Site', - 'type' => 'list', - 'title' => 'Select your site', - 'values' => array( - 'Austria' => 'at', - 'Germany' => 'de', - 'Switzerland' => 'ch' - ), - 'exampleValue' => 'de', - ), - 'include_ratings' => array( - 'name' => 'Include ratings', - 'type' => 'checkbox', - 'title' => 'Activate to include ratings in the feed' - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'defaultValue' => 3, - 'title' => "Maximum number of items to return in the feed.\n0 = unlimited" - ) - ), - array( - 'company' => array( - 'name' => 'Company', - 'required' => true, - 'exampleValue' => 'kununu', - 'title' => 'Insert company name (i.e. Kununu) or URI path (i.e. kununu)' - ) - ) - ); - - private $companyName = ''; - - public function getURI() { - if(!is_null($this->getInput('company')) && !is_null($this->getInput('site'))) { - - $company = $this->fixCompanyName($this->getInput('company')); - $site = $this->getInput('site'); - - return sprintf('%s%s/%s', self::URI, $site, $company); - } - - return parent::getURI(); - } - - public function getName() { - if(!is_null($this->getInput('company'))) { - $company = $this->fixCompanyName($this->getInput('company')); - return ($this->companyName ?: $company) . ' - ' . self::NAME; - } - - return parent::getName(); - } - - public function getIcon() { - return 'https://www.kununu.com/favicon-196x196.png'; - } - - public function collectData(){ - $full = $this->getInput('full'); - - // Load page - $json = json_decode(getContents($this->getAPI()), true); - $this->companyName = $json['common']['name']; - $baseURI = $this->getURI() . '/bewertung/'; - - $limit = $this->getInput('limit') ?: 0; - - // Go through all articles - foreach($json['reviews'] as $review) { - $item = array(); - $item['author'] = $review['position'] . ' / ' . $review['department']; - $item['timestamp'] = $review['createdAt']; - $item['title'] = $review['roundedScore'] . ' : ' . $review['title']; - $item['uri'] = $baseURI . $review['uuid']; - $item['content'] = $this->extractArticleDescription($review); - $this->items[] = $item; - - if ($limit > 0 && count($this->items) >= $limit) break; - - } - } - - /** - * Returns JSON API url - */ - private function getAPI() { - $company = $this->fixCompanyName($this->getInput('company')); - $site = $this->getInput('site'); - - return self::URI . 'middlewares/profiles/' . - $site . '/' . $company . - '/reviews?reviewType=employees&urlParams=sort=newest&sort=newest&page=1'; - } - - /* - * Returns a fixed version of the provided company name - */ - private function fixCompanyName($company){ - $company = trim($company); - $company = str_replace(' ', '-', $company); - $company = strtolower($company); - - $umlauts = Array('/ä/','/ö/','/ü/','/Ä/','/Ö/','/Ü/','/ß/'); - $replace = Array('ae','oe','ue','Ae','Oe','Ue','ss'); - - return preg_replace($umlauts, $replace, $company); - } - - /** - * Returns the description from a given article - */ - private function extractArticleDescription($json){ - $retVal = ''; - foreach($json['texts'] as $text) { - $retVal .= '<h4>' . $text['id'] . '</h4><p>' . $text['text'] . '</p>'; - } - - if($this->getInput('include_ratings') && !empty($json['ratings'])) { - $retVal .= (empty($retVal) ? '' : '<hr>') . '<table>'; - foreach($json['ratings'] as $rating) { - $retVal .= <<<EOD + +class KununuBridge extends BridgeAbstract +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'Kununu Bridge'; + const URI = 'https://www.kununu.com/'; + const CACHE_TIMEOUT = 86400; // 24h + const DESCRIPTION = 'Returns the latest reviews for a company and site of your choice.'; + + const PARAMETERS = [ + 'global' => [ + 'site' => [ + 'name' => 'Site', + 'type' => 'list', + 'title' => 'Select your site', + 'values' => [ + 'Austria' => 'at', + 'Germany' => 'de', + 'Switzerland' => 'ch' + ], + 'exampleValue' => 'de', + ], + 'include_ratings' => [ + 'name' => 'Include ratings', + 'type' => 'checkbox', + 'title' => 'Activate to include ratings in the feed' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'defaultValue' => 3, + 'title' => "Maximum number of items to return in the feed.\n0 = unlimited" + ] + ], + [ + 'company' => [ + 'name' => 'Company', + 'required' => true, + 'exampleValue' => 'kununu', + 'title' => 'Insert company name (i.e. Kununu) or URI path (i.e. kununu)' + ] + ] + ]; + + private $companyName = ''; + + public function getURI() + { + if (!is_null($this->getInput('company')) && !is_null($this->getInput('site'))) { + $company = $this->fixCompanyName($this->getInput('company')); + $site = $this->getInput('site'); + + return sprintf('%s%s/%s', self::URI, $site, $company); + } + + return parent::getURI(); + } + + public function getName() + { + if (!is_null($this->getInput('company'))) { + $company = $this->fixCompanyName($this->getInput('company')); + return ($this->companyName ?: $company) . ' - ' . self::NAME; + } + + return parent::getName(); + } + + public function getIcon() + { + return 'https://www.kununu.com/favicon-196x196.png'; + } + + public function collectData() + { + $full = $this->getInput('full'); + + // Load page + $json = json_decode(getContents($this->getAPI()), true); + $this->companyName = $json['common']['name']; + $baseURI = $this->getURI() . '/bewertung/'; + + $limit = $this->getInput('limit') ?: 0; + + // Go through all articles + foreach ($json['reviews'] as $review) { + $item = []; + $item['author'] = $review['position'] . ' / ' . $review['department']; + $item['timestamp'] = $review['createdAt']; + $item['title'] = $review['roundedScore'] . ' : ' . $review['title']; + $item['uri'] = $baseURI . $review['uuid']; + $item['content'] = $this->extractArticleDescription($review); + $this->items[] = $item; + + if ($limit > 0 && count($this->items) >= $limit) { + break; + } + } + } + + /** + * Returns JSON API url + */ + private function getAPI() + { + $company = $this->fixCompanyName($this->getInput('company')); + $site = $this->getInput('site'); + + return self::URI . 'middlewares/profiles/' . + $site . '/' . $company . + '/reviews?reviewType=employees&urlParams=sort=newest&sort=newest&page=1'; + } + + /* + * Returns a fixed version of the provided company name + */ + private function fixCompanyName($company) + { + $company = trim($company); + $company = str_replace(' ', '-', $company); + $company = strtolower($company); + + $umlauts = ['/ä/','/ö/','/ü/','/Ä/','/Ö/','/Ü/','/ß/']; + $replace = ['ae','oe','ue','Ae','Oe','Ue','ss']; + + return preg_replace($umlauts, $replace, $company); + } + + /** + * Returns the description from a given article + */ + private function extractArticleDescription($json) + { + $retVal = ''; + foreach ($json['texts'] as $text) { + $retVal .= '<h4>' . $text['id'] . '</h4><p>' . $text['text'] . '</p>'; + } + + if ($this->getInput('include_ratings') && !empty($json['ratings'])) { + $retVal .= (empty($retVal) ? '' : '<hr>') . '<table>'; + foreach ($json['ratings'] as $rating) { + $retVal .= <<<EOD <tr> <td>{$rating['id']} <td>{$rating['roundedScore']} <td>{$rating['text']} </tr> EOD; - } - $retVal .= '</table>'; - } + } + $retVal .= '</table>'; + } - return $retVal; - } + return $retVal; + } } diff --git a/bridges/LWNprevBridge.php b/bridges/LWNprevBridge.php index 40b1b129..358f841a 100644 --- a/bridges/LWNprevBridge.php +++ b/bridges/LWNprevBridge.php @@ -1,266 +1,278 @@ <?php -class LWNprevBridge extends BridgeAbstract{ - const MAINTAINER = 'Pierre Mazière'; - const NAME = 'LWN Free Weekly Edition'; - const URI = 'https://lwn.net/'; - const CACHE_TIMEOUT = 604800; // 1 week - const DESCRIPTION = 'LWN Free Weekly Edition available one week late'; - - private $editionTimeStamp; - - public function getURI(){ - return self::URI . 'free/bigpage'; - } - - private function jumpToNextTag(&$node){ - while($node && $node->nodeType === XML_TEXT_NODE) { - $nextNode = $node->nextSibling; - if(!$nextNode) { - break; - } - $node = $nextNode; - } - } - - private function jumpToPreviousTag(&$node){ - while($node && $node->nodeType === XML_TEXT_NODE) { - $previousNode = $node->previousSibling; - if(!$previousNode) { - break; - } - $node = $previousNode; - } - } - - public function collectData(){ - // Because the LWN page is written in loose HTML and not XHTML, - // Simple HTML Dom is not accurate enough for the job - $content = getContents($this->getURI()); - - $contents = explode('<b>Page editor</b>', $content); - - foreach($contents as $content) { - if(strpos($content, '<html>') === false) { - $content = <<<EOD + +class LWNprevBridge extends BridgeAbstract +{ + const MAINTAINER = 'Pierre Mazière'; + const NAME = 'LWN Free Weekly Edition'; + const URI = 'https://lwn.net/'; + const CACHE_TIMEOUT = 604800; // 1 week + const DESCRIPTION = 'LWN Free Weekly Edition available one week late'; + + private $editionTimeStamp; + + public function getURI() + { + return self::URI . 'free/bigpage'; + } + + private function jumpToNextTag(&$node) + { + while ($node && $node->nodeType === XML_TEXT_NODE) { + $nextNode = $node->nextSibling; + if (!$nextNode) { + break; + } + $node = $nextNode; + } + } + + private function jumpToPreviousTag(&$node) + { + while ($node && $node->nodeType === XML_TEXT_NODE) { + $previousNode = $node->previousSibling; + if (!$previousNode) { + break; + } + $node = $previousNode; + } + } + + public function collectData() + { + // Because the LWN page is written in loose HTML and not XHTML, + // Simple HTML Dom is not accurate enough for the job + $content = getContents($this->getURI()); + + $contents = explode('<b>Page editor</b>', $content); + + foreach ($contents as $content) { + if (strpos($content, '<html>') === false) { + $content = <<<EOD <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html><head><title>LWN</title></head><body>{$content}</body></html> EOD; - } else { - $content = $content . '</body></html>'; - } - - libxml_use_internal_errors(true); - $html = new DOMDocument(); - $html->loadHTML($content); - libxml_clear_errors(); - - $edition = $html->getElementsByTagName('h1'); - if($edition->length !== 0) { - $text = $edition->item(0)->textContent; - $this->editionTimeStamp = strtotime( - substr($text, strpos($text, 'for ') + strlen('for ')) - ); - } - - if(strpos($content, 'Cat1HL') === false) { - $items = $this->getFeatureContents($html); - } elseif(strpos($content, 'Cat3HL') === false) { - $items = $this->getBriefItems($html); - } else { - $items = $this->getAnnouncements($html); - } - - $this->items = array_merge($this->items, $items); - } - } - - private function getArticleContent(&$title){ - $link = $title->firstChild; - $this->jumpToNextTag($link); - $item['uri'] = self::URI; - if($link->nodeName === 'a') { - $item['uri'] .= $link->getAttribute('href'); - } - - $item['timestamp'] = $this->editionTimeStamp; - - $node = $title; - $content = ''; - $contentEnd = false; - while(!$contentEnd) { - $node = $node->nextSibling; - if(!$node || ( - $node->nodeType !== XML_TEXT_NODE && - $node->nodeName === 'h2' || ( - !is_null($node->attributes) && - !is_null($class = $node->attributes->getNamedItem('class')) && - in_array($class->nodeValue, array('Cat1HL','Cat2HL')) - ) - ) - ) { - $contentEnd = true; - } else { - $content .= $node->C14N(); - } - } - $item['content'] = $content; - return $item; - } - - private function getFeatureContents(&$html){ - $items = array(); - foreach($html->getElementsByTagName('h3') as $title) { - if($title->getAttribute('class') !== 'SummaryHL') { - continue; - } - - $item = array(); - - $author = $title->nextSibling; - $this->jumpToNextTag($author); - if($author->getAttribute('class') === 'FeatureByline') { - $item['author'] = $author->getElementsByTagName('b')->item(0)->textContent; - } else { - continue; - } - - $item['title'] = $title->textContent; - - $items[] = array_merge($item, $this->getArticleContent($title)); - } - return $items; - } - - private function getItemPrefix(&$cat, &$cats){ - $cat1 = ''; - $cat2 = ''; - $cat3 = ''; - switch($cat->getAttribute('class')) { - case 'Cat3HL': - $cat3 = $cat->textContent; - $cat = $cat->previousSibling; - $this->jumpToPreviousTag($cat); - $cats[2] = $cat3; - if($cat->getAttribute('class') !== 'Cat2HL') { - break; - } - // fall-through? Looks like a bug - case 'Cat2HL': - $cat2 = $cat->textContent; - $cat = $cat->previousSibling; - $this->jumpToPreviousTag($cat); - $cats[1] = $cat2; - if(empty($cat3)) { - $cats[2] = ''; - } - if($cat->getAttribute('class') !== 'Cat1HL') { - break; - } - // fall-through? Looks like a bug - case 'Cat1HL': - $cat1 = $cat->textContent; - $cats[0] = $cat1; - if(empty($cat3)) { - $cats[2] = ''; - } - if(empty($cat2)) { - $cats[1] = ''; - } - break; - default: - break; - } - - $prefix = ''; - if(!empty($cats[0])) { - $prefix .= '[' . $cats[0] . ($cats[1] ? '/' . $cats[1] : '') . '] '; - } - return $prefix; - } - - private function getAnnouncements(&$html){ - $items = array(); - $cats = array('','',''); - - foreach($html->getElementsByTagName('p') as $newsletters) { - if($newsletters->getAttribute('class') !== 'Cat3HL') { - continue; - } - - $item = array(); - - $item['uri'] = self::URI . '#' . count($items); - - $item['timestamp'] = $this->editionTimeStamp; - - $item['author'] = 'LWN'; - - $cat = $newsletters->previousSibling; - $this->jumpToPreviousTag($cat); - $prefix = $this->getItemPrefix($cat, $cats); - $item['title'] = $prefix . ' ' . $newsletters->textContent; - - $node = $newsletters; - $content = ''; - $contentEnd = false; - while(!$contentEnd) { - $node = $node->nextSibling; - if(!$node || ( - $node->nodeType !== XML_TEXT_NODE && ( - !is_null($node->attributes) && - !is_null($class = $node->attributes->getNamedItem('class')) && - in_array($class->nodeValue, array('Cat1HL','Cat2HL','Cat3HL')) - ) - ) - ) { - $contentEnd = true; - } else { - $content .= $node->C14N(); - } - } - $item['content'] = $content; - $items[] = $item; - } - - foreach($html->getElementsByTagName('h2') as $title) { - if($title->getAttribute('class') !== 'SummaryHL') { - continue; - } - - $item = array(); - - $cat = $title->previousSibling; - $this->jumpToPreviousTag($cat); - $cat = $cat->previousSibling; - $this->jumpToPreviousTag($cat); - $prefix = $this->getItemPrefix($cat, $cats); - $item['title'] = $prefix . ' ' . $title->textContent; - $items[] = array_merge($item, $this->getArticleContent($title)); - } - - return $items; - } - - private function getBriefItems(&$html){ - $items = array(); - $cats = array('','',''); - foreach($html->getElementsByTagName('h2') as $title) { - if($title->getAttribute('class') !== 'SummaryHL') { - continue; - } - - $item = array(); - - $cat = $title->previousSibling; - $this->jumpToPreviousTag($cat); - $cat = $cat->previousSibling; - $this->jumpToPreviousTag($cat); - $prefix = $this->getItemPrefix($cat, $cats); - $item['title'] = $prefix . ' ' . $title->textContent; - $items[] = array_merge($item, $this->getArticleContent($title)); - } - - return $items; - } + } else { + $content = $content . '</body></html>'; + } + + libxml_use_internal_errors(true); + $html = new DOMDocument(); + $html->loadHTML($content); + libxml_clear_errors(); + + $edition = $html->getElementsByTagName('h1'); + if ($edition->length !== 0) { + $text = $edition->item(0)->textContent; + $this->editionTimeStamp = strtotime( + substr($text, strpos($text, 'for ') + strlen('for ')) + ); + } + + if (strpos($content, 'Cat1HL') === false) { + $items = $this->getFeatureContents($html); + } elseif (strpos($content, 'Cat3HL') === false) { + $items = $this->getBriefItems($html); + } else { + $items = $this->getAnnouncements($html); + } + + $this->items = array_merge($this->items, $items); + } + } + + private function getArticleContent(&$title) + { + $link = $title->firstChild; + $this->jumpToNextTag($link); + $item['uri'] = self::URI; + if ($link->nodeName === 'a') { + $item['uri'] .= $link->getAttribute('href'); + } + + $item['timestamp'] = $this->editionTimeStamp; + + $node = $title; + $content = ''; + $contentEnd = false; + while (!$contentEnd) { + $node = $node->nextSibling; + if ( + !$node || ( + $node->nodeType !== XML_TEXT_NODE && + $node->nodeName === 'h2' || ( + !is_null($node->attributes) && + !is_null($class = $node->attributes->getNamedItem('class')) && + in_array($class->nodeValue, ['Cat1HL','Cat2HL']) + ) + ) + ) { + $contentEnd = true; + } else { + $content .= $node->C14N(); + } + } + $item['content'] = $content; + return $item; + } + + private function getFeatureContents(&$html) + { + $items = []; + foreach ($html->getElementsByTagName('h3') as $title) { + if ($title->getAttribute('class') !== 'SummaryHL') { + continue; + } + + $item = []; + + $author = $title->nextSibling; + $this->jumpToNextTag($author); + if ($author->getAttribute('class') === 'FeatureByline') { + $item['author'] = $author->getElementsByTagName('b')->item(0)->textContent; + } else { + continue; + } + + $item['title'] = $title->textContent; + + $items[] = array_merge($item, $this->getArticleContent($title)); + } + return $items; + } + + private function getItemPrefix(&$cat, &$cats) + { + $cat1 = ''; + $cat2 = ''; + $cat3 = ''; + switch ($cat->getAttribute('class')) { + case 'Cat3HL': + $cat3 = $cat->textContent; + $cat = $cat->previousSibling; + $this->jumpToPreviousTag($cat); + $cats[2] = $cat3; + if ($cat->getAttribute('class') !== 'Cat2HL') { + break; + } + // fall-through? Looks like a bug + case 'Cat2HL': + $cat2 = $cat->textContent; + $cat = $cat->previousSibling; + $this->jumpToPreviousTag($cat); + $cats[1] = $cat2; + if (empty($cat3)) { + $cats[2] = ''; + } + if ($cat->getAttribute('class') !== 'Cat1HL') { + break; + } + // fall-through? Looks like a bug + case 'Cat1HL': + $cat1 = $cat->textContent; + $cats[0] = $cat1; + if (empty($cat3)) { + $cats[2] = ''; + } + if (empty($cat2)) { + $cats[1] = ''; + } + break; + default: + break; + } + + $prefix = ''; + if (!empty($cats[0])) { + $prefix .= '[' . $cats[0] . ($cats[1] ? '/' . $cats[1] : '') . '] '; + } + return $prefix; + } + + private function getAnnouncements(&$html) + { + $items = []; + $cats = ['','','']; + + foreach ($html->getElementsByTagName('p') as $newsletters) { + if ($newsletters->getAttribute('class') !== 'Cat3HL') { + continue; + } + + $item = []; + + $item['uri'] = self::URI . '#' . count($items); + + $item['timestamp'] = $this->editionTimeStamp; + + $item['author'] = 'LWN'; + + $cat = $newsletters->previousSibling; + $this->jumpToPreviousTag($cat); + $prefix = $this->getItemPrefix($cat, $cats); + $item['title'] = $prefix . ' ' . $newsletters->textContent; + + $node = $newsletters; + $content = ''; + $contentEnd = false; + while (!$contentEnd) { + $node = $node->nextSibling; + if ( + !$node || ( + $node->nodeType !== XML_TEXT_NODE && ( + !is_null($node->attributes) && + !is_null($class = $node->attributes->getNamedItem('class')) && + in_array($class->nodeValue, ['Cat1HL','Cat2HL','Cat3HL']) + ) + ) + ) { + $contentEnd = true; + } else { + $content .= $node->C14N(); + } + } + $item['content'] = $content; + $items[] = $item; + } + + foreach ($html->getElementsByTagName('h2') as $title) { + if ($title->getAttribute('class') !== 'SummaryHL') { + continue; + } + + $item = []; + + $cat = $title->previousSibling; + $this->jumpToPreviousTag($cat); + $cat = $cat->previousSibling; + $this->jumpToPreviousTag($cat); + $prefix = $this->getItemPrefix($cat, $cats); + $item['title'] = $prefix . ' ' . $title->textContent; + $items[] = array_merge($item, $this->getArticleContent($title)); + } + + return $items; + } + + private function getBriefItems(&$html) + { + $items = []; + $cats = ['','','']; + foreach ($html->getElementsByTagName('h2') as $title) { + if ($title->getAttribute('class') !== 'SummaryHL') { + continue; + } + + $item = []; + + $cat = $title->previousSibling; + $this->jumpToPreviousTag($cat); + $cat = $cat->previousSibling; + $this->jumpToPreviousTag($cat); + $prefix = $this->getItemPrefix($cat, $cats); + $item['title'] = $prefix . ' ' . $title->textContent; + $items[] = array_merge($item, $this->getArticleContent($title)); + } + + return $items; + } } -?> diff --git a/bridges/LaCentraleBridge.php b/bridges/LaCentraleBridge.php index dc6c1a97..cf898a06 100644 --- a/bridges/LaCentraleBridge.php +++ b/bridges/LaCentraleBridge.php @@ -1,473 +1,474 @@ <?php -class LaCentraleBridge extends BridgeAbstract { +class LaCentraleBridge extends BridgeAbstract +{ + const MAINTAINER = 'jacknumber'; + const NAME = 'La Centrale'; + const URI = 'https://www.lacentrale.fr/'; + const DESCRIPTION = 'Returns most recent vehicules ads from LaCentrale'; - const MAINTAINER = 'jacknumber'; - const NAME = 'La Centrale'; - const URI = 'https://www.lacentrale.fr/'; - const DESCRIPTION = 'Returns most recent vehicules ads from LaCentrale'; + const PARAMETERS = [ [ + 'type' => [ + 'name' => 'Type de véhicule', + 'type' => 'list', + 'values' => [ + 'Voiture' => 'car', + 'Camion/Pickup' => 'truck', + 'Moto' => 'moto', + 'Scooter' => 'scooter', + 'Quad' => 'quad', + 'Caravane/Camping-car' => 'mobileHome' + ] + ], + 'brand' => [ + 'name' => 'Marque', + 'type' => 'list', + 'values' => [ + '' => '', + 'ABARTH' => 'ABARTH', + 'AC' => 'AC', + 'AIXAM' => 'AIXAM', + 'ALFA ROMEO' => 'ALFA ROMEO', + 'ALKE' => 'ALKE', + 'ALPINA' => 'ALPINA', + 'ALPINE' => 'ALPINE', + 'AMC' => 'AMC', + 'ANAIG' => 'ANAIG', + 'APRILIA' => 'APRILIA', + 'ARIEL' => 'ARIEL', + 'ASTON MARTIN' => 'ASTON MARTIN', + 'AUDI' => 'AUDI', + 'AUSTIN HEALEY' => 'AUSTIN HEALEY', + 'AUSTIN' => 'AUSTIN', + 'AUTOBIANCHI' => 'AUTOBIANCHI', + 'AVINTON' => 'AVINTON', + 'BELLIER' => 'BELLIER', + 'BENELLI' => 'BENELLI', + 'BENTLEY' => 'BENTLEY', + 'BETA' => 'BETA', + 'BMW' => 'BMW', + 'BOLLORE' => 'BOLLORE', + 'BRIXTON' => 'BRIXTON', + 'BUELL' => 'BUELL', + 'BUGATTI' => 'BUGATTI', + 'BUICK' => 'BUICK', + 'BULLIT' => 'BULLIT', + 'CADILLAC' => 'CADILLAC', + 'CASALINI' => 'CASALINI', + 'CATERHAM' => 'CATERHAM', + 'CHATENET' => 'CHATENET', + 'CHEVROLET' => 'CHEVROLET', + 'CHRYSLER' => 'CHRYSLER', + 'CHUNLAN' => 'CHUNLAN', + 'CITROEN' => 'CITROEN', + 'COURB' => 'COURB', + 'CR&S' => 'CR&S', + 'CUPRA' => 'CUPRA', + 'CYCLONE' => 'CYCLONE', + 'DACIA' => 'DACIA', + 'DAELIM' => 'DAELIM', + 'DAEWOO' => 'DAEWOO', + 'DAF' => 'DAF', + 'DAIHATSU' => 'DAIHATSU', + 'DANGEL' => 'DANGEL', + 'DATSUN' => 'DATSUN', + 'DE SOTO' => 'DE SOTO', + 'DE TOMASO' => 'DE TOMASO', + 'DERBI' => 'DERBI', + 'DEVINCI' => 'DEVINCI', + 'DODGE' => 'DODGE', + 'DONKERVOORT' => 'DONKERVOORT', + 'DS' => 'DS', + 'DUCATI' => 'DUCATI', + 'DUCATY' => 'DUCATY', + 'DUE' => 'DUE', + 'ENFIELD' => 'ENFIELD', + 'EXCALIBUR' => 'EXCALIBUR', + 'FACEL VEGA' => 'FACEL VEGA', + 'FANTIC MOTOR' => 'FANTIC MOTOR', + 'FERRARI' => 'FERRARI', + 'FIAT' => 'FIAT', + 'FISKER' => 'FISKER', + 'FORD' => 'FORD', + 'FUSO' => 'FUSO', + 'GAS GAS' => 'GAS GAS', + 'GILERA' => 'GILERA', + 'GMC' => 'GMC', + 'GOWINN' => 'GOWINN', + 'GRANDIN' => 'GRANDIN', + 'HARLEY DAVIDSON' => 'HARLEY DAVIDSON', + 'HOMMELL' => 'HOMMELL', + 'HONDA' => 'HONDA', + 'HUMMER' => 'HUMMER', + 'HUSABERG' => 'HUSABERG', + 'HUSQVARNA' => 'HUSQVARNA', + 'HYOSUNG' => 'HYOSUNG', + 'HYUNDAI' => 'HYUNDAI', + 'INDIAN' => 'INDIAN', + 'INFINITI' => 'INFINITI', + 'INNOCENTI' => 'INNOCENTI', + 'ISUZU' => 'ISUZU', + 'IVECO' => 'IVECO', + 'JAGUAR' => 'JAGUAR', + 'JDM SIMPA' => 'JDM SIMPA', + 'JEEP' => 'JEEP', + 'JENSEN' => 'JENSEN', + 'JIAYUAN' => 'JIAYUAN', + 'KAWASAKI' => 'KAWASAKI', + 'KEEWAY' => 'KEEWAY', + 'KIA' => 'KIA', + 'KSR' => 'KSR', + 'KTM' => 'KTM', + 'KYMCO' => 'KYMCO', + 'LADA' => 'LADA', + 'LAMBORGHINI' => 'LAMBORGHINI', + 'LANCIA' => 'LANCIA', + 'LAND ROVER' => 'LAND ROVER', + 'LEXUS' => 'LEXUS', + 'LIGIER' => 'LIGIER', + 'LINCOLN' => 'LINCOLN', + 'LONDON TAXI COMPANY' => 'LONDON TAXI COMPANY', + 'LOTUS' => 'LOTUS', + 'MAGPOWER' => 'MAGPOWER', + 'MAN' => 'MAN', + 'MASAI' => 'MASAI', + 'MASERATI' => 'MASERATI', + 'MASH' => 'MASH', + 'MATRA' => 'MATRA', + 'MAYBACH' => 'MAYBACH', + 'MAZDA' => 'MAZDA', + 'MCLAREN' => 'MCLAREN', + 'MEGA' => 'MEGA', + 'MERCEDES' => 'MERCEDES', + 'MERCEDES-AMG' => 'MERCEDES-AMG', + 'MERCURY' => 'MERCURY', + 'MEYERS MANX' => 'MEYERS MANX', + 'MG' => 'MG', + 'MIA ELECTRIC' => 'MIA ELECTRIC', + 'MICROCAR' => 'MICROCAR', + 'MINAUTO' => 'MINAUTO', + 'MINI' => 'MINI', + 'MITSUBISHI' => 'MITSUBISHI', + 'MORGAN' => 'MORGAN', + 'MORRIS' => 'MORRIS', + 'MOTO GUZZI' => 'MOTO GUZZI', + 'MOTO MORINI' => 'MOTO MORINI', + 'MOTOBECANE' => 'MOTOBECANE', + 'MPM MOTORS' => 'MPM MOTORS', + 'MV AGUSTA' => 'MV AGUSTA', + 'NISSAN' => 'NISSAN', + 'NORTON' => 'NORTON', + 'NSU' => 'NSU', + 'OLDSMOBILE' => 'OLDSMOBILE', + 'OPEL' => 'OPEL', + 'ORCAL' => 'ORCAL', + 'OSSA' => 'OSSA', + 'PACKARD' => 'PACKARD', + 'PANTHER' => 'PANTHER', + 'PEUGEOT' => 'PEUGEOT', + 'PGO' => 'PGO', + 'PIAGGIO' => 'PIAGGIO', + 'PLYMOUTH' => 'PLYMOUTH', + 'POLARIS' => 'POLARIS', + 'PONTIAC' => 'PONTIAC', + 'PORSCHE' => 'PORSCHE', + 'REALM' => 'REALM', + 'REGAL RAPTOR' => 'REGAL RAPTOR', + 'RENAULT' => 'RENAULT', + 'RIEJU' => 'RIEJU', + 'ROLLS ROYCE' => 'ROLLS ROYCE', + 'ROVER' => 'ROVER', + 'ROYAL ENFIELD' => 'ROYAL ENFIELD', + 'SAAB' => 'SAAB', + 'SANTANA' => 'SANTANA', + 'SCANIA' => 'SCANIA', + 'SEAT' => 'SEAT', + 'SECMA' => 'SECMA', + 'SHELBY' => 'SHELBY', + 'SHERCO' => 'SHERCO', + 'SIMCA' => 'SIMCA', + 'SKODA' => 'SKODA', + 'SMART' => 'SMART', + 'SPYKER' => 'SPYKER', + 'SSANGYONG' => 'SSANGYONG', + 'STUDEBAKER' => 'STUDEBAKER', + 'SUBARU' => 'SUBARU', + 'SUNBEAM' => 'SUNBEAM', + 'SUZUKI' => 'SUZUKI', + 'SWM' => 'SWM', + 'SYM' => 'SYM', + 'TALBOT SIMCA' => 'TALBOT SIMCA', + 'TALBOT' => 'TALBOT', + 'TEILHOL' => 'TEILHOL', + 'TESLA' => 'TESLA', + 'TM' => 'TM', + 'TNT MOTOR' => 'TNT MOTOR', + 'TOYOTA' => 'TOYOTA', + 'TRIUMPH' => 'TRIUMPH', + 'TVR' => 'TVR', + 'VAUXHALL' => 'VAUXHALL', + 'VESPA' => 'VESPA', + 'VICTORY' => 'VICTORY', + 'VOLKSWAGEN' => 'VOLKSWAGEN', + 'VOLVO' => 'VOLVO', + 'VOXAN' => 'VOXAN', + 'WIESMANN' => 'WIESMANN', + 'YAMAHA' => 'YAMAHA', + 'YCF' => 'YCF', + 'ZERO' => 'ZERO', + 'ZONGSHEN' => 'ZONGSHEN' + ] + ], + 'model' => [ + 'name' => 'Modèle', + 'type' => 'text', + 'title' => 'Get the exact name on LaCentrale' + ], + 'versions' => [ + 'name' => 'Version(s)', + 'type' => 'text', + 'title' => 'Get the exact name(s) on LaCentrale. Separate by comma' + ], + 'category' => [ + 'name' => 'Catégorie', + 'type' => 'list', + 'values' => [ + '' => '', + 'Voiture' => [ + '4x4, SUV & Crossover' => '47', + 'Citadine' => '40', + 'Berline' => '41_42', + 'Break' => '43', + 'Cabriolet' => '46', + 'Coupé' => '45', + 'Monospace' => '44', + 'Bus et minibus' => '82', + 'Fourgonnette' => '85', + 'Fourgon (< 3,5 tonnes)' => '81', + 'Pick-up' => '50', + 'Voiture société, commerciale' => '80', + 'Sans permis' => '48', + 'Camion (> 3,5 tonnes)' => '83', + ], + 'Camion/Pickup' => [ + 'Camion (> 3,5 tonnes)' => '83', + 'Fourgon (< 3,5 tonnes)' => '81', + 'Bus et minibus' => '82', + 'Fourgonnette' => '85', + 'Pick-up' => '50', + 'Voiture société, commerciale' => '80' + ], + 'Moto' => [ + 'Custom' => '60', + 'Offroad' => '61', + 'Roadster' => '62', + 'GT' => '63', + 'Mini moto' => '64', + 'Mobylette' => '65', + 'Supermotard' => '66', + 'Trail' => '67', + 'Side-car' => '69', + 'Sportive' => '68' + ], + 'Caravane/Camping-car' => [ + 'Caravane' => '423', + 'Profilé' => '506', + 'Fourgon aménagé' => '507', + 'Intégral' => '508', + 'Capucine' => '510' + ] + ] + ], + 'pricemin' => [ + 'name' => 'Prix min', + 'type' => 'number' + ], + 'pricemax' => [ + 'name' => 'Prix max', + 'type' => 'number' + ], + 'location' => [ + 'name' => 'CP ou département', + 'type' => 'number', + 'title' => 'Only one' + ], + 'distance' => [ + 'name' => 'Rayon de recherche', + 'type' => 'list', + 'values' => [ + '' => '', + '10 km' => '1', + '20 km' => '2', + '50 km' => '3', + '100 km' => '4', + '200 km' => '5' + ] + ], + 'region' => [ + 'name' => 'Région', + 'type' => 'list', + 'values' => [ + '' => '', + 'Auvergne-Rhône-Alpes' => 'FR-ARA', + 'Bourgogne-Franche-Comté' => 'FR-BFC', + 'Bretagne' => 'FR-BRE', + 'Centre-Val de Loire' => 'FR-CVL', + 'Corse' => 'FR-COR', + 'Grand Est' => 'FR-GES', + 'Hauts-de-France' => 'FR-HDF', + 'Île-de-France' => 'FR-IDF', + 'Normandie' => 'FR-NOR', + 'Nouvelle-Aquitaine' => 'FR-PAC', + 'Occitanie' => 'FR-PDL', + 'Pays de la Loire' => 'FR-OCC', + 'Provence-Alpes-Côte d\'Azur' => 'FR-NAQ' + ] + ], + 'mileagemin' => [ + 'name' => 'Kilométrage min', + 'type' => 'number' + ], + 'mileagemax' => [ + 'name' => 'Kilométrage max', + 'type' => 'number' + ], + 'yearmin' => [ + 'name' => 'Année min', + 'type' => 'number' + ], + 'yearmax' => [ + 'name' => 'Année max', + 'type' => 'number' + ], + 'cubiccapacitymin' => [ + 'name' => 'Cylindrée min', + 'type' => 'number' + ], + 'cubiccapacitymax' => [ + 'name' => 'Cylindrée max', + 'type' => 'number' + ], + 'fuel' => [ + 'name' => 'Énergie', + 'type' => 'list', + 'values' => [ + '' => '', + 'Diesel' => 'dies', + 'Essence' => 'ess', + 'Électrique' => 'elec', + 'Hybride' => 'hyb', + 'GPL' => 'gpl', + 'Bioéthanol' => 'eth', + 'Autre' => 'alt' + ] + ], + 'gearbox' => [ + 'name' => 'Boite de vitesse', + 'type' => 'list', + 'values' => [ + '' => '', + 'Boite automatique' => 'AUTO', + 'Boite mécanique' => 'MANUAL' + ] + ], + 'doors' => [ + 'name' => 'Nombre de portes', + 'type' => 'list', + 'values' => [ + '' => '', + '2 portes' => '2', + '3 portes' => '3', + '4 portes' => '4', + '5 portes' => '5', + '6 portes ou plus' => '6' + ] + ], + 'firsthand' => [ + 'name' => 'Première main', + 'type' => 'checkbox' + ], + 'seller' => [ + 'name' => 'Vendeur', + 'type' => 'list', + 'values' => [ + '' => '', + 'Particulier' => 'PART', + 'Professionel' => 'PRO' + ] + ], + 'sort' => [ + 'name' => 'Tri', + 'type' => 'list', + 'values' => [ + 'Prix (croissant)' => 'priceAsc', + 'Prix (décroissant)' => 'priceDesc', + 'Marque (croissant)' => 'makeAsc', + 'Marque (décroissant)' => 'makeDesc', + 'Kilométrage (croissant)' => 'mileageAsc', + 'Kilométrage (décroissant)' => 'mileageDesc', + 'Année (croissant)' => 'yearAsc', + 'Année (décroissant)' => 'yearDesc', + 'Département (croissant)' => 'visitPlaceAsc', + 'Département (décroissant)' => 'visitPlaceDesc' + ] + ], + ]]; - const PARAMETERS = array( array( - 'type' => array( - 'name' => 'Type de véhicule', - 'type' => 'list', - 'values' => array( - 'Voiture' => 'car', - 'Camion/Pickup' => 'truck', - 'Moto' => 'moto', - 'Scooter' => 'scooter', - 'Quad' => 'quad', - 'Caravane/Camping-car' => 'mobileHome' - ) - ), - 'brand' => array( - 'name' => 'Marque', - 'type' => 'list', - 'values' => array( - '' => '', - 'ABARTH' => 'ABARTH', - 'AC' => 'AC', - 'AIXAM' => 'AIXAM', - 'ALFA ROMEO' => 'ALFA ROMEO', - 'ALKE' => 'ALKE', - 'ALPINA' => 'ALPINA', - 'ALPINE' => 'ALPINE', - 'AMC' => 'AMC', - 'ANAIG' => 'ANAIG', - 'APRILIA' => 'APRILIA', - 'ARIEL' => 'ARIEL', - 'ASTON MARTIN' => 'ASTON MARTIN', - 'AUDI' => 'AUDI', - 'AUSTIN HEALEY' => 'AUSTIN HEALEY', - 'AUSTIN' => 'AUSTIN', - 'AUTOBIANCHI' => 'AUTOBIANCHI', - 'AVINTON' => 'AVINTON', - 'BELLIER' => 'BELLIER', - 'BENELLI' => 'BENELLI', - 'BENTLEY' => 'BENTLEY', - 'BETA' => 'BETA', - 'BMW' => 'BMW', - 'BOLLORE' => 'BOLLORE', - 'BRIXTON' => 'BRIXTON', - 'BUELL' => 'BUELL', - 'BUGATTI' => 'BUGATTI', - 'BUICK' => 'BUICK', - 'BULLIT' => 'BULLIT', - 'CADILLAC' => 'CADILLAC', - 'CASALINI' => 'CASALINI', - 'CATERHAM' => 'CATERHAM', - 'CHATENET' => 'CHATENET', - 'CHEVROLET' => 'CHEVROLET', - 'CHRYSLER' => 'CHRYSLER', - 'CHUNLAN' => 'CHUNLAN', - 'CITROEN' => 'CITROEN', - 'COURB' => 'COURB', - 'CR&S' => 'CR&S', - 'CUPRA' => 'CUPRA', - 'CYCLONE' => 'CYCLONE', - 'DACIA' => 'DACIA', - 'DAELIM' => 'DAELIM', - 'DAEWOO' => 'DAEWOO', - 'DAF' => 'DAF', - 'DAIHATSU' => 'DAIHATSU', - 'DANGEL' => 'DANGEL', - 'DATSUN' => 'DATSUN', - 'DE SOTO' => 'DE SOTO', - 'DE TOMASO' => 'DE TOMASO', - 'DERBI' => 'DERBI', - 'DEVINCI' => 'DEVINCI', - 'DODGE' => 'DODGE', - 'DONKERVOORT' => 'DONKERVOORT', - 'DS' => 'DS', - 'DUCATI' => 'DUCATI', - 'DUCATY' => 'DUCATY', - 'DUE' => 'DUE', - 'ENFIELD' => 'ENFIELD', - 'EXCALIBUR' => 'EXCALIBUR', - 'FACEL VEGA' => 'FACEL VEGA', - 'FANTIC MOTOR' => 'FANTIC MOTOR', - 'FERRARI' => 'FERRARI', - 'FIAT' => 'FIAT', - 'FISKER' => 'FISKER', - 'FORD' => 'FORD', - 'FUSO' => 'FUSO', - 'GAS GAS' => 'GAS GAS', - 'GILERA' => 'GILERA', - 'GMC' => 'GMC', - 'GOWINN' => 'GOWINN', - 'GRANDIN' => 'GRANDIN', - 'HARLEY DAVIDSON' => 'HARLEY DAVIDSON', - 'HOMMELL' => 'HOMMELL', - 'HONDA' => 'HONDA', - 'HUMMER' => 'HUMMER', - 'HUSABERG' => 'HUSABERG', - 'HUSQVARNA' => 'HUSQVARNA', - 'HYOSUNG' => 'HYOSUNG', - 'HYUNDAI' => 'HYUNDAI', - 'INDIAN' => 'INDIAN', - 'INFINITI' => 'INFINITI', - 'INNOCENTI' => 'INNOCENTI', - 'ISUZU' => 'ISUZU', - 'IVECO' => 'IVECO', - 'JAGUAR' => 'JAGUAR', - 'JDM SIMPA' => 'JDM SIMPA', - 'JEEP' => 'JEEP', - 'JENSEN' => 'JENSEN', - 'JIAYUAN' => 'JIAYUAN', - 'KAWASAKI' => 'KAWASAKI', - 'KEEWAY' => 'KEEWAY', - 'KIA' => 'KIA', - 'KSR' => 'KSR', - 'KTM' => 'KTM', - 'KYMCO' => 'KYMCO', - 'LADA' => 'LADA', - 'LAMBORGHINI' => 'LAMBORGHINI', - 'LANCIA' => 'LANCIA', - 'LAND ROVER' => 'LAND ROVER', - 'LEXUS' => 'LEXUS', - 'LIGIER' => 'LIGIER', - 'LINCOLN' => 'LINCOLN', - 'LONDON TAXI COMPANY' => 'LONDON TAXI COMPANY', - 'LOTUS' => 'LOTUS', - 'MAGPOWER' => 'MAGPOWER', - 'MAN' => 'MAN', - 'MASAI' => 'MASAI', - 'MASERATI' => 'MASERATI', - 'MASH' => 'MASH', - 'MATRA' => 'MATRA', - 'MAYBACH' => 'MAYBACH', - 'MAZDA' => 'MAZDA', - 'MCLAREN' => 'MCLAREN', - 'MEGA' => 'MEGA', - 'MERCEDES' => 'MERCEDES', - 'MERCEDES-AMG' => 'MERCEDES-AMG', - 'MERCURY' => 'MERCURY', - 'MEYERS MANX' => 'MEYERS MANX', - 'MG' => 'MG', - 'MIA ELECTRIC' => 'MIA ELECTRIC', - 'MICROCAR' => 'MICROCAR', - 'MINAUTO' => 'MINAUTO', - 'MINI' => 'MINI', - 'MITSUBISHI' => 'MITSUBISHI', - 'MORGAN' => 'MORGAN', - 'MORRIS' => 'MORRIS', - 'MOTO GUZZI' => 'MOTO GUZZI', - 'MOTO MORINI' => 'MOTO MORINI', - 'MOTOBECANE' => 'MOTOBECANE', - 'MPM MOTORS' => 'MPM MOTORS', - 'MV AGUSTA' => 'MV AGUSTA', - 'NISSAN' => 'NISSAN', - 'NORTON' => 'NORTON', - 'NSU' => 'NSU', - 'OLDSMOBILE' => 'OLDSMOBILE', - 'OPEL' => 'OPEL', - 'ORCAL' => 'ORCAL', - 'OSSA' => 'OSSA', - 'PACKARD' => 'PACKARD', - 'PANTHER' => 'PANTHER', - 'PEUGEOT' => 'PEUGEOT', - 'PGO' => 'PGO', - 'PIAGGIO' => 'PIAGGIO', - 'PLYMOUTH' => 'PLYMOUTH', - 'POLARIS' => 'POLARIS', - 'PONTIAC' => 'PONTIAC', - 'PORSCHE' => 'PORSCHE', - 'REALM' => 'REALM', - 'REGAL RAPTOR' => 'REGAL RAPTOR', - 'RENAULT' => 'RENAULT', - 'RIEJU' => 'RIEJU', - 'ROLLS ROYCE' => 'ROLLS ROYCE', - 'ROVER' => 'ROVER', - 'ROYAL ENFIELD' => 'ROYAL ENFIELD', - 'SAAB' => 'SAAB', - 'SANTANA' => 'SANTANA', - 'SCANIA' => 'SCANIA', - 'SEAT' => 'SEAT', - 'SECMA' => 'SECMA', - 'SHELBY' => 'SHELBY', - 'SHERCO' => 'SHERCO', - 'SIMCA' => 'SIMCA', - 'SKODA' => 'SKODA', - 'SMART' => 'SMART', - 'SPYKER' => 'SPYKER', - 'SSANGYONG' => 'SSANGYONG', - 'STUDEBAKER' => 'STUDEBAKER', - 'SUBARU' => 'SUBARU', - 'SUNBEAM' => 'SUNBEAM', - 'SUZUKI' => 'SUZUKI', - 'SWM' => 'SWM', - 'SYM' => 'SYM', - 'TALBOT SIMCA' => 'TALBOT SIMCA', - 'TALBOT' => 'TALBOT', - 'TEILHOL' => 'TEILHOL', - 'TESLA' => 'TESLA', - 'TM' => 'TM', - 'TNT MOTOR' => 'TNT MOTOR', - 'TOYOTA' => 'TOYOTA', - 'TRIUMPH' => 'TRIUMPH', - 'TVR' => 'TVR', - 'VAUXHALL' => 'VAUXHALL', - 'VESPA' => 'VESPA', - 'VICTORY' => 'VICTORY', - 'VOLKSWAGEN' => 'VOLKSWAGEN', - 'VOLVO' => 'VOLVO', - 'VOXAN' => 'VOXAN', - 'WIESMANN' => 'WIESMANN', - 'YAMAHA' => 'YAMAHA', - 'YCF' => 'YCF', - 'ZERO' => 'ZERO', - 'ZONGSHEN' => 'ZONGSHEN' - ) - ), - 'model' => array( - 'name' => 'Modèle', - 'type' => 'text', - 'title' => 'Get the exact name on LaCentrale' - ), - 'versions' => array( - 'name' => 'Version(s)', - 'type' => 'text', - 'title' => 'Get the exact name(s) on LaCentrale. Separate by comma' - ), - 'category' => array( - 'name' => 'Catégorie', - 'type' => 'list', - 'values' => array( - '' => '', - 'Voiture' => array( - '4x4, SUV & Crossover' => '47', - 'Citadine' => '40', - 'Berline' => '41_42', - 'Break' => '43', - 'Cabriolet' => '46', - 'Coupé' => '45', - 'Monospace' => '44', - 'Bus et minibus' => '82', - 'Fourgonnette' => '85', - 'Fourgon (< 3,5 tonnes)' => '81', - 'Pick-up' => '50', - 'Voiture société, commerciale' => '80', - 'Sans permis' => '48', - 'Camion (> 3,5 tonnes)' => '83', - ), - 'Camion/Pickup' => array( - 'Camion (> 3,5 tonnes)' => '83', - 'Fourgon (< 3,5 tonnes)' => '81', - 'Bus et minibus' => '82', - 'Fourgonnette' => '85', - 'Pick-up' => '50', - 'Voiture société, commerciale' => '80' - ), - 'Moto' => array( - 'Custom' => '60', - 'Offroad' => '61', - 'Roadster' => '62', - 'GT' => '63', - 'Mini moto' => '64', - 'Mobylette' => '65', - 'Supermotard' => '66', - 'Trail' => '67', - 'Side-car' => '69', - 'Sportive' => '68' - ), - 'Caravane/Camping-car' => array( - 'Caravane' => '423', - 'Profilé' => '506', - 'Fourgon aménagé' => '507', - 'Intégral' => '508', - 'Capucine' => '510' - ) - ) - ), - 'pricemin' => array( - 'name' => 'Prix min', - 'type' => 'number' - ), - 'pricemax' => array( - 'name' => 'Prix max', - 'type' => 'number' - ), - 'location' => array( - 'name' => 'CP ou département', - 'type' => 'number', - 'title' => 'Only one' - ), - 'distance' => array( - 'name' => 'Rayon de recherche', - 'type' => 'list', - 'values' => array( - '' => '', - '10 km' => '1', - '20 km' => '2', - '50 km' => '3', - '100 km' => '4', - '200 km' => '5' - ) - ), - 'region' => array( - 'name' => 'Région', - 'type' => 'list', - 'values' => array( - '' => '', - 'Auvergne-Rhône-Alpes' => 'FR-ARA', - 'Bourgogne-Franche-Comté' => 'FR-BFC', - 'Bretagne' => 'FR-BRE', - 'Centre-Val de Loire' => 'FR-CVL', - 'Corse' => 'FR-COR', - 'Grand Est' => 'FR-GES', - 'Hauts-de-France' => 'FR-HDF', - 'Île-de-France' => 'FR-IDF', - 'Normandie' => 'FR-NOR', - 'Nouvelle-Aquitaine' => 'FR-PAC', - 'Occitanie' => 'FR-PDL', - 'Pays de la Loire' => 'FR-OCC', - 'Provence-Alpes-Côte d\'Azur' => 'FR-NAQ' - ) - ), - 'mileagemin' => array( - 'name' => 'Kilométrage min', - 'type' => 'number' - ), - 'mileagemax' => array( - 'name' => 'Kilométrage max', - 'type' => 'number' - ), - 'yearmin' => array( - 'name' => 'Année min', - 'type' => 'number' - ), - 'yearmax' => array( - 'name' => 'Année max', - 'type' => 'number' - ), - 'cubiccapacitymin' => array( - 'name' => 'Cylindrée min', - 'type' => 'number' - ), - 'cubiccapacitymax' => array( - 'name' => 'Cylindrée max', - 'type' => 'number' - ), - 'fuel' => array( - 'name' => 'Énergie', - 'type' => 'list', - 'values' => array( - '' => '', - 'Diesel' => 'dies', - 'Essence' => 'ess', - 'Électrique' => 'elec', - 'Hybride' => 'hyb', - 'GPL' => 'gpl', - 'Bioéthanol' => 'eth', - 'Autre' => 'alt' - ) - ), - 'gearbox' => array( - 'name' => 'Boite de vitesse', - 'type' => 'list', - 'values' => array( - '' => '', - 'Boite automatique' => 'AUTO', - 'Boite mécanique' => 'MANUAL' - ) - ), - 'doors' => array( - 'name' => 'Nombre de portes', - 'type' => 'list', - 'values' => array( - '' => '', - '2 portes' => '2', - '3 portes' => '3', - '4 portes' => '4', - '5 portes' => '5', - '6 portes ou plus' => '6' - ) - ), - 'firsthand' => array( - 'name' => 'Première main', - 'type' => 'checkbox' - ), - 'seller' => array( - 'name' => 'Vendeur', - 'type' => 'list', - 'values' => array( - '' => '', - 'Particulier' => 'PART', - 'Professionel' => 'PRO' - ) - ), - 'sort' => array( - 'name' => 'Tri', - 'type' => 'list', - 'values' => array( - 'Prix (croissant)' => 'priceAsc', - 'Prix (décroissant)' => 'priceDesc', - 'Marque (croissant)' => 'makeAsc', - 'Marque (décroissant)' => 'makeDesc', - 'Kilométrage (croissant)' => 'mileageAsc', - 'Kilométrage (décroissant)' => 'mileageDesc', - 'Année (croissant)' => 'yearAsc', - 'Année (décroissant)' => 'yearDesc', - 'Département (croissant)' => 'visitPlaceAsc', - 'Département (décroissant)' => 'visitPlaceDesc' - ) - ), - )); + public function collectData() + { + if ( + !empty($this->getInput('distance')) + && is_null($this->getInput('location')) + ) { + returnClientError('You need a place ("CP ou département") to search arround.'); + } - public function collectData(){ - if(!empty($this->getInput('distance')) - && is_null($this->getInput('location')) - ) { - returnClientError('You need a place ("CP ou département") to search arround.'); - } + $params = [ + 'vertical' => $this->getInput('type'), + 'makesModelsCommercialNames' => $this->getInput('brand') . ':' . $this->getInput('model'), + 'versions' => $this->getInput('versions'), + 'categories' => $this->getInput('category'), + 'priceMin' => $this->getInput('pricemin'), + 'priceMax' => $this->getInput('pricemax'), + 'dptCp' => $this->getInput('location'), + 'distance' => $this->getInput('distance'), + 'regions' => $this->getInput('region'), + 'mileageMin' => $this->getInput('mileagemin'), + 'mileageMax' => $this->getInput('mileagemax'), + 'yearMin' => $this->getInput('yearmin'), + 'yearMax' => $this->getInput('yearmax'), + 'cubicMin' => $this->getInput('cubiccapacitymin'), + 'cubicMax' => $this->getInput('cubiccapacitymax'), + 'energies' => $this->getInput('fuel'), + 'firstHand' => $this->getInput('firsthand') ? 'true' : 'false', + 'gearbox' => $this->getInput('gearbox'), + 'doors' => $this->getInput('doors'), + 'sortBy' => $this->getInput('sort') + ]; + $url = sprintf('%slisting?%s', self::URI, http_build_query($params)); + $html = getSimpleHTMLDOM($url); - $params = array( - 'vertical' => $this->getInput('type'), - 'makesModelsCommercialNames' => $this->getInput('brand') . ':' . $this->getInput('model'), - 'versions' => $this->getInput('versions'), - 'categories' => $this->getInput('category'), - 'priceMin' => $this->getInput('pricemin'), - 'priceMax' => $this->getInput('pricemax'), - 'dptCp' => $this->getInput('location'), - 'distance' => $this->getInput('distance'), - 'regions' => $this->getInput('region'), - 'mileageMin' => $this->getInput('mileagemin'), - 'mileageMax' => $this->getInput('mileagemax'), - 'yearMin' => $this->getInput('yearmin'), - 'yearMax' => $this->getInput('yearmax'), - 'cubicMin' => $this->getInput('cubiccapacitymin'), - 'cubicMax' => $this->getInput('cubiccapacitymax'), - 'energies' => $this->getInput('fuel'), - 'firstHand' => $this->getInput('firsthand') ? 'true' : 'false', - 'gearbox' => $this->getInput('gearbox'), - 'doors' => $this->getInput('doors'), - 'sortBy' => $this->getInput('sort') - ); - $url = sprintf('%slisting?%s', self::URI, http_build_query($params)); - $html = getSimpleHTMLDOM($url); + $elements = $html->find('.adLineContainer'); + foreach ($elements as $element) { + $item = []; + $item['uri'] = trim(self::URI, '/') . $element->find('div > a', 0)->href; + $item['title'] = $element->find('.searchCard__makeModel', 0)->plaintext; + $item['sellerType'] = $element->find('.searchCard__customer', 0)->plaintext; + $item['author'] = $item['sellerType']; + $item['version'] = $element->find('.searchCard__version', 0)->plaintext; + $item['price'] = $element->find('.searchCard__fieldPrice', 0)->plaintext; + $item['year'] = $element->find('.searchCard__year', 0)->plaintext; + $item['mileage'] = $element->find('.searchCard__mileage', 0)->plaintext; + // The image is lazyloaded with ajax - $elements = $html->find('.adLineContainer'); - foreach($elements as $element) { - - $item = array(); - $item['uri'] = trim(self::URI, '/') . $element->find('div > a', 0)->href; - $item['title'] = $element->find('.searchCard__makeModel', 0)->plaintext; - $item['sellerType'] = $element->find('.searchCard__customer', 0)->plaintext; - $item['author'] = $item['sellerType']; - $item['version'] = $element->find('.searchCard__version', 0)->plaintext; - $item['price'] = $element->find('.searchCard__fieldPrice', 0)->plaintext; - $item['year'] = $element->find('.searchCard__year', 0)->plaintext; - $item['mileage'] = $element->find('.searchCard__mileage', 0)->plaintext; - // The image is lazyloaded with ajax - - $item['content'] = ' + $item['content'] = ' <br>Variation : ' . $item['version'] - . '<br>Prix : ' . $item['price'] - . '<br>Année : ' . $item['year'] - . '<br>Kilométrage : ' . $item['mileage'] - . '<br>Type de vendeur : ' . $item['sellerType']; + . '<br>Prix : ' . $item['price'] + . '<br>Année : ' . $item['year'] + . '<br>Kilométrage : ' . $item['mileage'] + . '<br>Type de vendeur : ' . $item['sellerType']; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/LaTeX3ProjectNewslettersBridge.php b/bridges/LaTeX3ProjectNewslettersBridge.php index 61bc1f6d..dcf1ba0d 100644 --- a/bridges/LaTeX3ProjectNewslettersBridge.php +++ b/bridges/LaTeX3ProjectNewslettersBridge.php @@ -1,33 +1,37 @@ <?php -class LaTeX3ProjectNewslettersBridge extends BridgeAbstract { - const MAINTAINER = 'µKöff'; - const NAME = 'LaTeX3 Project Newsletters'; - const URI = 'https://www.latex-project.org'; - const DESCRIPTION = 'Newsletters by the LaTeX3 project team covering topics of interest in the area of +class LaTeX3ProjectNewslettersBridge extends BridgeAbstract +{ + const MAINTAINER = 'µKöff'; + const NAME = 'LaTeX3 Project Newsletters'; + const URI = 'https://www.latex-project.org'; + const DESCRIPTION = 'Newsletters by the LaTeX3 project team covering topics of interest in the area of LaTeX3/expl3 development. They appear in irregular intervals and are not necessarily tied to individual releases of the software (as the LaTeX3 kernel code is updated rather often).'; - public function collectData(){ - $html = getSimpleHTMLDOM(static::URI . '/news/latex3-news/') or returnServerError('No contents received!'); - $newsContainer = $html->find('article tbody', 0); + public function collectData() + { + $html = getSimpleHTMLDOM(static::URI . '/news/latex3-news/') or returnServerError('No contents received!'); + $newsContainer = $html->find('article tbody', 0); - foreach($newsContainer->find('tr') as $row) { - $this->items[] = $this->collectArticle($row); - } - } + foreach ($newsContainer->find('tr') as $row) { + $this->items[] = $this->collectArticle($row); + } + } - private function collectArticle($element) { - $item = array(); - $item['uri'] = static::URI . $element->find('td', 1)->find('a', 0)->href; - $item['title'] = $element->find('td', 1)->find('a', 0)->plaintext; - $item['timestamp'] = DateTime::createFromFormat('Y/m/d', $element->find('td', 0)->plaintext)->getTimestamp(); - $item['content'] = $element->find('td', 2)->plaintext; - $item['author'] = 'LaTeX3 Project'; - return $item; - } + private function collectArticle($element) + { + $item = []; + $item['uri'] = static::URI . $element->find('td', 1)->find('a', 0)->href; + $item['title'] = $element->find('td', 1)->find('a', 0)->plaintext; + $item['timestamp'] = DateTime::createFromFormat('Y/m/d', $element->find('td', 0)->plaintext)->getTimestamp(); + $item['content'] = $element->find('td', 2)->plaintext; + $item['author'] = 'LaTeX3 Project'; + return $item; + } - public function getIcon(){ - return self::URI . '/favicon.ico'; - } + public function getIcon() + { + return self::URI . '/favicon.ico'; + } } diff --git a/bridges/LeBonCoinBridge.php b/bridges/LeBonCoinBridge.php index 87389d18..62416b98 100644 --- a/bridges/LeBonCoinBridge.php +++ b/bridges/LeBonCoinBridge.php @@ -1,538 +1,532 @@ <?php -class LeBonCoinBridge extends BridgeAbstract { - - const MAINTAINER = 'jacknumber'; - const NAME = 'LeBonCoin'; - const URI = 'https://www.leboncoin.fr/'; - const DESCRIPTION = 'Returns most recent results from LeBonCoin'; - - const PARAMETERS = array( - array( - 'keywords' => array('name' => 'Mots-Clés'), - 'region' => array( - 'name' => 'Région', - 'type' => 'list', - 'values' => array( - 'Toute la France' => '', - 'Alsace' => '1', - 'Aquitaine' => '2', - 'Auvergne' => '3', - 'Basse Normandie' => '4', - 'Bourgogne' => '5', - 'Bretagne' => '6', - 'Centre' => '7', - 'Champagne Ardenne' => '8', - 'Corse' => '9', - 'Franche Comté' => '10', - 'Haute Normandie' => '11', - 'Ile de France' => '12', - 'Languedoc Roussillon' => '13', - 'Limousin' => '14', - 'Lorraine' => '15', - 'Midi Pyrénées' => '16', - 'Nord Pas De Calais' => '17', - 'Pays de la Loire' => '18', - 'Picardie' => '19', - 'Poitou Charentes' => '20', - 'Provence Alpes Côte d\'Azur' => '21', - 'Rhône-Alpes' => '22', - 'Guadeloupe' => '23', - 'Martinique' => '24', - 'Guyane' => '25', - 'Réunion' => '26' - ) - ), - 'department' => array( - 'name' => 'Département', - 'type' => 'list', - 'values' => array( - '' => '', - 'Ain' => '1', - 'Aisne' => '2', - 'Allier' => '3', - 'Alpes-de-Haute-Provence' => '4', - 'Hautes-Alpes' => '5', - 'Alpes-Maritimes' => '6', - 'Ardèche' => '7', - 'Ardennes' => '8', - 'Ariège' => '9', - 'Aube' => '10', - 'Aude' => '11', - 'Aveyron' => '12', - 'Bouches-du-Rhône' => '13', - 'Calvados' => '14', - 'Cantal' => '15', - 'Charente' => '16', - 'Charente-Maritime' => '17', - 'Cher' => '18', - 'Corrèze' => '19', - 'Corse-du-Sud' => '2A', - 'Haute-Corse' => '2B', - 'Côte-d\'Or' => '21', - 'Côtes-d\'Armor' => '22', - 'Creuse' => '23', - 'Dordogne' => '24', - 'Doubs' => '25', - 'Drôme' => '26', - 'Eure' => '27', - 'Eure-et-Loir' => '28', - 'Finistère' => '29', - 'Gard' => '30', - 'Haute-Garonne' => '31', - 'Gers' => '32', - 'Gironde' => '33', - 'Hérault' => '34', - 'Ille-et-Vilaine' => '35', - 'Indre' => '36', - 'Indre-et-Loire' => '37', - 'Isère' => '38', - 'Jura' => '39', - 'Landes' => '40', - 'Loir-et-Cher' => '41', - 'Loire' => '42', - 'Haute-Loire' => '43', - 'Loire-Atlantique' => '44', - 'Loiret' => '45', - 'Lot' => '46', - 'Lot-et-Garonne' => '47', - 'Lozère' => '48', - 'Maine-et-Loire' => '49', - 'Manche' => '50', - 'Marne' => '51', - 'Haute-Marne' => '52', - 'Mayenne' => '53', - 'Meurthe-et-Moselle' => '54', - 'Meuse' => '55', - 'Morbihan' => '56', - 'Moselle' => '57', - 'Nièvre' => '58', - 'Nord' => '59', - 'Oise' => '60', - 'Orne' => '61', - 'Pas-de-Calais' => '62', - 'Puy-de-Dôme' => '63', - 'Pyrénées-Atlantiques' => '64', - 'Hautes-Pyrénées' => '65', - 'Pyrénées-Orientales' => '66', - 'Bas-Rhin' => '67', - 'Haut-Rhin' => '68', - 'Rhône' => '69', - 'Haute-Saône' => '70', - 'Saône-et-Loire' => '71', - 'Sarthe' => '72', - 'Savoie' => '73', - 'Haute-Savoie' => '74', - 'Paris' => '75', - 'Seine-Maritime' => '76', - 'Seine-et-Marne' => '77', - 'Yvelines' => '78', - 'Deux-Sèvres' => '79', - 'Somme' => '80', - 'Tarn' => '81', - 'Tarn-et-Garonne' => '82', - 'Var' => '83', - 'Vaucluse' => '84', - 'Vendée' => '85', - 'Vienne' => '86', - 'Haute-Vienne' => '87', - 'Vosges' => '88', - 'Yonne' => '89', - 'Territoire de Belfort' => '90', - 'Essonne' => '91', - 'Hauts-de-Seine' => '92', - 'Seine-Saint-Denis' => '93', - 'Val-de-Marne' => '94', - 'Val-d\'Oise' => '95' - ) - ), - 'cities' => array( - 'name' => 'Villes', - 'title' => 'Codes postaux séparés par des virgules' - ), - 'category' => array( - 'name' => 'Catégorie', - 'type' => 'list', - 'values' => array( - 'Toutes catégories' => '', - 'EMPLOI' => array( - 'Emploi et recrutement' => '71', - 'Offres d\'emploi et jobs' => '33' - ), - 'VÉHICULES' => array( - 'Tous' => '1', - 'Voitures' => '2', - 'Motos' => '3', - 'Caravaning' => '4', - 'Utilitaires' => '5', - 'Equipement Auto' => '6', - 'Equipement Moto' => '44', - 'Equipement Caravaning' => '50', - 'Nautisme' => '7', - 'Equipement Nautisme' => '51' - ), - 'IMMOBILIER' => array( - 'Tous' => '8', - 'Ventes immobilières' => '9', - 'Locations' => '10', - 'Colocations' => '11', - 'Bureaux & Commerces' => '13' - ), - 'VACANCES' => array( - 'Tous' => '66', - 'Locations & Gîtes' => '12', - 'Chambres d\'hôtes' => '67', - 'Campings' => '68', - 'Hôtels' => '69', - 'Hébergements insolites' => '70' - ), - 'MULTIMÉDIA' => array( - 'Tous' => '14', - 'Informatique' => '15', - 'Consoles & Jeux vidéo' => '43', - 'Image & Son' => '16', - 'Téléphonie' => '17' - ), - 'LOISIRS' => array( - 'Tous' => '24', - 'DVD / Films' => '25', - 'CD / Musique' => '26', - 'Livres' => '27', - 'Animaux' => '28', - 'Vélos' => '55', - 'Sports & Hobbies' => '29', - 'Instruments de musique' => '30', - 'Collection' => '40', - 'Jeux & Jouets' => '41', - 'Vins & Gastronomie' => '48' - ), - 'MATÉRIEL PROFESSIONNEL' => array( - 'Tous' => '56', - 'Matériel Agricole' => '57', - 'Transport - Manutention' => '58', - 'BTP - Chantier Gros-oeuvre' => '59', - 'Outillage - Matériaux 2nd-oeuvre' => '60', - 'Équipements Industriels' => '32', - 'Restauration - Hôtellerie' => '61', - 'Fournitures de Bureau' => '62', - 'Commerces & Marchés' => '63', - 'Matériel Médical' => '64' - ), - 'SERVICES' => array( - 'Tous' => '31', - 'Prestations de services' => '34', - 'Billetterie' => '35', - 'Événements' => '49', - 'Cours particuliers' => '36', - 'Covoiturage' => '65' - ), - 'MAISON' => array( - 'Tous' => '18', - 'Ameublement' => '19', - 'Électroménager' => '20', - 'Arts de la table' => '45', - 'Décoration' => '39', - 'Linge de maison' => '46', - 'Bricolage' => '21', - 'Jardinage' => '52', - 'Vêtements' => '22', - 'Chaussures' => '53', - 'Accessoires & Bagagerie' => '47', - 'Montres & Bijoux' => '42', - 'Équipement bébé' => '23', - 'Vêtements bébé' => '54', - ), - 'AUTRES' => '37' - ) - ), - 'pricemin' => array( - 'name' => 'Prix min', - 'type' => 'number' - ), - 'pricemax' => array( - 'name' => 'Prix max', - 'type' => 'number' - ), - 'estate' => array( - 'name' => 'Type de bien', - 'type' => 'list', - 'values' => array( - '' => '', - 'Maison' => '1', - 'Appartement' => '2', - 'Terrain' => '3', - 'Parking' => '4', - 'Autre' => '5' - ) - ), - 'roomsmin' => array( - 'name' => 'Pièces min', - 'type' => 'number' - ), - 'roomsmax' => array( - 'name' => 'Pièces max', - 'type' => 'number' - ), - 'squaremin' => array( - 'name' => 'Surface min', - 'type' => 'number' - ), - 'squaremax' => array( - 'name' => 'Surface max', - 'type' => 'number' - ), - 'mileagemin' => array( - 'name' => 'Kilométrage min', - 'type' => 'number' - ), - 'mileagemax' => array( - 'name' => 'Kilométrage max', - 'type' => 'number' - ), - 'yearmin' => array( - 'name' => 'Année min', - 'type' => 'number' - ), - 'yearmax' => array( - 'name' => 'Année max', - 'type' => 'number' - ), - 'cubiccapacitymin' => array( - 'name' => 'Cylindrée min', - 'type' => 'number' - ), - 'cubiccapacitymax' => array( - 'name' => 'Cylindrée max', - 'type' => 'number' - ), - 'fuel' => array( - 'name' => 'Énergie', - 'type' => 'list', - 'values' => array( - '' => '', - 'Essence' => '1', - 'Diesel' => '2', - 'GPL' => '3', - 'Électrique' => '4', - 'Hybride' => '6', - 'Autre' => '5' - ) - ), - 'owner' => array( - 'name' => 'Vendeur', - 'type' => 'list', - 'values' => array( - 'Tous' => '', - 'Particuliers' => 'private', - 'Professionnels' => 'pro' - ) - ) - ) - ); - - public static $LBC_API_KEY = 'ba0c2dad52b3ec'; - - private function getRange($field, $range_min, $range_max){ - - if(!is_null($range_min) - && !is_null($range_max) - && $range_min > $range_max) { - returnClientError('Min-' . $field . ' must be lower than max-' . $field . '.'); - } - - if(!is_null($range_min) - && is_null($range_max)) { - returnClientError('Max-' . $field . ' is needed when min-' . $field . ' is setted (range).'); - } - - return array( - 'min' => $range_min, - 'max' => $range_max - ); - } - - public function collectData(){ - - $url = 'https://api.leboncoin.fr/api/adfinder/v1/search'; - $data = $this->buildRequestJson(); - - $header = array( - 'User-Agent: LBC;Android;10;SAMSUNG;phone;0aaaaaaaaaaaaaaa;wifi;8.24.3.8;152437;0', - 'Content-Type: application/json', - 'X-LBC-CC: 7', - 'Accept: application/json,application/hal+json', - 'Content-Length: ' . strlen($data), - 'api_key: ' . self::$LBC_API_KEY - ); - - $opts = array( - CURLOPT_CUSTOMREQUEST => 'POST', - CURLOPT_POSTFIELDS => $data - - ); - - $content = getContents($url, $header, $opts); - - $json = json_decode($content); - - if($json->total === 0) { - return; - } - - foreach($json->ads as $element) { - - $item['title'] = $element->subject; - $item['content'] = $element->body; - $item['date'] = $element->index_date; - $item['timestamp'] = strtotime($element->index_date); - $item['uri'] = $element->url; - $item['ad_type'] = $element->ad_type; - $item['author'] = $element->owner->name; - - if(isset($element->location->city)) { - - $item['city'] = $element->location->city; - $item['content'] .= ' -- ' . $element->location->city; - - } - - if(isset($element->location->zipcode)) { - $item['zipcode'] = $element->location->zipcode; - } - if(isset($element->price)) { - - $item['price'] = $element->price[0]; - $item['content'] .= ' -- ' . current($element->price) . '€'; - - } - - if(isset($element->images->urls)) { - - $item['thumbnail'] = $element->images->thumb_url; - $item['enclosures'] = array(); - - foreach($element->images->urls as $image) { - $item['enclosures'][] = $image; - } - - } - - $this->items[] = $item; - } - } - - private function buildRequestJson() { - - $requestJson = new StdClass(); - $requestJson->owner_type = $this->getInput('owner'); - $requestJson->filters = new StdClass(); - - $requestJson->filters->keywords = array( - 'text' => $this->getInput('keywords') - ); - - if($this->getInput('region') != '') { - $requestJson->filters->location['regions'] = array($this->getInput('region')); - } - - if($this->getInput('department') != '') { - $requestJson->filters->location['departments'] = array($this->getInput('department')); - } - - if($this->getInput('cities') != '') { - - $requestJson->filters->location['city_zipcodes'] = array(); - - foreach (explode(',', $this->getInput('cities')) as $zipcode) { - - $requestJson->filters->location['city_zipcodes'][] = array( - 'zipcode' => trim($zipcode) - ); - } - - } - - $requestJson->filters->category = array( - 'id' => $this->getInput('category') - ); - - if($this->getInput('pricemin') != '' - || $this->getInput('pricemax') != '') { - - $requestJson->filters->ranges->price = $this->getRange( - 'price', - $this->getInput('pricemin'), - $this->getInput('pricemax') - ); - - } - - if($this->getInput('estate') != '') { - $requestJson->filters->enums['real_estate_type'] = array($this->getInput('estate')); - } - - if($this->getInput('roomsmin') != '' - || $this->getInput('roomsmax') != '') { - - $requestJson->filters->ranges->rooms = $this->getRange( - 'rooms', - $this->getInput('roomsmin'), - $this->getInput('roomsmax') - ); - - } - - if($this->getInput('squaremin') != '' - || $this->getInput('squaremax') != '') { - - $requestJson->filters->ranges->square = $this->getRange( - 'square', - $this->getInput('squaremin'), - $this->getInput('squaremax') - ); - - } - - if($this->getInput('mileagemin') != '' - || $this->getInput('mileagemax') != '') { - - $requestJson->filters->ranges->mileage = $this->getRange( - 'mileage', - $this->getInput('mileagemin'), - $this->getInput('mileagemax') - ); - - } - - if($this->getInput('yearmin') != '' - || $this->getInput('yearmax') != '') { - - $requestJson->filters->ranges->regdate = $this->getRange( - 'year', - $this->getInput('yearmin'), - $this->getInput('yearmax') - ); - - } - - if($this->getInput('cubiccapacitymin') != '' - || $this->getInput('cubiccapacitymax') != '') { - - $requestJson->filters->ranges->cubic_capacity = $this->getRange( - 'cubic_capacity', - $this->getInput('cubiccapacitymin'), - $this->getInput('cubiccapacitymax') - ); - - } - - if($this->getInput('fuel') != '') { - $requestJson->filters->enums['fuel'] = array($this->getInput('fuel')); - } - - $requestJson->limit = 30; - - return json_encode($requestJson); - - } +class LeBonCoinBridge extends BridgeAbstract +{ + const MAINTAINER = 'jacknumber'; + const NAME = 'LeBonCoin'; + const URI = 'https://www.leboncoin.fr/'; + const DESCRIPTION = 'Returns most recent results from LeBonCoin'; + + const PARAMETERS = [ + [ + 'keywords' => ['name' => 'Mots-Clés'], + 'region' => [ + 'name' => 'Région', + 'type' => 'list', + 'values' => [ + 'Toute la France' => '', + 'Alsace' => '1', + 'Aquitaine' => '2', + 'Auvergne' => '3', + 'Basse Normandie' => '4', + 'Bourgogne' => '5', + 'Bretagne' => '6', + 'Centre' => '7', + 'Champagne Ardenne' => '8', + 'Corse' => '9', + 'Franche Comté' => '10', + 'Haute Normandie' => '11', + 'Ile de France' => '12', + 'Languedoc Roussillon' => '13', + 'Limousin' => '14', + 'Lorraine' => '15', + 'Midi Pyrénées' => '16', + 'Nord Pas De Calais' => '17', + 'Pays de la Loire' => '18', + 'Picardie' => '19', + 'Poitou Charentes' => '20', + 'Provence Alpes Côte d\'Azur' => '21', + 'Rhône-Alpes' => '22', + 'Guadeloupe' => '23', + 'Martinique' => '24', + 'Guyane' => '25', + 'Réunion' => '26' + ] + ], + 'department' => [ + 'name' => 'Département', + 'type' => 'list', + 'values' => [ + '' => '', + 'Ain' => '1', + 'Aisne' => '2', + 'Allier' => '3', + 'Alpes-de-Haute-Provence' => '4', + 'Hautes-Alpes' => '5', + 'Alpes-Maritimes' => '6', + 'Ardèche' => '7', + 'Ardennes' => '8', + 'Ariège' => '9', + 'Aube' => '10', + 'Aude' => '11', + 'Aveyron' => '12', + 'Bouches-du-Rhône' => '13', + 'Calvados' => '14', + 'Cantal' => '15', + 'Charente' => '16', + 'Charente-Maritime' => '17', + 'Cher' => '18', + 'Corrèze' => '19', + 'Corse-du-Sud' => '2A', + 'Haute-Corse' => '2B', + 'Côte-d\'Or' => '21', + 'Côtes-d\'Armor' => '22', + 'Creuse' => '23', + 'Dordogne' => '24', + 'Doubs' => '25', + 'Drôme' => '26', + 'Eure' => '27', + 'Eure-et-Loir' => '28', + 'Finistère' => '29', + 'Gard' => '30', + 'Haute-Garonne' => '31', + 'Gers' => '32', + 'Gironde' => '33', + 'Hérault' => '34', + 'Ille-et-Vilaine' => '35', + 'Indre' => '36', + 'Indre-et-Loire' => '37', + 'Isère' => '38', + 'Jura' => '39', + 'Landes' => '40', + 'Loir-et-Cher' => '41', + 'Loire' => '42', + 'Haute-Loire' => '43', + 'Loire-Atlantique' => '44', + 'Loiret' => '45', + 'Lot' => '46', + 'Lot-et-Garonne' => '47', + 'Lozère' => '48', + 'Maine-et-Loire' => '49', + 'Manche' => '50', + 'Marne' => '51', + 'Haute-Marne' => '52', + 'Mayenne' => '53', + 'Meurthe-et-Moselle' => '54', + 'Meuse' => '55', + 'Morbihan' => '56', + 'Moselle' => '57', + 'Nièvre' => '58', + 'Nord' => '59', + 'Oise' => '60', + 'Orne' => '61', + 'Pas-de-Calais' => '62', + 'Puy-de-Dôme' => '63', + 'Pyrénées-Atlantiques' => '64', + 'Hautes-Pyrénées' => '65', + 'Pyrénées-Orientales' => '66', + 'Bas-Rhin' => '67', + 'Haut-Rhin' => '68', + 'Rhône' => '69', + 'Haute-Saône' => '70', + 'Saône-et-Loire' => '71', + 'Sarthe' => '72', + 'Savoie' => '73', + 'Haute-Savoie' => '74', + 'Paris' => '75', + 'Seine-Maritime' => '76', + 'Seine-et-Marne' => '77', + 'Yvelines' => '78', + 'Deux-Sèvres' => '79', + 'Somme' => '80', + 'Tarn' => '81', + 'Tarn-et-Garonne' => '82', + 'Var' => '83', + 'Vaucluse' => '84', + 'Vendée' => '85', + 'Vienne' => '86', + 'Haute-Vienne' => '87', + 'Vosges' => '88', + 'Yonne' => '89', + 'Territoire de Belfort' => '90', + 'Essonne' => '91', + 'Hauts-de-Seine' => '92', + 'Seine-Saint-Denis' => '93', + 'Val-de-Marne' => '94', + 'Val-d\'Oise' => '95' + ] + ], + 'cities' => [ + 'name' => 'Villes', + 'title' => 'Codes postaux séparés par des virgules' + ], + 'category' => [ + 'name' => 'Catégorie', + 'type' => 'list', + 'values' => [ + 'Toutes catégories' => '', + 'EMPLOI' => [ + 'Emploi et recrutement' => '71', + 'Offres d\'emploi et jobs' => '33' + ], + 'VÉHICULES' => [ + 'Tous' => '1', + 'Voitures' => '2', + 'Motos' => '3', + 'Caravaning' => '4', + 'Utilitaires' => '5', + 'Equipement Auto' => '6', + 'Equipement Moto' => '44', + 'Equipement Caravaning' => '50', + 'Nautisme' => '7', + 'Equipement Nautisme' => '51' + ], + 'IMMOBILIER' => [ + 'Tous' => '8', + 'Ventes immobilières' => '9', + 'Locations' => '10', + 'Colocations' => '11', + 'Bureaux & Commerces' => '13' + ], + 'VACANCES' => [ + 'Tous' => '66', + 'Locations & Gîtes' => '12', + 'Chambres d\'hôtes' => '67', + 'Campings' => '68', + 'Hôtels' => '69', + 'Hébergements insolites' => '70' + ], + 'MULTIMÉDIA' => [ + 'Tous' => '14', + 'Informatique' => '15', + 'Consoles & Jeux vidéo' => '43', + 'Image & Son' => '16', + 'Téléphonie' => '17' + ], + 'LOISIRS' => [ + 'Tous' => '24', + 'DVD / Films' => '25', + 'CD / Musique' => '26', + 'Livres' => '27', + 'Animaux' => '28', + 'Vélos' => '55', + 'Sports & Hobbies' => '29', + 'Instruments de musique' => '30', + 'Collection' => '40', + 'Jeux & Jouets' => '41', + 'Vins & Gastronomie' => '48' + ], + 'MATÉRIEL PROFESSIONNEL' => [ + 'Tous' => '56', + 'Matériel Agricole' => '57', + 'Transport - Manutention' => '58', + 'BTP - Chantier Gros-oeuvre' => '59', + 'Outillage - Matériaux 2nd-oeuvre' => '60', + 'Équipements Industriels' => '32', + 'Restauration - Hôtellerie' => '61', + 'Fournitures de Bureau' => '62', + 'Commerces & Marchés' => '63', + 'Matériel Médical' => '64' + ], + 'SERVICES' => [ + 'Tous' => '31', + 'Prestations de services' => '34', + 'Billetterie' => '35', + 'Événements' => '49', + 'Cours particuliers' => '36', + 'Covoiturage' => '65' + ], + 'MAISON' => [ + 'Tous' => '18', + 'Ameublement' => '19', + 'Électroménager' => '20', + 'Arts de la table' => '45', + 'Décoration' => '39', + 'Linge de maison' => '46', + 'Bricolage' => '21', + 'Jardinage' => '52', + 'Vêtements' => '22', + 'Chaussures' => '53', + 'Accessoires & Bagagerie' => '47', + 'Montres & Bijoux' => '42', + 'Équipement bébé' => '23', + 'Vêtements bébé' => '54', + ], + 'AUTRES' => '37' + ] + ], + 'pricemin' => [ + 'name' => 'Prix min', + 'type' => 'number' + ], + 'pricemax' => [ + 'name' => 'Prix max', + 'type' => 'number' + ], + 'estate' => [ + 'name' => 'Type de bien', + 'type' => 'list', + 'values' => [ + '' => '', + 'Maison' => '1', + 'Appartement' => '2', + 'Terrain' => '3', + 'Parking' => '4', + 'Autre' => '5' + ] + ], + 'roomsmin' => [ + 'name' => 'Pièces min', + 'type' => 'number' + ], + 'roomsmax' => [ + 'name' => 'Pièces max', + 'type' => 'number' + ], + 'squaremin' => [ + 'name' => 'Surface min', + 'type' => 'number' + ], + 'squaremax' => [ + 'name' => 'Surface max', + 'type' => 'number' + ], + 'mileagemin' => [ + 'name' => 'Kilométrage min', + 'type' => 'number' + ], + 'mileagemax' => [ + 'name' => 'Kilométrage max', + 'type' => 'number' + ], + 'yearmin' => [ + 'name' => 'Année min', + 'type' => 'number' + ], + 'yearmax' => [ + 'name' => 'Année max', + 'type' => 'number' + ], + 'cubiccapacitymin' => [ + 'name' => 'Cylindrée min', + 'type' => 'number' + ], + 'cubiccapacitymax' => [ + 'name' => 'Cylindrée max', + 'type' => 'number' + ], + 'fuel' => [ + 'name' => 'Énergie', + 'type' => 'list', + 'values' => [ + '' => '', + 'Essence' => '1', + 'Diesel' => '2', + 'GPL' => '3', + 'Électrique' => '4', + 'Hybride' => '6', + 'Autre' => '5' + ] + ], + 'owner' => [ + 'name' => 'Vendeur', + 'type' => 'list', + 'values' => [ + 'Tous' => '', + 'Particuliers' => 'private', + 'Professionnels' => 'pro' + ] + ] + ] + ]; + + public static $LBC_API_KEY = 'ba0c2dad52b3ec'; + + private function getRange($field, $range_min, $range_max) + { + if ( + !is_null($range_min) + && !is_null($range_max) + && $range_min > $range_max + ) { + returnClientError('Min-' . $field . ' must be lower than max-' . $field . '.'); + } + + if ( + !is_null($range_min) + && is_null($range_max) + ) { + returnClientError('Max-' . $field . ' is needed when min-' . $field . ' is setted (range).'); + } + + return [ + 'min' => $range_min, + 'max' => $range_max + ]; + } + + public function collectData() + { + $url = 'https://api.leboncoin.fr/api/adfinder/v1/search'; + $data = $this->buildRequestJson(); + + $header = [ + 'User-Agent: LBC;Android;10;SAMSUNG;phone;0aaaaaaaaaaaaaaa;wifi;8.24.3.8;152437;0', + 'Content-Type: application/json', + 'X-LBC-CC: 7', + 'Accept: application/json,application/hal+json', + 'Content-Length: ' . strlen($data), + 'api_key: ' . self::$LBC_API_KEY + ]; + + $opts = [ + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => $data + + ]; + + $content = getContents($url, $header, $opts); + + $json = json_decode($content); + + if ($json->total === 0) { + return; + } + + foreach ($json->ads as $element) { + $item['title'] = $element->subject; + $item['content'] = $element->body; + $item['date'] = $element->index_date; + $item['timestamp'] = strtotime($element->index_date); + $item['uri'] = $element->url; + $item['ad_type'] = $element->ad_type; + $item['author'] = $element->owner->name; + + if (isset($element->location->city)) { + $item['city'] = $element->location->city; + $item['content'] .= ' -- ' . $element->location->city; + } + + if (isset($element->location->zipcode)) { + $item['zipcode'] = $element->location->zipcode; + } + + if (isset($element->price)) { + $item['price'] = $element->price[0]; + $item['content'] .= ' -- ' . current($element->price) . '€'; + } + + if (isset($element->images->urls)) { + $item['thumbnail'] = $element->images->thumb_url; + $item['enclosures'] = []; + + foreach ($element->images->urls as $image) { + $item['enclosures'][] = $image; + } + } + + $this->items[] = $item; + } + } + + private function buildRequestJson() + { + $requestJson = new StdClass(); + $requestJson->owner_type = $this->getInput('owner'); + $requestJson->filters = new StdClass(); + + $requestJson->filters->keywords = [ + 'text' => $this->getInput('keywords') + ]; + + if ($this->getInput('region') != '') { + $requestJson->filters->location['regions'] = [$this->getInput('region')]; + } + + if ($this->getInput('department') != '') { + $requestJson->filters->location['departments'] = [$this->getInput('department')]; + } + + if ($this->getInput('cities') != '') { + $requestJson->filters->location['city_zipcodes'] = []; + + foreach (explode(',', $this->getInput('cities')) as $zipcode) { + $requestJson->filters->location['city_zipcodes'][] = [ + 'zipcode' => trim($zipcode) + ]; + } + } + + $requestJson->filters->category = [ + 'id' => $this->getInput('category') + ]; + + if ( + $this->getInput('pricemin') != '' + || $this->getInput('pricemax') != '' + ) { + $requestJson->filters->ranges->price = $this->getRange( + 'price', + $this->getInput('pricemin'), + $this->getInput('pricemax') + ); + } + + if ($this->getInput('estate') != '') { + $requestJson->filters->enums['real_estate_type'] = [$this->getInput('estate')]; + } + + if ( + $this->getInput('roomsmin') != '' + || $this->getInput('roomsmax') != '' + ) { + $requestJson->filters->ranges->rooms = $this->getRange( + 'rooms', + $this->getInput('roomsmin'), + $this->getInput('roomsmax') + ); + } + + if ( + $this->getInput('squaremin') != '' + || $this->getInput('squaremax') != '' + ) { + $requestJson->filters->ranges->square = $this->getRange( + 'square', + $this->getInput('squaremin'), + $this->getInput('squaremax') + ); + } + + if ( + $this->getInput('mileagemin') != '' + || $this->getInput('mileagemax') != '' + ) { + $requestJson->filters->ranges->mileage = $this->getRange( + 'mileage', + $this->getInput('mileagemin'), + $this->getInput('mileagemax') + ); + } + + if ( + $this->getInput('yearmin') != '' + || $this->getInput('yearmax') != '' + ) { + $requestJson->filters->ranges->regdate = $this->getRange( + 'year', + $this->getInput('yearmin'), + $this->getInput('yearmax') + ); + } + + if ( + $this->getInput('cubiccapacitymin') != '' + || $this->getInput('cubiccapacitymax') != '' + ) { + $requestJson->filters->ranges->cubic_capacity = $this->getRange( + 'cubic_capacity', + $this->getInput('cubiccapacitymin'), + $this->getInput('cubiccapacitymax') + ); + } + + if ($this->getInput('fuel') != '') { + $requestJson->filters->enums['fuel'] = [$this->getInput('fuel')]; + } + + $requestJson->limit = 30; + + return json_encode($requestJson); + } } diff --git a/bridges/LeMondeInformatiqueBridge.php b/bridges/LeMondeInformatiqueBridge.php index 823872fb..678e405f 100644 --- a/bridges/LeMondeInformatiqueBridge.php +++ b/bridges/LeMondeInformatiqueBridge.php @@ -1,39 +1,43 @@ <?php -class LeMondeInformatiqueBridge extends FeedExpander { - const MAINTAINER = 'ORelio'; - const NAME = 'Le Monde Informatique'; - const URI = 'https://www.lemondeinformatique.fr/'; - const DESCRIPTION = 'Returns the newest articles.'; +class LeMondeInformatiqueBridge extends FeedExpander +{ + const MAINTAINER = 'ORelio'; + const NAME = 'Le Monde Informatique'; + const URI = 'https://www.lemondeinformatique.fr/'; + const DESCRIPTION = 'Returns the newest articles.'; - public function collectData(){ - $this->collectExpandableDatas(self::URI . 'rss/rss.xml', 10); - } + public function collectData() + { + $this->collectExpandableDatas(self::URI . 'rss/rss.xml', 10); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $article_html = getSimpleHTMLDOMCached($item['uri']); + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $article_html = getSimpleHTMLDOMCached($item['uri']); - //Deduce thumbnail URL from article image URL - $item['enclosures'] = array( - str_replace( - '/grande/', - '/petite/', - $article_html->find('.article-image > img, figure > img', 0)->src - ) - ); + //Deduce thumbnail URL from article image URL + $item['enclosures'] = [ + str_replace( + '/grande/', + '/petite/', + $article_html->find('.article-image > img, figure > img', 0)->src + ) + ]; - //No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail - $content_node = $article_html->find('div.col-primary, div.col-sm-9', 0); - $item['content'] = $this->cleanArticle($content_node->innertext); - $item['author'] = $article_html->find('div.author-infos', 0)->find('b', 0)->plaintext; + //No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail + $content_node = $article_html->find('div.col-primary, div.col-sm-9', 0); + $item['content'] = $this->cleanArticle($content_node->innertext); + $item['author'] = $article_html->find('div.author-infos', 0)->find('b', 0)->plaintext; - return $item; - } + return $item; + } - private function cleanArticle($article_html){ - $article_html = stripWithDelimiters($article_html, '<script', '</script>'); - $article_html = explode('<p class="contact-error', $article_html)[0] . '</div>'; - return $article_html; - } + private function cleanArticle($article_html) + { + $article_html = stripWithDelimiters($article_html, '<script', '</script>'); + $article_html = explode('<p class="contact-error', $article_html)[0] . '</div>'; + return $article_html; + } } diff --git a/bridges/LegifranceJOBridge.php b/bridges/LegifranceJOBridge.php index cfbfad46..2d86c2ce 100644 --- a/bridges/LegifranceJOBridge.php +++ b/bridges/LegifranceJOBridge.php @@ -1,73 +1,77 @@ <?php -class LegifranceJOBridge extends BridgeAbstract { - const MAINTAINER = 'Pierre Mazière'; - const NAME = 'Journal Officiel de la République Française'; - // This uri returns a snippet of js. Should probably be https://www.legifrance.gouv.fr/jorf/jo/ - const URI = 'https://www.legifrance.gouv.fr/affichJO.do'; - const DESCRIPTION = 'Returns the laws and decrees officially registered daily in France'; +class LegifranceJOBridge extends BridgeAbstract +{ + const MAINTAINER = 'Pierre Mazière'; + const NAME = 'Journal Officiel de la République Française'; + // This uri returns a snippet of js. Should probably be https://www.legifrance.gouv.fr/jorf/jo/ + const URI = 'https://www.legifrance.gouv.fr/affichJO.do'; + const DESCRIPTION = 'Returns the laws and decrees officially registered daily in France'; - const PARAMETERS = array(); + const PARAMETERS = []; - private $author; - private $timestamp; - private $uri; + private $author; + private $timestamp; + private $uri; - private function extractItem($section, $subsection = null, $origin = null){ - $item = array(); - $item['author'] = $this->author; - $item['timestamp'] = $this->timestamp; - $item['uri'] = $this->uri . '#' . count($this->items); - $item['title'] = $section->plaintext; + private function extractItem($section, $subsection = null, $origin = null) + { + $item = []; + $item['author'] = $this->author; + $item['timestamp'] = $this->timestamp; + $item['uri'] = $this->uri . '#' . count($this->items); + $item['title'] = $section->plaintext; - if(!is_null($origin)) { - $item['title'] = '[ ' . $item['title'] . ' / ' . $subsection->plaintext . ' ] ' . $origin->plaintext; - $data = $origin; - } elseif(!is_null($subsection)) { - $item['title'] = '[ ' . $item['title'] . ' ] ' . $subsection->plaintext; - $data = $subsection; - } else { - $data = $section; - } + if (!is_null($origin)) { + $item['title'] = '[ ' . $item['title'] . ' / ' . $subsection->plaintext . ' ] ' . $origin->plaintext; + $data = $origin; + } elseif (!is_null($subsection)) { + $item['title'] = '[ ' . $item['title'] . ' ] ' . $subsection->plaintext; + $data = $subsection; + } else { + $data = $section; + } - $item['content'] = ''; - foreach($data->nextSibling()->find('a') as $content) { - $text = $content->plaintext; - $href = $content->nextSibling()->getAttribute('resource'); - $item['content'] .= '<p><a href="' . $href . '">' . $text . '</a></p>'; - } - return $item; - } + $item['content'] = ''; + foreach ($data->nextSibling()->find('a') as $content) { + $text = $content->plaintext; + $href = $content->nextSibling()->getAttribute('resource'); + $item['content'] .= '<p><a href="' . $href . '">' . $text . '</a></p>'; + } + return $item; + } - public function getIcon() { - return 'https://www.legifrance.gouv.fr/img/favicon.ico'; - } + public function getIcon() + { + return 'https://www.legifrance.gouv.fr/img/favicon.ico'; + } - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI) - or $this->returnServer('Unable to download ' . self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI) + or $this->returnServer('Unable to download ' . self::URI); - $this->author = trim($html->find('h2.titleJO', 0)->plaintext); - $uri = $html->find('h2.titleELI', 0)->plaintext; - $this->uri = trim(substr($uri, strpos($uri, 'https'))); - $this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5)); + $this->author = trim($html->find('h2.titleJO', 0)->plaintext); + $uri = $html->find('h2.titleELI', 0)->plaintext; + $this->uri = trim(substr($uri, strpos($uri, 'https'))); + $this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5)); - foreach($html->find('h3') as $section) { - $subsections = $section->nextSibling()->find('h4'); - foreach($subsections as $subsection) { - $origins = $subsection->nextSibling()->find('h5'); - foreach($origins as $origin) { - $this->items[] = $this->extractItem($section, $subsection, $origin); - } - if(!empty($origins)) { - continue; - } - $this->items[] = $this->extractItem($section, $subsection); - } - if(!empty($subsections)) { - continue; - } - $this->items[] = $this->extractItem($section); - } - } + foreach ($html->find('h3') as $section) { + $subsections = $section->nextSibling()->find('h4'); + foreach ($subsections as $subsection) { + $origins = $subsection->nextSibling()->find('h5'); + foreach ($origins as $origin) { + $this->items[] = $this->extractItem($section, $subsection, $origin); + } + if (!empty($origins)) { + continue; + } + $this->items[] = $this->extractItem($section, $subsection); + } + if (!empty($subsections)) { + continue; + } + $this->items[] = $this->extractItem($section); + } + } } diff --git a/bridges/LegoIdeasBridge.php b/bridges/LegoIdeasBridge.php index 442aba64..c4361f1f 100644 --- a/bridges/LegoIdeasBridge.php +++ b/bridges/LegoIdeasBridge.php @@ -1,96 +1,101 @@ <?php -class LegoIdeasBridge extends BridgeAbstract { - const NAME = 'Lego Ideas'; - const URI = 'https://ideas.lego.com/'; - const DESCRIPTION = 'Community Supported Lego Builds'; - const MAINTAINER = 'sal0max'; - const CACHE_TIMEOUT = 60 * 60 * 2; // 2h - const PARAMETERS = array( array( - 'support_value_min' => array( - 'name' => 'Minimum Supporters', - 'title' => 'The number of people that need to have supported a project at minimum. + +class LegoIdeasBridge extends BridgeAbstract +{ + const NAME = 'Lego Ideas'; + const URI = 'https://ideas.lego.com/'; + const DESCRIPTION = 'Community Supported Lego Builds'; + const MAINTAINER = 'sal0max'; + const CACHE_TIMEOUT = 60 * 60 * 2; // 2h + const PARAMETERS = [ [ + 'support_value_min' => [ + 'name' => 'Minimum Supporters', + 'title' => 'The number of people that need to have supported a project at minimum. Once a project reaches 10,000 supporters, it gets reviewed by the lego experts.', - 'type' => 'number', - 'defaultValue' => 1000 - ), - 'idea_phase' => array( - 'name' => 'Idea Phase', - 'type' => 'list', - 'values' => array( - 'Gathering Support' => 'idea_gathering_support', - 'Achieved Support' => 'idea_achieved_support', - 'In Review' => 'idea_in_review', - 'Approved Ideas' => 'idea_idea_approved', - 'Not Approved Ideas' => 'idea_idea_not_approved', - 'On Shelves' => 'idea_on_shelves', - 'Expired Ideas' => 'idea_expired_ideas', - ), - 'defaultValue' => 'idea_gathering_support' - ) - ) - ); + 'type' => 'number', + 'defaultValue' => 1000 + ], + 'idea_phase' => [ + 'name' => 'Idea Phase', + 'type' => 'list', + 'values' => [ + 'Gathering Support' => 'idea_gathering_support', + 'Achieved Support' => 'idea_achieved_support', + 'In Review' => 'idea_in_review', + 'Approved Ideas' => 'idea_idea_approved', + 'Not Approved Ideas' => 'idea_idea_not_approved', + 'On Shelves' => 'idea_on_shelves', + 'Expired Ideas' => 'idea_expired_ideas', + ], + 'defaultValue' => 'idea_gathering_support' + ] + ] + ]; - public function getURI() { - // link to the corresponding page on the website, not the api endpoint - return self::URI . 'search/global_search/ideas' - . "?support_value={$this->getInput('support_value_min')}" - . '&support_value=10000' - . "&idea_phase={$this->getInput('idea_phase')}" - . '&sort=most_recent'; - } + public function getURI() + { + // link to the corresponding page on the website, not the api endpoint + return self::URI . 'search/global_search/ideas' + . "?support_value={$this->getInput('support_value_min')}" + . '&support_value=10000' + . "&idea_phase={$this->getInput('idea_phase')}" + . '&sort=most_recent'; + } - public function collectData() { - $header = array( - 'Content-Type: application/json', - 'Accept: application/json' - ); - $opts = array( - CURLOPT_POST => 1, - CURLOPT_POSTFIELDS => $this->getHttpPostData() - ); - $responseData = getContents($this->getHttpPostURI(), $header, $opts) or - returnServerError('Unable to query Lego Ideas API.'); + public function collectData() + { + $header = [ + 'Content-Type: application/json', + 'Accept: application/json' + ]; + $opts = [ + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => $this->getHttpPostData() + ]; + $responseData = getContents($this->getHttpPostURI(), $header, $opts) or + returnServerError('Unable to query Lego Ideas API.'); - foreach (json_decode($responseData)->results as $project) { - preg_match('/datetime=\"(\S+)\"/', $project->entity->published_at, $date_matches); - $datetime = $date_matches[1]; - $link = self::URI . $project->entity->view_url; - $title = $project->entity->title; - $desc = $project->entity->content; - $imageUrl = $project->entity->image_url; - $creator = $project->entity->creator->alias; - $uuid = $project->entity->uuid; + foreach (json_decode($responseData)->results as $project) { + preg_match('/datetime=\"(\S+)\"/', $project->entity->published_at, $date_matches); + $datetime = $date_matches[1]; + $link = self::URI . $project->entity->view_url; + $title = $project->entity->title; + $desc = $project->entity->content; + $imageUrl = $project->entity->image_url; + $creator = $project->entity->creator->alias; + $uuid = $project->entity->uuid; - $item = array( - 'uri' => $link, - 'title' => $title, - 'timestamp' => strtotime($datetime), - 'author' => $creator, - 'content' => <<<EOD + $item = [ + 'uri' => $link, + 'title' => $title, + 'timestamp' => strtotime($datetime), + 'author' => $creator, + 'content' => <<<EOD <p><img src="{$imageUrl}" alt="{$title}"/></p> <p>{$desc}</p> EOD - ); - $this->items[] = $item; - } - } - - /** - * Returns the API endpoint - */ - private function getHttpPostURI() { - return self::URI . '/search/global_search/ideas'; - } + ]; + $this->items[] = $item; + } + } - /** - * Returns the API query - */ - private function getHttpPostData() { + /** + * Returns the API endpoint + */ + private function getHttpPostURI() + { + return self::URI . '/search/global_search/ideas'; + } - $phase = $this->getInput('idea_phase'); - $minSupporters = $this->getInput('support_value_min'); + /** + * Returns the API query + */ + private function getHttpPostData() + { + $phase = $this->getInput('idea_phase'); + $minSupporters = $this->getInput('support_value_min'); - return <<<EOD + return <<<EOD { "filters": { "idea_phase": [ "$phase" ], "support_value": [ $minSupporters, 10000 ] @@ -98,5 +103,5 @@ EOD "sort": [ "most_recent:desc" ] } EOD; - } + } } diff --git a/bridges/LesJoiesDuCodeBridge.php b/bridges/LesJoiesDuCodeBridge.php index 3f62de9b..a2a5e4b6 100644 --- a/bridges/LesJoiesDuCodeBridge.php +++ b/bridges/LesJoiesDuCodeBridge.php @@ -1,36 +1,38 @@ <?php -class LesJoiesDuCodeBridge extends BridgeAbstract { - const MAINTAINER = 'superbaillot.net'; - const NAME = 'Les Joies Du Code'; - const URI = 'https://lesjoiesducode.fr/'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'LesJoiesDuCode'; +class LesJoiesDuCodeBridge extends BridgeAbstract +{ + const MAINTAINER = 'superbaillot.net'; + const NAME = 'Les Joies Du Code'; + const URI = 'https://lesjoiesducode.fr/'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'LesJoiesDuCode'; - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - foreach($html->find('article.blog-post') as $element) { - $item = array(); - $temp = $element->find('h1 a', 0); - $titre = html_entity_decode($temp->innertext); - $url = $temp->href; + foreach ($html->find('article.blog-post') as $element) { + $item = []; + $temp = $element->find('h1 a', 0); + $titre = html_entity_decode($temp->innertext); + $url = $temp->href; - $temp = $element->find('div.blog-post-content', 0); + $temp = $element->find('div.blog-post-content', 0); - // retrieve .gif instead of static .jpg - $images = $temp->find('p img'); - foreach($images as $image) { - $img_src = str_replace('.jpg', '.gif', $image->src); - $image->src = $img_src; - } - $content = $temp->innertext; + // retrieve .gif instead of static .jpg + $images = $temp->find('p img'); + foreach ($images as $image) { + $img_src = str_replace('.jpg', '.gif', $image->src); + $image->src = $img_src; + } + $content = $temp->innertext; - $item['content'] = trim($content); - $item['uri'] = $url; - $item['title'] = trim($titre); + $item['content'] = trim($content); + $item['uri'] = $url; + $item['title'] = trim($titre); - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/ListverseBridge.php b/bridges/ListverseBridge.php index f597c0b4..ba6d7397 100644 --- a/bridges/ListverseBridge.php +++ b/bridges/ListverseBridge.php @@ -1,22 +1,25 @@ <?php -class ListverseBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'Listverse Bridge'; - const URI = 'https://listverse.com/'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'RSS feed for Listverse'; +class ListverseBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'Listverse Bridge'; + const URI = 'https://listverse.com/'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'RSS feed for Listverse'; - public function collectData(){ - $this->collectExpandableDatas('https://listverse.com/feed/', 15); - } + public function collectData() + { + $this->collectExpandableDatas('https://listverse.com/feed/', 15); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); - $article = $articlePage->find('#articlecontentonly', 0); - $item['content'] = $article; - return $item; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + $article = $articlePage->find('#articlecontentonly', 0); + $item['content'] = $article; + return $item; + } } diff --git a/bridges/LolibooruBridge.php b/bridges/LolibooruBridge.php index fb4689be..92d5fe9e 100644 --- a/bridges/LolibooruBridge.php +++ b/bridges/LolibooruBridge.php @@ -1,10 +1,9 @@ <?php -class LolibooruBridge extends MoebooruBridge { - - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Lolibooru'; - const URI = 'https://lolibooru.moe/'; - const DESCRIPTION = 'Returns images from given page and tags'; - +class LolibooruBridge extends MoebooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Lolibooru'; + const URI = 'https://lolibooru.moe/'; + const DESCRIPTION = 'Returns images from given page and tags'; } diff --git a/bridges/MallTvBridge.php b/bridges/MallTvBridge.php index 4556a62f..93a07c25 100644 --- a/bridges/MallTvBridge.php +++ b/bridges/MallTvBridge.php @@ -1,71 +1,76 @@ <?php -class MallTvBridge extends BridgeAbstract { - - const NAME = 'MALL.TV Bridge'; - const URI = 'https://www.mall.tv'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'Return newest videos'; - const MAINTAINER = 'kolarcz'; - - const PARAMETERS = array( - array( - 'url' => array( - 'name' => 'url to the show', - 'required' => true, - 'exampleValue' => 'https://www.mall.tv/zivot-je-hra' - ) - ) - ); - - private function fixChars($text) { - return html_entity_decode($text, ENT_QUOTES, 'UTF-8'); - } - - private function getUploadTimeFromUrl($url) { - $html = getSimpleHTMLDOM($url); - - $scriptLdJson = $html->find('script[type="application/ld+json"]', 0)->innertext; - if (!preg_match('/[\'"]uploadDate[\'"]\s*:\s*[\'"](\d{4}-\d{2}-\d{2})[\'"]/', $scriptLdJson, $match)) { - returnServerError('Could not get date from MALL.TV detail page'); - } - - return strtotime($match[1]); - } - - public function collectData() { - $url = $this->getInput('url'); - - if (!preg_match('/^https:\/\/www\.mall\.tv\/[a-z0-9-]+(\/[a-z0-9-]+)?\/?$/', $url)) { - returnServerError('Invalid url'); - } - - $html = getSimpleHTMLDOM($url); - - $this->feedUri = $url; - $this->feedName = $this->fixChars($html->find('title', 0)->plaintext); - - foreach ($html->find('section.isVideo .video-card') as $element) { - $itemTitle = $element->find('.video-card__details-link', 0); - $itemThumbnail = $element->find('.video-card__thumbnail', 0); - $itemUri = self::URI . $itemTitle->getAttribute('href'); - - $item = array( - 'title' => $this->fixChars($itemTitle->plaintext), - 'uri' => $itemUri, - 'content' => '<img src="' . $itemThumbnail->getAttribute('data-src') . '" />', - 'timestamp' => $this->getUploadTimeFromUrl($itemUri) - ); - - $this->items[] = $item; - } - } - - public function getURI() { - return isset($this->feedUri) ? $this->feedUri : parent::getURI(); - } - - public function getName() { - return isset($this->feedName) ? $this->feedName : parent::getName(); - } +class MallTvBridge extends BridgeAbstract +{ + const NAME = 'MALL.TV Bridge'; + const URI = 'https://www.mall.tv'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'Return newest videos'; + const MAINTAINER = 'kolarcz'; + + const PARAMETERS = [ + [ + 'url' => [ + 'name' => 'url to the show', + 'required' => true, + 'exampleValue' => 'https://www.mall.tv/zivot-je-hra' + ] + ] + ]; + + private function fixChars($text) + { + return html_entity_decode($text, ENT_QUOTES, 'UTF-8'); + } + + private function getUploadTimeFromUrl($url) + { + $html = getSimpleHTMLDOM($url); + + $scriptLdJson = $html->find('script[type="application/ld+json"]', 0)->innertext; + if (!preg_match('/[\'"]uploadDate[\'"]\s*:\s*[\'"](\d{4}-\d{2}-\d{2})[\'"]/', $scriptLdJson, $match)) { + returnServerError('Could not get date from MALL.TV detail page'); + } + + return strtotime($match[1]); + } + + public function collectData() + { + $url = $this->getInput('url'); + + if (!preg_match('/^https:\/\/www\.mall\.tv\/[a-z0-9-]+(\/[a-z0-9-]+)?\/?$/', $url)) { + returnServerError('Invalid url'); + } + + $html = getSimpleHTMLDOM($url); + + $this->feedUri = $url; + $this->feedName = $this->fixChars($html->find('title', 0)->plaintext); + + foreach ($html->find('section.isVideo .video-card') as $element) { + $itemTitle = $element->find('.video-card__details-link', 0); + $itemThumbnail = $element->find('.video-card__thumbnail', 0); + $itemUri = self::URI . $itemTitle->getAttribute('href'); + + $item = [ + 'title' => $this->fixChars($itemTitle->plaintext), + 'uri' => $itemUri, + 'content' => '<img src="' . $itemThumbnail->getAttribute('data-src') . '" />', + 'timestamp' => $this->getUploadTimeFromUrl($itemUri) + ]; + + $this->items[] = $item; + } + } + + public function getURI() + { + return isset($this->feedUri) ? $this->feedUri : parent::getURI(); + } + + public function getName() + { + return isset($this->feedName) ? $this->feedName : parent::getName(); + } } diff --git a/bridges/MangaDexBridge.php b/bridges/MangaDexBridge.php index 6cfd18e9..143cd732 100644 --- a/bridges/MangaDexBridge.php +++ b/bridges/MangaDexBridge.php @@ -1,232 +1,245 @@ <?php -class MangaDexBridge extends BridgeAbstract { - const NAME = 'MangaDex Bridge'; - const URI = 'https://mangadex.org/'; - const API_ROOT = 'https://api.mangadex.org/'; - const DESCRIPTION = 'Returns MangaDex items using the API'; - - const PARAMETERS = array( - 'global' => array( - 'limit' => array( - 'name' => 'Item Limit', - 'type' => 'number', - 'defaultValue' => 10, - 'required' => true - ), - 'lang' => array( - 'name' => 'Chapter Languages (default=all)', - 'title' => 'comma-separated, two-letter language codes (example "en,jp")', - 'exampleValue' => 'en,jp', - 'required' => false - ), - ), - 'Title Chapters' => array( - 'url' => array( - 'name' => 'URL to title page', - 'exampleValue' => 'https://mangadex.org/title/f9c33607-9180-4ba6-b85c-e4b5faee7192/official-test-manga', - 'required' => true - ), - 'external' => array( - 'name' => 'Allow external feed items', - 'type' => 'checkbox', - 'title' => 'Some chapters are inaccessible or only available on an external site. Include these?' - ) - ), - 'Search Chapters' => array( - 'chapter' => array( - 'name' => 'Chapter Number (default=all)', - 'title' => 'The example value finds the newest first chapters', - 'exampleValue' => 1, - 'required' => false - ), - 'groups' => array( - 'name' => 'Group UUID (default=all)', - 'title' => 'This can be found in the MangaDex Group Page URL', - 'exampleValue' => '00e03853-1b96-4f41-9542-c71b8692033b', - 'required' => false, - ), - 'uploader' => array( - 'name' => 'User UUID (default=all)', - 'title' => 'This can be found in the MangaDex User Page URL', - 'exampleValue' => 'd2ae45e0-b5e2-4e7f-a688-17925c2d7d6b', - 'required' => false, - ), - 'external' => array( - 'name' => 'Allow external feed items', - 'type' => 'checkbox', - 'title' => 'Some chapters are inaccessible or only available on an external site. Include these?' - ) - ) - // Future Manga Contexts: - // Manga List (by author or tags): https://api.mangadex.org/swagger.html#/Manga/get-search-manga - // Random Manga: https://api.mangadex.org/swagger.html#/Manga/get-manga-random - // Future Chapter Contexts: - // User Lists https://api.mangadex.org/swagger.html#/Feed/get-list-id-feed - // - // https://api.mangadex.org/docs/get-covers/ - ); - - const TITLE_REGEX = '#title/(?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})#'; - - protected $feedName = ''; - protected $feedURI = ''; - - protected function buildArrayQuery($name, $array) { - $query = ''; - foreach($array as $item) { - $query .= '&' . $name . '=' . $item; - } - return $query; - } - - protected function getAPI() { - $params = array( - 'limit' => $this->getInput('limit') - ); - - $array_params = array(); - if (!empty($this->getInput('lang'))) { - $array_params['translatedLanguage[]'] = explode(',', $this->getInput('lang')); - } - - switch($this->queriedContext) { - case 'Title Chapters': - preg_match(self::TITLE_REGEX, $this->getInput('url'), $matches) - or returnClientError('Invalid URL Parameter'); - $this->feedURI = self::URI . 'title/' . $matches['uuid']; - $params['order[readableAt]'] = 'desc'; - if (!$this->getInput('external')) { - $params['includeFutureUpdates'] = '0'; - } - $array_params['includes[]'] = array('manga', 'scanlation_group', 'user'); - $uri = self::API_ROOT . 'manga/' . $matches['uuid'] . '/feed'; - break; - case 'Search Chapters': - $params['chapter'] = $this->getInput('chapter'); - $params['groups[]'] = $this->getInput('groups'); - $params['uploader'] = $this->getInput('uploader'); - $params['order[readableAt]'] = 'desc'; - if (!$this->getInput('external')) { - $params['includeFutureUpdates'] = '0'; - } - $array_params['includes[]'] = array('manga', 'scanlation_group', 'user'); - $uri = self::API_ROOT . 'chapter'; - break; - default: - returnServerError('Unimplemented Context (getAPI)'); - } - - // Remove null keys - $params = array_filter($params, function($v) { - return !empty($v); - }); - - $uri .= '?' . http_build_query($params); - - // Arrays are passed as repeated keys to MangaDex - // This cannot be handled by http_build_query - foreach($array_params as $name => $array_param) { - $uri .= $this->buildArrayQuery($name, $array_param); - } - - return $uri; - } - - public function getName() { - switch($this->queriedContext) { - case 'Title Chapters': - return $this->feedName . ' Chapters'; - case 'Search Chapters': - return 'MangaDex Chapter Search'; - default: - return parent::getName(); - } - } - - public function getURI() { - switch($this->queriedContext) { - case 'Title Chapters': - return $this->feedURI; - default: - return parent::getURI(); - } - } - - public function collectData() { - $api_uri = $this->getAPI(); - $header = array( - 'Content-Type: application/json' - ); - $content = json_decode(getContents($api_uri, $header), true); - if ($content['result'] == 'ok') { - $content = $content['data']; - } else { - returnServerError('Could not retrieve API results'); - } - - switch($this->queriedContext) { - case 'Title Chapters': - $this->getChapters($content); - break; - case 'Search Chapters': - $this->getChapters($content); - break; - default: - returnServerError('Unimplemented Context (collectData)'); - } - } - - protected function getChapters($content) { - foreach($content as $chapter) { - $item = array(); - $item['uid'] = $chapter['id']; - $item['uri'] = self::URI . 'chapter/' . $chapter['id']; - - // External chapter - if (!$this->getInput('external') && $chapter['attributes']['pages'] == 0) - continue; - - $item['title'] = ''; - if (isset($chapter['attributes']['volume'])) - $item['title'] .= 'Volume ' . $chapter['attributes']['volume'] . ' '; - if (isset($chapter['attributes']['chapter'])) - $item['title'] .= 'Chapter ' . $chapter['attributes']['chapter']; - if (!empty($chapter['attributes']['title'])) { - $item['title'] .= ' - ' . $chapter['attributes']['title']; - } - $item['title'] .= ' [' . $chapter['attributes']['translatedLanguage'] . ']'; - - $item['timestamp'] = $chapter['attributes']['readableAt']; - - $groups = array(); - $users = array(); - foreach($chapter['relationships'] as $rel) { - switch($rel['type']) { - case 'scanlation_group': - $groups[] = $rel['attributes']['name']; - break; - case 'manga': - if (empty($this->feedName)) - $this->feedName = reset($rel['attributes']['title']); - if ($this->queriedContext !== 'Title Chapters') - $item['title'] = reset($rel['attributes']['title']) . ' ' . $item['title']; - break; - case 'user': - if (isset($item['author'])) { - $users[] = $rel['attributes']['username']; - } else { - $item['author'] = $rel['attributes']['username']; - } - break; - } - } - $item['content'] = 'Groups: ' . - (empty($groups) ? 'No Group' : implode(', ', $groups)); - if (!empty($users)) { - $item['content'] .= '<br>Other Users: ' . implode(', ', $users); - } - - $this->items[] = $item; - } - } + +class MangaDexBridge extends BridgeAbstract +{ + const NAME = 'MangaDex Bridge'; + const URI = 'https://mangadex.org/'; + const API_ROOT = 'https://api.mangadex.org/'; + const DESCRIPTION = 'Returns MangaDex items using the API'; + + const PARAMETERS = [ + 'global' => [ + 'limit' => [ + 'name' => 'Item Limit', + 'type' => 'number', + 'defaultValue' => 10, + 'required' => true + ], + 'lang' => [ + 'name' => 'Chapter Languages (default=all)', + 'title' => 'comma-separated, two-letter language codes (example "en,jp")', + 'exampleValue' => 'en,jp', + 'required' => false + ], + ], + 'Title Chapters' => [ + 'url' => [ + 'name' => 'URL to title page', + 'exampleValue' => 'https://mangadex.org/title/f9c33607-9180-4ba6-b85c-e4b5faee7192/official-test-manga', + 'required' => true + ], + 'external' => [ + 'name' => 'Allow external feed items', + 'type' => 'checkbox', + 'title' => 'Some chapters are inaccessible or only available on an external site. Include these?' + ] + ], + 'Search Chapters' => [ + 'chapter' => [ + 'name' => 'Chapter Number (default=all)', + 'title' => 'The example value finds the newest first chapters', + 'exampleValue' => 1, + 'required' => false + ], + 'groups' => [ + 'name' => 'Group UUID (default=all)', + 'title' => 'This can be found in the MangaDex Group Page URL', + 'exampleValue' => '00e03853-1b96-4f41-9542-c71b8692033b', + 'required' => false, + ], + 'uploader' => [ + 'name' => 'User UUID (default=all)', + 'title' => 'This can be found in the MangaDex User Page URL', + 'exampleValue' => 'd2ae45e0-b5e2-4e7f-a688-17925c2d7d6b', + 'required' => false, + ], + 'external' => [ + 'name' => 'Allow external feed items', + 'type' => 'checkbox', + 'title' => 'Some chapters are inaccessible or only available on an external site. Include these?' + ] + ] + // Future Manga Contexts: + // Manga List (by author or tags): https://api.mangadex.org/swagger.html#/Manga/get-search-manga + // Random Manga: https://api.mangadex.org/swagger.html#/Manga/get-manga-random + // Future Chapter Contexts: + // User Lists https://api.mangadex.org/swagger.html#/Feed/get-list-id-feed + // + // https://api.mangadex.org/docs/get-covers/ + ]; + + const TITLE_REGEX = '#title/(?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})#'; + + protected $feedName = ''; + protected $feedURI = ''; + + protected function buildArrayQuery($name, $array) + { + $query = ''; + foreach ($array as $item) { + $query .= '&' . $name . '=' . $item; + } + return $query; + } + + protected function getAPI() + { + $params = [ + 'limit' => $this->getInput('limit') + ]; + + $array_params = []; + if (!empty($this->getInput('lang'))) { + $array_params['translatedLanguage[]'] = explode(',', $this->getInput('lang')); + } + + switch ($this->queriedContext) { + case 'Title Chapters': + preg_match(self::TITLE_REGEX, $this->getInput('url'), $matches) + or returnClientError('Invalid URL Parameter'); + $this->feedURI = self::URI . 'title/' . $matches['uuid']; + $params['order[readableAt]'] = 'desc'; + if (!$this->getInput('external')) { + $params['includeFutureUpdates'] = '0'; + } + $array_params['includes[]'] = ['manga', 'scanlation_group', 'user']; + $uri = self::API_ROOT . 'manga/' . $matches['uuid'] . '/feed'; + break; + case 'Search Chapters': + $params['chapter'] = $this->getInput('chapter'); + $params['groups[]'] = $this->getInput('groups'); + $params['uploader'] = $this->getInput('uploader'); + $params['order[readableAt]'] = 'desc'; + if (!$this->getInput('external')) { + $params['includeFutureUpdates'] = '0'; + } + $array_params['includes[]'] = ['manga', 'scanlation_group', 'user']; + $uri = self::API_ROOT . 'chapter'; + break; + default: + returnServerError('Unimplemented Context (getAPI)'); + } + + // Remove null keys + $params = array_filter($params, function ($v) { + return !empty($v); + }); + + $uri .= '?' . http_build_query($params); + + // Arrays are passed as repeated keys to MangaDex + // This cannot be handled by http_build_query + foreach ($array_params as $name => $array_param) { + $uri .= $this->buildArrayQuery($name, $array_param); + } + + return $uri; + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Title Chapters': + return $this->feedName . ' Chapters'; + case 'Search Chapters': + return 'MangaDex Chapter Search'; + default: + return parent::getName(); + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Title Chapters': + return $this->feedURI; + default: + return parent::getURI(); + } + } + + public function collectData() + { + $api_uri = $this->getAPI(); + $header = [ + 'Content-Type: application/json' + ]; + $content = json_decode(getContents($api_uri, $header), true); + if ($content['result'] == 'ok') { + $content = $content['data']; + } else { + returnServerError('Could not retrieve API results'); + } + + switch ($this->queriedContext) { + case 'Title Chapters': + $this->getChapters($content); + break; + case 'Search Chapters': + $this->getChapters($content); + break; + default: + returnServerError('Unimplemented Context (collectData)'); + } + } + + protected function getChapters($content) + { + foreach ($content as $chapter) { + $item = []; + $item['uid'] = $chapter['id']; + $item['uri'] = self::URI . 'chapter/' . $chapter['id']; + + // External chapter + if (!$this->getInput('external') && $chapter['attributes']['pages'] == 0) { + continue; + } + + $item['title'] = ''; + if (isset($chapter['attributes']['volume'])) { + $item['title'] .= 'Volume ' . $chapter['attributes']['volume'] . ' '; + } + if (isset($chapter['attributes']['chapter'])) { + $item['title'] .= 'Chapter ' . $chapter['attributes']['chapter']; + } + if (!empty($chapter['attributes']['title'])) { + $item['title'] .= ' - ' . $chapter['attributes']['title']; + } + $item['title'] .= ' [' . $chapter['attributes']['translatedLanguage'] . ']'; + + $item['timestamp'] = $chapter['attributes']['readableAt']; + + $groups = []; + $users = []; + foreach ($chapter['relationships'] as $rel) { + switch ($rel['type']) { + case 'scanlation_group': + $groups[] = $rel['attributes']['name']; + break; + case 'manga': + if (empty($this->feedName)) { + $this->feedName = reset($rel['attributes']['title']); + } + if ($this->queriedContext !== 'Title Chapters') { + $item['title'] = reset($rel['attributes']['title']) . ' ' . $item['title']; + } + break; + case 'user': + if (isset($item['author'])) { + $users[] = $rel['attributes']['username']; + } else { + $item['author'] = $rel['attributes']['username']; + } + break; + } + } + $item['content'] = 'Groups: ' . + (empty($groups) ? 'No Group' : implode(', ', $groups)); + if (!empty($users)) { + $item['content'] .= '<br>Other Users: ' . implode(', ', $users); + } + + $this->items[] = $item; + } + } } diff --git a/bridges/MarktplaatsBridge.php b/bridges/MarktplaatsBridge.php index ccbb64f9..136b85b4 100644 --- a/bridges/MarktplaatsBridge.php +++ b/bridges/MarktplaatsBridge.php @@ -1,130 +1,133 @@ <?php -class MarktplaatsBridge extends BridgeAbstract { - const NAME = 'Marktplaats'; - const URI = 'https://marktplaats.nl'; - const DESCRIPTION = 'Read search queries from marktplaats.nl'; - const PARAMETERS = array( - 'Search' => array( - 'q' => array( - 'name' => 'query', - 'type' => 'text', - 'exampleValue' => 'lamp', - 'required' => true, - 'title' => 'The search string for marktplaats', - ), - 'z' => array( - 'name' => 'zipcode', - 'type' => 'text', - 'required' => false, - 'exampleValue' => '1013AA', - 'title' => 'Zip code for location limited searches', - ), - 'd' => array( - 'name' => 'distance', - 'type' => 'number', - 'required' => false, - 'exampleValue' => '100000', - 'title' => 'The distance in meters from the zipcode', - ), - 'f' => array( - 'name' => 'priceFrom', - 'type' => 'number', - 'required' => false, - 'title' => 'The minimal price in cents', - ), - 't' => array( - 'name' => 'priceTo', - 'type' => 'number', - 'required' => false, - 'title' => 'The maximal price in cents', - ), - 's' => array( - 'name' => 'showGlobal', - 'type' => 'checkbox', - 'required' => false, - 'title' => 'Include result with negative distance', - ), - 'i' => array( - 'name' => 'includeImage', - 'type' => 'checkbox', - 'required' => false, - 'title' => 'Include the image at the end of the content', - ), - 'r' => array( - 'name' => 'includeRaw', - 'type' => 'checkbox', - 'required' => false, - 'title' => 'Include the raw data behind the content', - ) - ) - ); - const CACHE_TIMEOUT = 900; +class MarktplaatsBridge extends BridgeAbstract +{ + const NAME = 'Marktplaats'; + const URI = 'https://marktplaats.nl'; + const DESCRIPTION = 'Read search queries from marktplaats.nl'; + const PARAMETERS = [ + 'Search' => [ + 'q' => [ + 'name' => 'query', + 'type' => 'text', + 'exampleValue' => 'lamp', + 'required' => true, + 'title' => 'The search string for marktplaats', + ], + 'z' => [ + 'name' => 'zipcode', + 'type' => 'text', + 'required' => false, + 'exampleValue' => '1013AA', + 'title' => 'Zip code for location limited searches', + ], + 'd' => [ + 'name' => 'distance', + 'type' => 'number', + 'required' => false, + 'exampleValue' => '100000', + 'title' => 'The distance in meters from the zipcode', + ], + 'f' => [ + 'name' => 'priceFrom', + 'type' => 'number', + 'required' => false, + 'title' => 'The minimal price in cents', + ], + 't' => [ + 'name' => 'priceTo', + 'type' => 'number', + 'required' => false, + 'title' => 'The maximal price in cents', + ], + 's' => [ + 'name' => 'showGlobal', + 'type' => 'checkbox', + 'required' => false, + 'title' => 'Include result with negative distance', + ], + 'i' => [ + 'name' => 'includeImage', + 'type' => 'checkbox', + 'required' => false, + 'title' => 'Include the image at the end of the content', + ], + 'r' => [ + 'name' => 'includeRaw', + 'type' => 'checkbox', + 'required' => false, + 'title' => 'Include the raw data behind the content', + ] + ] + ]; + const CACHE_TIMEOUT = 900; - public function collectData() { - $query = ''; - $excludeGlobal = false; - if(!is_null($this->getInput('z')) && !is_null($this->getInput('d'))) { - $query = '&postcode=' . $this->getInput('z') . '&distanceMeters=' . $this->getInput('d'); - } - if(!is_null($this->getInput('f'))) { - $query .= '&PriceCentsFrom=' . $this->getInput('f'); - } - if(!is_null($this->getInput('t'))) { - $query .= '&PriceCentsTo=' . $this->getInput('t'); - } - if(!is_null($this->getInput('s'))) { - if(!$this->getInput('s')) { - $excludeGlobal = true; - } - } - $url = 'https://www.marktplaats.nl/lrp/api/search?query=' . urlencode($this->getInput('q')) . $query; - $jsonString = getSimpleHTMLDOM($url); - $jsonObj = json_decode($jsonString); - foreach($jsonObj->listings as $listing) { - if(!$excludeGlobal || $listing->location->distanceMeters >= 0) { - $item = array(); - $item['uri'] = 'https://marktplaats.nl' . $listing->vipUrl; - $item['title'] = $listing->title; - $item['timestamp'] = $listing->date; - $item['author'] = $listing->sellerInformation->sellerName; - $item['content'] = $listing->description; - $item['categories'] = $listing->verticals; - $item['uid'] = $listing->itemId; - if(!is_null($this->getInput('i')) && !empty($listing->imageUrls)) { - $item['enclosures'] = $listing->imageUrls; - if(is_array($listing->imageUrls)) { - foreach($listing->imageUrls as $imgurl) { - $item['content'] .= "<br />\n<img src='https:" . $imgurl . "' />"; - } - } else { - $item['content'] .= "<br>\n<img src='https:" . $listing->imageUrls . "' />"; - } - } - if(!is_null($this->getInput('r'))) { - if($this->getInput('r')) { - $item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing); - } - } - $item['content'] .= "<br>\n<br>\nPrice: " . $listing->priceInfo->priceCents / 100; - $item['content'] .= ' (' . $listing->priceInfo->priceType . ')'; - if(!empty($listing->location->cityName)) { - $item['content'] .= "<br><br>\n" . $listing->location->cityName; - } - if(!is_null($this->getInput('r'))) { - if($this->getInput('r')) { - $item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing); - } - } - $this->items[] = $item; - } - } - } + public function collectData() + { + $query = ''; + $excludeGlobal = false; + if (!is_null($this->getInput('z')) && !is_null($this->getInput('d'))) { + $query = '&postcode=' . $this->getInput('z') . '&distanceMeters=' . $this->getInput('d'); + } + if (!is_null($this->getInput('f'))) { + $query .= '&PriceCentsFrom=' . $this->getInput('f'); + } + if (!is_null($this->getInput('t'))) { + $query .= '&PriceCentsTo=' . $this->getInput('t'); + } + if (!is_null($this->getInput('s'))) { + if (!$this->getInput('s')) { + $excludeGlobal = true; + } + } + $url = 'https://www.marktplaats.nl/lrp/api/search?query=' . urlencode($this->getInput('q')) . $query; + $jsonString = getSimpleHTMLDOM($url); + $jsonObj = json_decode($jsonString); + foreach ($jsonObj->listings as $listing) { + if (!$excludeGlobal || $listing->location->distanceMeters >= 0) { + $item = []; + $item['uri'] = 'https://marktplaats.nl' . $listing->vipUrl; + $item['title'] = $listing->title; + $item['timestamp'] = $listing->date; + $item['author'] = $listing->sellerInformation->sellerName; + $item['content'] = $listing->description; + $item['categories'] = $listing->verticals; + $item['uid'] = $listing->itemId; + if (!is_null($this->getInput('i')) && !empty($listing->imageUrls)) { + $item['enclosures'] = $listing->imageUrls; + if (is_array($listing->imageUrls)) { + foreach ($listing->imageUrls as $imgurl) { + $item['content'] .= "<br />\n<img src='https:" . $imgurl . "' />"; + } + } else { + $item['content'] .= "<br>\n<img src='https:" . $listing->imageUrls . "' />"; + } + } + if (!is_null($this->getInput('r'))) { + if ($this->getInput('r')) { + $item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing); + } + } + $item['content'] .= "<br>\n<br>\nPrice: " . $listing->priceInfo->priceCents / 100; + $item['content'] .= ' (' . $listing->priceInfo->priceType . ')'; + if (!empty($listing->location->cityName)) { + $item['content'] .= "<br><br>\n" . $listing->location->cityName; + } + if (!is_null($this->getInput('r'))) { + if ($this->getInput('r')) { + $item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing); + } + } + $this->items[] = $item; + } + } + } - public function getName(){ - if(!is_null($this->getInput('q'))) { - return $this->getInput('q') . ' - Marktplaats'; - } - return parent::getName(); - } + public function getName() + { + if (!is_null($this->getInput('q'))) { + return $this->getInput('q') . ' - Marktplaats'; + } + return parent::getName(); + } } diff --git a/bridges/MastodonBridge.php b/bridges/MastodonBridge.php index bbbc5587..04c92ba5 100644 --- a/bridges/MastodonBridge.php +++ b/bridges/MastodonBridge.php @@ -1,196 +1,206 @@ <?php -class MastodonBridge extends BridgeAbstract { - // This script attempts to imitiate the behaviour of a read-only ActivityPub server - // to read the outbox. +class MastodonBridge extends BridgeAbstract +{ + // This script attempts to imitiate the behaviour of a read-only ActivityPub server + // to read the outbox. - // Note: Most PixelFed instances have ActivityPub outbox disabled, - // so use the official feed: https://pixelfed.instance/users/username.atom (Posts only) + // Note: Most PixelFed instances have ActivityPub outbox disabled, + // so use the official feed: https://pixelfed.instance/users/username.atom (Posts only) - const MAINTAINER = 'Austin Huang'; - const NAME = 'ActivityPub Bridge'; - const CACHE_TIMEOUT = 900; // 15mn - const DESCRIPTION = 'Returns recent statuses. Supports Mastodon, Pleroma and Misskey, among others. Access to + const MAINTAINER = 'Austin Huang'; + const NAME = 'ActivityPub Bridge'; + const CACHE_TIMEOUT = 900; // 15mn + const DESCRIPTION = 'Returns recent statuses. Supports Mastodon, Pleroma and Misskey, among others. Access to instances that have Authorized Fetch enabled requires <a href="https://rss-bridge.github.io/rss-bridge/Bridge_Specific/ActivityPub_(Mastodon).html">configuration</a>.'; - const URI = 'https://mastodon.social'; + const URI = 'https://mastodon.social'; - // Some Mastodon instances use Secure Mode which requires all requests to be signed. - // You do not need this for most instances, but if you want to support every known - // instance, then you should configure them. - // See also https://docs.joinmastodon.org/spec/security/#http - const CONFIGURATION = array( - 'private_key' => array( - 'required' => false, - ), - 'key_id' => array( - 'required' => false, - ), - ); + // Some Mastodon instances use Secure Mode which requires all requests to be signed. + // You do not need this for most instances, but if you want to support every known + // instance, then you should configure them. + // See also https://docs.joinmastodon.org/spec/security/#http + const CONFIGURATION = [ + 'private_key' => [ + 'required' => false, + ], + 'key_id' => [ + 'required' => false, + ], + ]; - const PARAMETERS = array(array( - 'canusername' => array( - 'name' => 'Canonical username', - 'exampleValue' => '@sebsauvage@framapiaf.org', - 'required' => true, - ), - 'norep' => array( - 'name' => 'Without replies', - 'type' => 'checkbox', - 'title' => 'Only return statuses that are not replies, as determined by relations (not mentions).' - ), - 'noboost' => array( - 'name' => 'Without boosts', - 'required' => false, - 'type' => 'checkbox', - 'title' => 'Hide boosts. Note that RSS-Bridge will fetch the original status from other federated instances.' - ) - )); + const PARAMETERS = [[ + 'canusername' => [ + 'name' => 'Canonical username', + 'exampleValue' => '@sebsauvage@framapiaf.org', + 'required' => true, + ], + 'norep' => [ + 'name' => 'Without replies', + 'type' => 'checkbox', + 'title' => 'Only return statuses that are not replies, as determined by relations (not mentions).' + ], + 'noboost' => [ + 'name' => 'Without boosts', + 'required' => false, + 'type' => 'checkbox', + 'title' => 'Hide boosts. Note that RSS-Bridge will fetch the original status from other federated instances.' + ] + ]]; - public function getName() { - if($this->getInput('canusername')) { - return $this->getInput('canusername'); - } - return parent::getName(); - } + public function getName() + { + if ($this->getInput('canusername')) { + return $this->getInput('canusername'); + } + return parent::getName(); + } - private function getInstance() { - preg_match('/^@[a-zA-Z0-9_]+@(.+)/', $this->getInput('canusername'), $matches); - return $matches[1]; - } + private function getInstance() + { + preg_match('/^@[a-zA-Z0-9_]+@(.+)/', $this->getInput('canusername'), $matches); + return $matches[1]; + } - private function getUsername() { - preg_match('/^@([a-zA-Z_0-9_]+)@.+/', $this->getInput('canusername'), $matches); - return $matches[1]; - } + private function getUsername() + { + preg_match('/^@([a-zA-Z_0-9_]+)@.+/', $this->getInput('canusername'), $matches); + return $matches[1]; + } - public function getURI(){ - if($this->getInput('canusername')) { - // We parse webfinger to make sure the URL is correct. This is mostly because - // MissKey uses user ID instead of the username in the endpoint, domain delegations, - // and also to be compatible with future ActivityPub implementations. - $resource = 'acct:' . $this->getUsername() . '@' . $this->getInstance(); - $webfingerUrl = 'https://' . $this->getInstance() . '/.well-known/webfinger?resource=' . $resource; - $webfingerHeader = array( - 'Content-Type: application/jrd+json' - ); - $webfinger = json_decode(getContents($webfingerUrl, $webfingerHeader), true); - foreach ($webfinger['links'] as $link) { - if ($link['type'] === 'application/activity+json') { - return $link['href']; - } - } - } + public function getURI() + { + if ($this->getInput('canusername')) { + // We parse webfinger to make sure the URL is correct. This is mostly because + // MissKey uses user ID instead of the username in the endpoint, domain delegations, + // and also to be compatible with future ActivityPub implementations. + $resource = 'acct:' . $this->getUsername() . '@' . $this->getInstance(); + $webfingerUrl = 'https://' . $this->getInstance() . '/.well-known/webfinger?resource=' . $resource; + $webfingerHeader = [ + 'Content-Type: application/jrd+json' + ]; + $webfinger = json_decode(getContents($webfingerUrl, $webfingerHeader), true); + foreach ($webfinger['links'] as $link) { + if ($link['type'] === 'application/activity+json') { + return $link['href']; + } + } + } - return parent::getURI(); - } + return parent::getURI(); + } - public function collectData() { - $url = $this->getURI() . '/outbox?page=true'; - $content = $this->fetchAP($url); - if ($content['id'] === $url) { - foreach ($content['orderedItems'] as $status) { - $this->items[] = $this->parseItem($status); - } - } else { - throw new \Exception('Unexpected response from server.'); - } - } + public function collectData() + { + $url = $this->getURI() . '/outbox?page=true'; + $content = $this->fetchAP($url); + if ($content['id'] === $url) { + foreach ($content['orderedItems'] as $status) { + $this->items[] = $this->parseItem($status); + } + } else { + throw new \Exception('Unexpected response from server.'); + } + } - protected function parseItem($content) { - $item = array(); - switch ($content['type']) { - case 'Announce': // boost - if ($this->getInput('noboost')) { - return null; - } - // We fetch the boosted content. - try { - $rtContent = $this->fetchAP($content['object']); - $rtUser = $this->loadCacheValue($rtContent['attributedTo'], 86400); - if (!isset($rtUser)) { - // We fetch the author, since we cannot always assume the format of the URL. - $user = $this->fetchAP($rtContent['attributedTo']); - preg_match('/https?:\/\/([a-z0-9-\.]{0,})\//', $rtContent['attributedTo'], $matches); - // We assume that the server name as indicated by the path is the actual server name, - // since using webfinger to delegate domains is not officially supported, and it only - // seems to work in one way. - $rtUser = '@' . $user['preferredUsername'] . '@' . $matches[1]; - $this->saveCacheValue($rtContent['attributedTo'], $rtUser); - } - $item['author'] = $rtUser; - $item['title'] = 'Shared a status by ' . $rtUser . ': '; - $item = $this->parseObject($rtContent, $item); - } catch (UnexpectedResponseException $th) { - $item['title'] = 'Shared an unreachable status: ' . $content['object']; - $item['content'] = $content['object']; - $item['uri'] = $content['object']; - } - break; - case 'Create': // posts - if ($this->getInput('norep') && isset($content['object']['inReplyTo'])) { - return null; - } - $item['author'] = $this->getInput('canusername'); - $item['title'] = ''; - $item = $this->parseObject($content['object'], $item); - } - $item['timestamp'] = $content['published']; - $item['uid'] = $content['id']; - return $item; - } + protected function parseItem($content) + { + $item = []; + switch ($content['type']) { + case 'Announce': // boost + if ($this->getInput('noboost')) { + return null; + } + // We fetch the boosted content. + try { + $rtContent = $this->fetchAP($content['object']); + $rtUser = $this->loadCacheValue($rtContent['attributedTo'], 86400); + if (!isset($rtUser)) { + // We fetch the author, since we cannot always assume the format of the URL. + $user = $this->fetchAP($rtContent['attributedTo']); + preg_match('/https?:\/\/([a-z0-9-\.]{0,})\//', $rtContent['attributedTo'], $matches); + // We assume that the server name as indicated by the path is the actual server name, + // since using webfinger to delegate domains is not officially supported, and it only + // seems to work in one way. + $rtUser = '@' . $user['preferredUsername'] . '@' . $matches[1]; + $this->saveCacheValue($rtContent['attributedTo'], $rtUser); + } + $item['author'] = $rtUser; + $item['title'] = 'Shared a status by ' . $rtUser . ': '; + $item = $this->parseObject($rtContent, $item); + } catch (UnexpectedResponseException $th) { + $item['title'] = 'Shared an unreachable status: ' . $content['object']; + $item['content'] = $content['object']; + $item['uri'] = $content['object']; + } + break; + case 'Create': // posts + if ($this->getInput('norep') && isset($content['object']['inReplyTo'])) { + return null; + } + $item['author'] = $this->getInput('canusername'); + $item['title'] = ''; + $item = $this->parseObject($content['object'], $item); + } + $item['timestamp'] = $content['published']; + $item['uid'] = $content['id']; + return $item; + } - protected function parseObject($object, $item) { - $item['content'] = $object['content']; - $strippedContent = strip_tags(str_replace('<br>', ' ', $object['content'])); + protected function parseObject($object, $item) + { + $item['content'] = $object['content']; + $strippedContent = strip_tags(str_replace('<br>', ' ', $object['content'])); - if (mb_strlen($strippedContent) > 75) { - $contentSubstring = mb_substr($strippedContent, 0, mb_strpos(wordwrap($strippedContent, 75), "\n")); - $item['title'] .= $contentSubstring . '...'; - } else { - $item['title'] .= $strippedContent; - } - $item['uri'] = $object['id']; - foreach ($object['attachment'] as $attachment) { - // Only process REMOTE pictures (prevent xss) - if ($attachment['mediaType'] - && preg_match('/^image\//', $attachment['mediaType'], $match) - && preg_match('/^http(s|):\/\//', $attachment['url'], $match) - ) { - $item['content'] = $item['content'] . '<br /><img '; - if ($attachment['name']) { - $item['content'] .= sprintf('alt="%s" ', $attachment['name']); - } - $item['content'] .= sprintf('src="%s" />', $attachment['url']); - } - } - return $item; - } + if (mb_strlen($strippedContent) > 75) { + $contentSubstring = mb_substr($strippedContent, 0, mb_strpos(wordwrap($strippedContent, 75), "\n")); + $item['title'] .= $contentSubstring . '...'; + } else { + $item['title'] .= $strippedContent; + } + $item['uri'] = $object['id']; + foreach ($object['attachment'] as $attachment) { + // Only process REMOTE pictures (prevent xss) + if ( + $attachment['mediaType'] + && preg_match('/^image\//', $attachment['mediaType'], $match) + && preg_match('/^http(s|):\/\//', $attachment['url'], $match) + ) { + $item['content'] = $item['content'] . '<br /><img '; + if ($attachment['name']) { + $item['content'] .= sprintf('alt="%s" ', $attachment['name']); + } + $item['content'] .= sprintf('src="%s" />', $attachment['url']); + } + } + return $item; + } - protected function fetchAP($url) { - $d = new DateTime(); - $d->setTimezone(new DateTimeZone('GMT')); - $date = $d->format('D, d M Y H:i:s e'); - preg_match('/https?:\/\/([a-z0-9-\.]{0,})(\/[^?#]+)/', $url, $matches); - $headers = array( - 'Accept: application/activity+json', - 'Host: ' . $matches[1], - 'Date: ' . $date - ); - $privateKey = $this->getOption('private_key'); - $keyId = $this->getOption('key_id'); - if ($privateKey && $keyId) { - $pkey = openssl_pkey_get_private('file://' . $privateKey); - $toSign = '(request-target): get ' . $matches[2] . "\nhost: " . $matches[1] . "\ndate: " . $date; - $result = openssl_sign($toSign, $signature, $pkey, 'RSA-SHA256'); - if ($result) { - Debug::log($toSign); - $sig = 'Signature: keyId="' . $keyId . '",headers="(request-target) host date",signature="' . - base64_encode($signature) . '"'; - Debug::log($sig); - array_push($headers, $sig); - } - } - return json_decode(getContents($url, $headers), true); - } + protected function fetchAP($url) + { + $d = new DateTime(); + $d->setTimezone(new DateTimeZone('GMT')); + $date = $d->format('D, d M Y H:i:s e'); + preg_match('/https?:\/\/([a-z0-9-\.]{0,})(\/[^?#]+)/', $url, $matches); + $headers = [ + 'Accept: application/activity+json', + 'Host: ' . $matches[1], + 'Date: ' . $date + ]; + $privateKey = $this->getOption('private_key'); + $keyId = $this->getOption('key_id'); + if ($privateKey && $keyId) { + $pkey = openssl_pkey_get_private('file://' . $privateKey); + $toSign = '(request-target): get ' . $matches[2] . "\nhost: " . $matches[1] . "\ndate: " . $date; + $result = openssl_sign($toSign, $signature, $pkey, 'RSA-SHA256'); + if ($result) { + Debug::log($toSign); + $sig = 'Signature: keyId="' . $keyId . '",headers="(request-target) host date",signature="' . + base64_encode($signature) . '"'; + Debug::log($sig); + array_push($headers, $sig); + } + } + return json_decode(getContents($url, $headers), true); + } } diff --git a/bridges/MediapartBlogsBridge.php b/bridges/MediapartBlogsBridge.php index b46ef2a2..fa8c3d5f 100644 --- a/bridges/MediapartBlogsBridge.php +++ b/bridges/MediapartBlogsBridge.php @@ -1,48 +1,53 @@ <?php -class MediapartBlogsBridge extends BridgeAbstract { - const NAME = 'Mediapart Blogs'; - const BASE_URI = 'https://blogs.mediapart.fr'; - const URI = self::BASE_URI . '/blogs'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - array( - 'slug' => array( - 'name' => 'Blog Slug', - 'type' => 'text', - 'title' => 'Blog user name', - 'required' => true, - 'exampleValue' => 'jean-vincot', - ) - ) - ); - - public function getIcon() { - return 'https://static.mediapart.fr/favicon/favicon-club.ico?v=2'; - } - - public function collectData() { - $html = getSimpleHTMLDOM(self::BASE_URI . '/' . $this->getInput('slug') . '/blog'); - - foreach($html->find('ul.post-list li') as $element) { - $item = array(); - - $item_title = $element->find('h3.title a', 0); - $item_divs = $element->find('div'); - - $item['title'] = $item_title->innertext; - $item['uri'] = self::BASE_URI . trim($item_title->href); - $item['author'] = $element->find('.author .subscriber', 0)->innertext; - $item['content'] = $item_divs[count($item_divs) - 2] . $item_divs[count($item_divs) - 1]; - $item['timestamp'] = strtotime($element->find('.author time', 0)->datetime); - - $this->items[] = $item; - } - } - - public function getName() { - if ($this->getInput('slug')) { - return self::NAME . ' | ' . $this->getInput('slug'); - } - return parent::getName(); - } + +class MediapartBlogsBridge extends BridgeAbstract +{ + const NAME = 'Mediapart Blogs'; + const BASE_URI = 'https://blogs.mediapart.fr'; + const URI = self::BASE_URI . '/blogs'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + [ + 'slug' => [ + 'name' => 'Blog Slug', + 'type' => 'text', + 'title' => 'Blog user name', + 'required' => true, + 'exampleValue' => 'jean-vincot', + ] + ] + ]; + + public function getIcon() + { + return 'https://static.mediapart.fr/favicon/favicon-club.ico?v=2'; + } + + public function collectData() + { + $html = getSimpleHTMLDOM(self::BASE_URI . '/' . $this->getInput('slug') . '/blog'); + + foreach ($html->find('ul.post-list li') as $element) { + $item = []; + + $item_title = $element->find('h3.title a', 0); + $item_divs = $element->find('div'); + + $item['title'] = $item_title->innertext; + $item['uri'] = self::BASE_URI . trim($item_title->href); + $item['author'] = $element->find('.author .subscriber', 0)->innertext; + $item['content'] = $item_divs[count($item_divs) - 2] . $item_divs[count($item_divs) - 1]; + $item['timestamp'] = strtotime($element->find('.author time', 0)->datetime); + + $this->items[] = $item; + } + } + + public function getName() + { + if ($this->getInput('slug')) { + return self::NAME . ' | ' . $this->getInput('slug'); + } + return parent::getName(); + } } diff --git a/bridges/MediapartBridge.php b/bridges/MediapartBridge.php index f7fff4ab..3c8c8317 100644 --- a/bridges/MediapartBridge.php +++ b/bridges/MediapartBridge.php @@ -1,65 +1,69 @@ <?php -class MediapartBridge extends FeedExpander { - const MAINTAINER = 'killruana'; - const NAME = 'Mediapart Bridge'; - const URI = 'https://www.mediapart.fr/'; - const PARAMETERS = array( - array( - 'single_page_mode' => array( - 'name' => 'Single page article', - 'type' => 'checkbox', - 'title' => 'Display long articles on a single page', - 'defaultValue' => 'checked' - ), - 'mpsessid' => array( - 'name' => 'MPSESSID', - 'type' => 'text', - 'title' => 'Value of the session cookie MPSESSID' - ) - ) - ); - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Returns the newest articles.'; +class MediapartBridge extends FeedExpander +{ + const MAINTAINER = 'killruana'; + const NAME = 'Mediapart Bridge'; + const URI = 'https://www.mediapart.fr/'; + const PARAMETERS = [ + [ + 'single_page_mode' => [ + 'name' => 'Single page article', + 'type' => 'checkbox', + 'title' => 'Display long articles on a single page', + 'defaultValue' => 'checked' + ], + 'mpsessid' => [ + 'name' => 'MPSESSID', + 'type' => 'text', + 'title' => 'Value of the session cookie MPSESSID' + ] + ] + ]; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Returns the newest articles.'; - public function collectData() { - $url = self::URI . 'articles/feed'; - $this->collectExpandableDatas($url); - } + public function collectData() + { + $url = self::URI . 'articles/feed'; + $this->collectExpandableDatas($url); + } - protected function parseItem($newsItem) { - $item = parent::parseItem($newsItem); + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); - // Mediapart provide multiple type of contents. - // We only process items relative to the newspaper - // See issue #1292 - https://github.com/RSS-Bridge/rss-bridge/issues/1292 - if (strpos($item['uri'], self::URI . 'journal/') === 0) { - // Enable single page mode? - if ($this->getInput('single_page_mode') === true) { - $item['uri'] .= '?onglet=full'; - } + // Mediapart provide multiple type of contents. + // We only process items relative to the newspaper + // See issue #1292 - https://github.com/RSS-Bridge/rss-bridge/issues/1292 + if (strpos($item['uri'], self::URI . 'journal/') === 0) { + // Enable single page mode? + if ($this->getInput('single_page_mode') === true) { + $item['uri'] .= '?onglet=full'; + } - // If a session cookie is defined, get the full article - $mpsessid = $this->getInput('mpsessid'); - if (!empty($mpsessid)) { - // Set the session cookie - $opt = array(); - $opt[CURLOPT_COOKIE] = 'MPSESSID=' . $mpsessid; + // If a session cookie is defined, get the full article + $mpsessid = $this->getInput('mpsessid'); + if (!empty($mpsessid)) { + // Set the session cookie + $opt = []; + $opt[CURLOPT_COOKIE] = 'MPSESSID=' . $mpsessid; - // Get the page - $articlePage = getSimpleHTMLDOM( - $newsItem->link . '?onglet=full', - array(), - $opt); + // Get the page + $articlePage = getSimpleHTMLDOM( + $newsItem->link . '?onglet=full', + [], + $opt + ); - // Extract the article content - $content = $articlePage->find('div.content-article', 0)->innertext; - $content = sanitize($content); - $content = defaultLinkTo($content, static::URI); - $item['content'] .= $content; - } - } + // Extract the article content + $content = $articlePage->find('div.content-article', 0)->innertext; + $content = sanitize($content); + $content = defaultLinkTo($content, static::URI); + $item['content'] .= $content; + } + } - return $item; - } + return $item; + } } diff --git a/bridges/MilbooruBridge.php b/bridges/MilbooruBridge.php index 24571279..54833b30 100644 --- a/bridges/MilbooruBridge.php +++ b/bridges/MilbooruBridge.php @@ -1,10 +1,9 @@ <?php -class MilbooruBridge extends Shimmie2Bridge { - - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Milbooru'; - const URI = 'http://sheslostcontrol.net/moe/shimmie/'; - const DESCRIPTION = 'Returns images from given page'; - +class MilbooruBridge extends Shimmie2Bridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Milbooru'; + const URI = 'http://sheslostcontrol.net/moe/shimmie/'; + const DESCRIPTION = 'Returns images from given page'; } diff --git a/bridges/MixCloudBridge.php b/bridges/MixCloudBridge.php index 2296af2f..d2b35989 100644 --- a/bridges/MixCloudBridge.php +++ b/bridges/MixCloudBridge.php @@ -1,61 +1,64 @@ <?php -class MixCloudBridge extends BridgeAbstract { - - const MAINTAINER = 'Alexis CHEMEL'; - const NAME = 'MixCloud'; - const URI = 'https://www.mixcloud.com'; - const API_URI = 'https://api.mixcloud.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns latest musics on user stream'; - - const PARAMETERS = array(array( - 'u' => array( - 'name' => 'username', - 'required' => true, - 'exampleValue' => 'DJJazzyJeff', - ) - )); - - public function getName(){ - if(!is_null($this->getInput('u'))) { - return 'MixCloud - ' . $this->getInput('u'); - } - - return parent::getName(); - } - - private static function compareDate($stream1, $stream2) { - return (strtotime($stream1['timestamp']) < strtotime($stream2['timestamp']) ? 1 : -1); - } - - public function collectData(){ - $user = urlencode($this->getInput('u')); - // Get Cloudcasts - $mixcloudUri = self::API_URI . $user . '/cloudcasts/'; - $content = getContents($mixcloudUri); - $casts = json_decode($content)->data; - - // Get Listens - $mixcloudUri = self::API_URI . $user . '/listens/'; - $content = getContents($mixcloudUri); - $listens = json_decode($content)->data; - - $streams = array_merge($casts, $listens); - - foreach($streams as $stream) { - $item = array(); - - $item['uri'] = $stream->url; - $item['title'] = $stream->name; - $item['content'] = '<img src="' . $stream->pictures->thumbnail . '" />'; - $item['author'] = $stream->user->name; - $item['timestamp'] = $stream->created_time; - - $this->items[] = $item; - } - - // Sort items by date - usort($this->items, array('MixCloudBridge', 'compareDate')); - } +class MixCloudBridge extends BridgeAbstract +{ + const MAINTAINER = 'Alexis CHEMEL'; + const NAME = 'MixCloud'; + const URI = 'https://www.mixcloud.com'; + const API_URI = 'https://api.mixcloud.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns latest musics on user stream'; + + const PARAMETERS = [[ + 'u' => [ + 'name' => 'username', + 'required' => true, + 'exampleValue' => 'DJJazzyJeff', + ] + ]]; + + public function getName() + { + if (!is_null($this->getInput('u'))) { + return 'MixCloud - ' . $this->getInput('u'); + } + + return parent::getName(); + } + + private static function compareDate($stream1, $stream2) + { + return (strtotime($stream1['timestamp']) < strtotime($stream2['timestamp']) ? 1 : -1); + } + + public function collectData() + { + $user = urlencode($this->getInput('u')); + // Get Cloudcasts + $mixcloudUri = self::API_URI . $user . '/cloudcasts/'; + $content = getContents($mixcloudUri); + $casts = json_decode($content)->data; + + // Get Listens + $mixcloudUri = self::API_URI . $user . '/listens/'; + $content = getContents($mixcloudUri); + $listens = json_decode($content)->data; + + $streams = array_merge($casts, $listens); + + foreach ($streams as $stream) { + $item = []; + + $item['uri'] = $stream->url; + $item['title'] = $stream->name; + $item['content'] = '<img src="' . $stream->pictures->thumbnail . '" />'; + $item['author'] = $stream->user->name; + $item['timestamp'] = $stream->created_time; + + $this->items[] = $item; + } + + // Sort items by date + usort($this->items, ['MixCloudBridge', 'compareDate']); + } } diff --git a/bridges/ModelKarteiBridge.php b/bridges/ModelKarteiBridge.php index 2a1bee9c..04b03fcf 100644 --- a/bridges/ModelKarteiBridge.php +++ b/bridges/ModelKarteiBridge.php @@ -1,102 +1,118 @@ <?php -class ModelKarteiBridge extends BridgeAbstract { - const NAME = 'model-kartei.de'; - const URI = 'https://www.model-kartei.de/'; - const DESCRIPTION = 'Get the public comp card gallery'; - const MAINTAINER = 'fulmeek'; - const PARAMETERS = array(array( - 'model_id' => array( - 'name' => 'Model ID', - 'required' => true, - 'exampleValue' => '614931' - ) - )); - - const LIMIT_ITEMS = 10; - - private $feedName = ''; - - public function collectData() { - $model_id = preg_replace('/[^0-9]/', '', $this->getInput('model_id')); - if (empty($model_id)) - returnServerError('Invalid model ID'); - - $html = getSimpleHTMLDOM(self::URI . 'sedcards/model/' . $model_id . '/'); - - $objTitle = $html->find('.sTitle', 0); - if ($objTitle) - $this->feedName = $objTitle->plaintext; - - $itemlist = $html->find('#photoList .photoPreview'); - if (!$itemlist) - returnServerError('No gallery'); - - foreach($itemlist as $idx => $element) { - if ($idx >= self::LIMIT_ITEMS) - break; - - $item = array(); - - $title = $element->title; - $date = $element->{'data-date'}; - $author = $this->feedName; - $text = ''; - - $objImage = $element->find('a.photoLink img', 0); - $objLink = $element->find('a.photoLink', 0); - - if ($objLink) { - $page = getSimpleHTMLDOMCached($objLink->href); - - if (empty($title)) { - $objTitle = $page->find('.p-title', 0); - if ($objTitle) - $title = $objTitle->plaintext; - } - if (empty($date)) { - $objDate = $page->find('.cameraDetails .date', 0); - if ($objDate) - $date = strtotime($objDate->parent()->plaintext); - } - if (empty($author)) { - $objAuthor = $page->find('.p-publisher a', 0); - if ($objAuthor) - $author = $objAuthor->plaintext; - } - - $objFullImage = $page->find('img#gofullscreen', 0); - if ($objFullImage) - $objImage = $objFullImage; - - $objText = $page->find('.p-desc', 0); - if ($objText) - $text = $objText->plaintext; - } - - $item['title'] = $title; - $item['timestamp'] = $date; - $item['author'] = $author; - - if ($objImage) - $item['content'] = '<img src="' . $objImage->src . '"/>'; - if ($objLink) { - $item['uri'] = $objLink->href; - if (!empty($item['content'])) - $item['content'] = '<a href="' . $objLink->href . '" target="_blank">' . $item['content'] . '</a>'; - } else { - $item['uri'] = 'urn:sha1:' . hash('sha1', $item['content']); - } - if (!empty($text)) - $item['content'] = '<p>' . $text . '</p>' . $item['content']; - - $this->items[] = $item; - } - } - - public function getName(){ - if(!empty($this->feedName)) { - return $this->feedName . ' - ' . self::NAME; - } - return parent::getName(); - } + +class ModelKarteiBridge extends BridgeAbstract +{ + const NAME = 'model-kartei.de'; + const URI = 'https://www.model-kartei.de/'; + const DESCRIPTION = 'Get the public comp card gallery'; + const MAINTAINER = 'fulmeek'; + const PARAMETERS = [[ + 'model_id' => [ + 'name' => 'Model ID', + 'required' => true, + 'exampleValue' => '614931' + ] + ]]; + + const LIMIT_ITEMS = 10; + + private $feedName = ''; + + public function collectData() + { + $model_id = preg_replace('/[^0-9]/', '', $this->getInput('model_id')); + if (empty($model_id)) { + returnServerError('Invalid model ID'); + } + + $html = getSimpleHTMLDOM(self::URI . 'sedcards/model/' . $model_id . '/'); + + $objTitle = $html->find('.sTitle', 0); + if ($objTitle) { + $this->feedName = $objTitle->plaintext; + } + + $itemlist = $html->find('#photoList .photoPreview'); + if (!$itemlist) { + returnServerError('No gallery'); + } + + foreach ($itemlist as $idx => $element) { + if ($idx >= self::LIMIT_ITEMS) { + break; + } + + $item = []; + + $title = $element->title; + $date = $element->{'data-date'}; + $author = $this->feedName; + $text = ''; + + $objImage = $element->find('a.photoLink img', 0); + $objLink = $element->find('a.photoLink', 0); + + if ($objLink) { + $page = getSimpleHTMLDOMCached($objLink->href); + + if (empty($title)) { + $objTitle = $page->find('.p-title', 0); + if ($objTitle) { + $title = $objTitle->plaintext; + } + } + if (empty($date)) { + $objDate = $page->find('.cameraDetails .date', 0); + if ($objDate) { + $date = strtotime($objDate->parent()->plaintext); + } + } + if (empty($author)) { + $objAuthor = $page->find('.p-publisher a', 0); + if ($objAuthor) { + $author = $objAuthor->plaintext; + } + } + + $objFullImage = $page->find('img#gofullscreen', 0); + if ($objFullImage) { + $objImage = $objFullImage; + } + + $objText = $page->find('.p-desc', 0); + if ($objText) { + $text = $objText->plaintext; + } + } + + $item['title'] = $title; + $item['timestamp'] = $date; + $item['author'] = $author; + + if ($objImage) { + $item['content'] = '<img src="' . $objImage->src . '"/>'; + } + if ($objLink) { + $item['uri'] = $objLink->href; + if (!empty($item['content'])) { + $item['content'] = '<a href="' . $objLink->href . '" target="_blank">' . $item['content'] . '</a>'; + } + } else { + $item['uri'] = 'urn:sha1:' . hash('sha1', $item['content']); + } + if (!empty($text)) { + $item['content'] = '<p>' . $text . '</p>' . $item['content']; + } + + $this->items[] = $item; + } + } + + public function getName() + { + if (!empty($this->feedName)) { + return $this->feedName . ' - ' . self::NAME; + } + return parent::getName(); + } } diff --git a/bridges/MoebooruBridge.php b/bridges/MoebooruBridge.php index 90b6a6ca..1af08575 100644 --- a/bridges/MoebooruBridge.php +++ b/bridges/MoebooruBridge.php @@ -1,55 +1,59 @@ <?php -class MoebooruBridge extends BridgeAbstract { - const NAME = 'Moebooru'; - const URI = 'https://moe.dev.myconan.net/'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns images from given page'; - const MAINTAINER = 'pmaziere'; +class MoebooruBridge extends BridgeAbstract +{ + const NAME = 'Moebooru'; + const URI = 'https://moe.dev.myconan.net/'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns images from given page'; + const MAINTAINER = 'pmaziere'; - const PARAMETERS = array( array( - 'p' => array( - 'name' => 'page', - 'defaultValue' => 1, - 'type' => 'number' - ), - 't' => array( - 'name' => 'tags' - ) - )); + const PARAMETERS = [ [ + 'p' => [ + 'name' => 'page', + 'defaultValue' => 1, + 'type' => 'number' + ], + 't' => [ + 'name' => 'tags' + ] + ]]; - protected function getFullURI(){ - return $this->getURI() - . 'post?page=' - . $this->getInput('p') - . '&tags=' - . urlencode($this->getInput('t')); - } + protected function getFullURI() + { + return $this->getURI() + . 'post?page=' + . $this->getInput('p') + . '&tags=' + . urlencode($this->getInput('t')); + } - public function collectData(){ - $html = getSimpleHTMLDOM($this->getFullURI()); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getFullURI()); - $input_json = explode('Post.register(', $html); - foreach($input_json as $element) - $data[] = preg_replace('/}\)(.*)/', '}', $element); - unset($data[0]); + $input_json = explode('Post.register(', $html); + foreach ($input_json as $element) { + $data[] = preg_replace('/}\)(.*)/', '}', $element); + } + unset($data[0]); - foreach($data as $datai) { - $json = json_decode($datai, true); - $item = array(); - $item['uri'] = $this->getURI() . '/post/show/' . $json['id']; - $item['postid'] = $json['id']; - $item['timestamp'] = $json['created_at']; - $item['imageUri'] = $json['file_url']; - $item['title'] = $this->getName() . ' | ' . $json['id']; - $item['content'] = '<a href="' - . $item['imageUri'] - . '"><img src="' - . $json['preview_url'] - . '" /></a><br>Tags: ' - . $json['tags']; + foreach ($data as $datai) { + $json = json_decode($datai, true); + $item = []; + $item['uri'] = $this->getURI() . '/post/show/' . $json['id']; + $item['postid'] = $json['id']; + $item['timestamp'] = $json['created_at']; + $item['imageUri'] = $json['file_url']; + $item['title'] = $this->getName() . ' | ' . $json['id']; + $item['content'] = '<a href="' + . $item['imageUri'] + . '"><img src="' + . $json['preview_url'] + . '" /></a><br>Tags: ' + . $json['tags']; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/MoinMoinBridge.php b/bridges/MoinMoinBridge.php index 1920c5a1..c8053587 100644 --- a/bridges/MoinMoinBridge.php +++ b/bridges/MoinMoinBridge.php @@ -1,327 +1,346 @@ <?php -class MoinMoinBridge extends BridgeAbstract { - - const MAINTAINER = 'logmanoriginal'; - const NAME = 'MoinMoin Bridge'; - const URI = 'https://moinmo.in'; - const DESCRIPTION = 'Generates feeds for pages of a MoinMoin (compatible) wiki'; - const PARAMETERS = array( - array( - 'source' => array( - 'name' => 'Source', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert wiki page URI (e.g.: https://moinmo.in/MoinMoin)', - 'exampleValue' => 'https://moinmo.in/MoinMoin' - ), - 'separator' => array( - 'name' => 'Separator', - 'type' => 'list', - 'requied' => true, - 'title' => 'Defines the separtor for splitting content into feeds', - 'defaultValue' => 'h2', - 'values' => array( - 'Header (h1)' => 'h1', - 'Header (h2)' => 'h2', - 'Header (h3)' => 'h3', - 'List element (li)' => 'li', - 'Anchor (a)' => 'a' - ) - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Number of items to return (from top)', - 'defaultValue' => -1 - ), - 'content' => array( - 'name' => 'Content', - 'type' => 'list', - 'required' => false, - 'title' => 'Defines how feed contents are build', - 'defaultValue' => 'separator', - 'values' => array( - 'By separator' => 'separator', - 'Follow link (only for anchor)' => 'follow', - 'None' => 'none' - ) - ) - ) - ); - - private $title = ''; - - public function collectData(){ - /* MoinMoin uses a rather unpleasent representation of HTML. Instead of - * using tags like <article/>, <navigation/>, <header/>, etc... it uses - * <div/>, <span/> and <p/>. Also each line is literaly identified via - * IDs. The only way to distinguish content is via headers, though not - * in all cases. - * - * Example (indented for the sake of readability): - * ... - * <span class="anchor" id="line-1"></span> - * <span class="anchor" id="line-2"></span> - * <span class="anchor" id="line-3"></span> - * <span class="anchor" id="line-4"></span> - * <span class="anchor" id="line-5"></span> - * <span class="anchor" id="line-6"></span> - * <span class="anchor" id="line-7"></span> - * <span class="anchor" id="line-8"></span> - * <span class="anchor" id="line-9"></span> - * <p class="line867">MoinMoin is a Wiki software implemented in - * <a class="interwiki" href="/Python" title="MoinMoin">Python</a> - * and distributed as Free Software under - * <a class="interwiki" href="/GPL" title="MoinMoin">GNU GPL license</a>. - * ... - */ - $html = getSimpleHTMLDOM($this->getInput('source')); - - // Some anchors link to local sites or local IDs (both don't work well - // in feeds) - $html = $this->fixAnchors($html); - - $this->title = $html->find('title', 0)->innertext . ' | ' . self::NAME; - - // Here we focus on simple author and timestamp information from the given - // page. Later we update this information in case the anchor is followed. - $author = $this->findAuthor($html); - $timestamp = $this->findTimestamp($html); - - $sections = $this->splitSections($html); - - foreach($sections as $section) { - $item = array(); - - $item['uri'] = $this->findSectionAnchor($section[0]); - - switch($this->getInput('content')) { - case 'none': // Do not return any content - break; - case 'follow': // Follow the anchor - // We can only follow anchors (use default otherwise) - if($this->getInput('separator') === 'a') { - $content = $this->followAnchor($item['uri']); - - // Return only actual content - $item['content'] = $content->find('div#page', 0)->innertext; - - // Each page could have its own author and timestamp - $author = $this->findAuthor($content); - $timestamp = $this->findTimestamp($content); - - break; - } - // fall-through - case 'separator': - default: // Use contents from the current page - $item['content'] = $this->cleanArticle($section[2]); - } - - if(!is_null($author)) $item['author'] = $author; - if(!is_null($timestamp)) $item['timestamp'] = $timestamp; - $item['title'] = strip_tags($section[1]); - - // Skip items with empty title - if(empty(trim($item['title']))) { - continue; - } - - $this->items[] = $item; - - if($this->getInput('limit') > 0 - && count($this->items) >= $this->getInput('limit')) { - break; - } - } - } - - public function getName(){ - return $this->title ?: parent::getName(); - } - - public function getURI(){ - return $this->getInput('source') ?: parent::getURI(); - } - - /** - * Splits the html into sections. - * - * Returns an array with one element per section. Each element consists of: - * [0] The entire section - * [1] The section title - * [2] The section content - */ - private function splitSections($html){ - $content = $html->find('div#page', 0)->innertext - or returnServerError('Unable to find <div id="page"/>!'); - - $sections = array(); - - $regex = implode( - '', - array( - "\<{$this->getInput('separator')}.+?(?=\>)\>", - "(.+?)(?=\<\/{$this->getInput('separator')}\>)", - "\<\/{$this->getInput('separator')}\>", - "(.+?)((?=\<{$this->getInput('separator')})|(?=\<div\sid=\"pagebottom\")){1}" - ) - ); - - preg_match_all( - '/' . $regex . '/m', - $content, - $sections, - PREG_SET_ORDER - ); - - // Some pages don't use headers, return page as one feed - if(count($sections) === 0) { - return array( - array( - $content, - $html->find('title', 0)->innertext, - $content - ) - ); - } - - return $sections; - } - - /** - * Returns the anchor for a given section - */ - private function findSectionAnchor($section){ - $html = str_get_html($section); - - // For IDs - $anchor = $html->find($this->getInput('separator') . '[id=]', 0); - if(!is_null($anchor)) { - return $this->getInput('source') . '#' . $anchor->id; - } - - // For actual anchors - $anchor = $html->find($this->getInput('separator') . '[href=]', 0); - if(!is_null($anchor)) { - return $anchor->href; - } - - // Nothing found - return $this->getInput('source'); - } - - /** - * Returns the author - * - * Notice: Some pages don't provide author information - */ - private function findAuthor($html){ - /* Example: - * <p id="pageinfo" class="info" dir="ltr" lang="en">MoinMoin: LocalSpellingWords - * (last edited 2017-02-16 15:36:31 by <span title="??? @ hosted-by.leaseweb.com - * [178.162.199.143]">hosted-by</span>)</p> - */ - $pageinfo = $html->find('[id="pageinfo"]', 0); - - if(is_null($pageinfo)) { - return null; - } else { - $author = $pageinfo->find('[title=]', 0); - if(is_null($author)) { - return null; - } else { - return trim(explode('@', $author->title)[0]); - } - } - } - - /** - * Returns the time of last edit - * - * Notice: Some pages don't provide this information - */ - private function findTimestamp($html){ - // See example of findAuthor() - $pageinfo = $html->find('[id="pageinfo"]', 0); - - if(is_null($pageinfo)) { - return null; - } else { - $timestamp = $pageinfo->innertext; - $matches = array(); - preg_match('/.+?(?=\().+?(?=\d)([0-9\-\s\:]+)/m', $pageinfo, $matches); - return strtotime($matches[1]); - } - } - - /** - * Returns the original HTML with all anchors fixed (makes relative anchors - * absolute) - */ - private function fixAnchors($html, $source = null){ - - $source = $source ?: $this->getURI(); - - foreach($html->find('a') as $anchor) { - switch(substr($anchor->href, 0, 1)) { - case 'h': // http or https, no actions required - break; - case '/': // some relative path - $anchor->href = $this->findDomain($source) . $anchor->href; - break; - case '#': // it's an ID - default: // probably something like ? or &, skip empty ones - if(!isset($anchor->href)) - break; - $anchor->href = $source . $anchor->href; - } - } - - return $html; - } - - /** - * Loads the full article of a given anchor (if the anchor is from the same - * wiki domain) - */ - private function followAnchor($anchor){ - if(strrpos($anchor, $this->findDomain($this->getInput('source')) === false)) { - return null; - } - - $html = getSimpleHTMLDOMCached($anchor); - if(!$html) { // Cannot load article - return null; - } - - return $this->fixAnchors($html, $anchor); - } - - /** - * Finds the domain for a given URI - */ - private function findDomain($uri){ - $matches = array(); - preg_match('/(http[s]{0,1}:\/\/.+?(?=\/))/', $uri, $matches); - return $matches[1]; - } - - /* This function is a copy from CNETBridge */ - private function stripWithDelimiters($string, $start, $end){ - while(strpos($string, $start) !== false) { - $section_to_remove = substr($string, strpos($string, $start)); - $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end)); - $string = str_replace($section_to_remove, '', $string); - } - - return $string; - } - - /* This function is based on CNETBridge */ - private function cleanArticle($article_html){ - $article_html = $this->stripWithDelimiters($article_html, '<script', '</script>'); - return $article_html; - } + +class MoinMoinBridge extends BridgeAbstract +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'MoinMoin Bridge'; + const URI = 'https://moinmo.in'; + const DESCRIPTION = 'Generates feeds for pages of a MoinMoin (compatible) wiki'; + const PARAMETERS = [ + [ + 'source' => [ + 'name' => 'Source', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert wiki page URI (e.g.: https://moinmo.in/MoinMoin)', + 'exampleValue' => 'https://moinmo.in/MoinMoin' + ], + 'separator' => [ + 'name' => 'Separator', + 'type' => 'list', + 'requied' => true, + 'title' => 'Defines the separtor for splitting content into feeds', + 'defaultValue' => 'h2', + 'values' => [ + 'Header (h1)' => 'h1', + 'Header (h2)' => 'h2', + 'Header (h3)' => 'h3', + 'List element (li)' => 'li', + 'Anchor (a)' => 'a' + ] + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Number of items to return (from top)', + 'defaultValue' => -1 + ], + 'content' => [ + 'name' => 'Content', + 'type' => 'list', + 'required' => false, + 'title' => 'Defines how feed contents are build', + 'defaultValue' => 'separator', + 'values' => [ + 'By separator' => 'separator', + 'Follow link (only for anchor)' => 'follow', + 'None' => 'none' + ] + ] + ] + ]; + + private $title = ''; + + public function collectData() + { + /* MoinMoin uses a rather unpleasent representation of HTML. Instead of + * using tags like <article/>, <navigation/>, <header/>, etc... it uses + * <div/>, <span/> and <p/>. Also each line is literaly identified via + * IDs. The only way to distinguish content is via headers, though not + * in all cases. + * + * Example (indented for the sake of readability): + * ... + * <span class="anchor" id="line-1"></span> + * <span class="anchor" id="line-2"></span> + * <span class="anchor" id="line-3"></span> + * <span class="anchor" id="line-4"></span> + * <span class="anchor" id="line-5"></span> + * <span class="anchor" id="line-6"></span> + * <span class="anchor" id="line-7"></span> + * <span class="anchor" id="line-8"></span> + * <span class="anchor" id="line-9"></span> + * <p class="line867">MoinMoin is a Wiki software implemented in + * <a class="interwiki" href="/Python" title="MoinMoin">Python</a> + * and distributed as Free Software under + * <a class="interwiki" href="/GPL" title="MoinMoin">GNU GPL license</a>. + * ... + */ + $html = getSimpleHTMLDOM($this->getInput('source')); + + // Some anchors link to local sites or local IDs (both don't work well + // in feeds) + $html = $this->fixAnchors($html); + + $this->title = $html->find('title', 0)->innertext . ' | ' . self::NAME; + + // Here we focus on simple author and timestamp information from the given + // page. Later we update this information in case the anchor is followed. + $author = $this->findAuthor($html); + $timestamp = $this->findTimestamp($html); + + $sections = $this->splitSections($html); + + foreach ($sections as $section) { + $item = []; + + $item['uri'] = $this->findSectionAnchor($section[0]); + + switch ($this->getInput('content')) { + case 'none': // Do not return any content + break; + case 'follow': // Follow the anchor + // We can only follow anchors (use default otherwise) + if ($this->getInput('separator') === 'a') { + $content = $this->followAnchor($item['uri']); + + // Return only actual content + $item['content'] = $content->find('div#page', 0)->innertext; + + // Each page could have its own author and timestamp + $author = $this->findAuthor($content); + $timestamp = $this->findTimestamp($content); + + break; + } + // fall-through + case 'separator': + default: // Use contents from the current page + $item['content'] = $this->cleanArticle($section[2]); + } + + if (!is_null($author)) { + $item['author'] = $author; + } + if (!is_null($timestamp)) { + $item['timestamp'] = $timestamp; + } + $item['title'] = strip_tags($section[1]); + + // Skip items with empty title + if (empty(trim($item['title']))) { + continue; + } + + $this->items[] = $item; + + if ( + $this->getInput('limit') > 0 + && count($this->items) >= $this->getInput('limit') + ) { + break; + } + } + } + + public function getName() + { + return $this->title ?: parent::getName(); + } + + public function getURI() + { + return $this->getInput('source') ?: parent::getURI(); + } + + /** + * Splits the html into sections. + * + * Returns an array with one element per section. Each element consists of: + * [0] The entire section + * [1] The section title + * [2] The section content + */ + private function splitSections($html) + { + $content = $html->find('div#page', 0)->innertext + or returnServerError('Unable to find <div id="page"/>!'); + + $sections = []; + + $regex = implode( + '', + [ + "\<{$this->getInput('separator')}.+?(?=\>)\>", + "(.+?)(?=\<\/{$this->getInput('separator')}\>)", + "\<\/{$this->getInput('separator')}\>", + "(.+?)((?=\<{$this->getInput('separator')})|(?=\<div\sid=\"pagebottom\")){1}" + ] + ); + + preg_match_all( + '/' . $regex . '/m', + $content, + $sections, + PREG_SET_ORDER + ); + + // Some pages don't use headers, return page as one feed + if (count($sections) === 0) { + return [ + [ + $content, + $html->find('title', 0)->innertext, + $content + ] + ]; + } + + return $sections; + } + + /** + * Returns the anchor for a given section + */ + private function findSectionAnchor($section) + { + $html = str_get_html($section); + + // For IDs + $anchor = $html->find($this->getInput('separator') . '[id=]', 0); + if (!is_null($anchor)) { + return $this->getInput('source') . '#' . $anchor->id; + } + + // For actual anchors + $anchor = $html->find($this->getInput('separator') . '[href=]', 0); + if (!is_null($anchor)) { + return $anchor->href; + } + + // Nothing found + return $this->getInput('source'); + } + + /** + * Returns the author + * + * Notice: Some pages don't provide author information + */ + private function findAuthor($html) + { + /* Example: + * <p id="pageinfo" class="info" dir="ltr" lang="en">MoinMoin: LocalSpellingWords + * (last edited 2017-02-16 15:36:31 by <span title="??? @ hosted-by.leaseweb.com + * [178.162.199.143]">hosted-by</span>)</p> + */ + $pageinfo = $html->find('[id="pageinfo"]', 0); + + if (is_null($pageinfo)) { + return null; + } else { + $author = $pageinfo->find('[title=]', 0); + if (is_null($author)) { + return null; + } else { + return trim(explode('@', $author->title)[0]); + } + } + } + + /** + * Returns the time of last edit + * + * Notice: Some pages don't provide this information + */ + private function findTimestamp($html) + { + // See example of findAuthor() + $pageinfo = $html->find('[id="pageinfo"]', 0); + + if (is_null($pageinfo)) { + return null; + } else { + $timestamp = $pageinfo->innertext; + $matches = []; + preg_match('/.+?(?=\().+?(?=\d)([0-9\-\s\:]+)/m', $pageinfo, $matches); + return strtotime($matches[1]); + } + } + + /** + * Returns the original HTML with all anchors fixed (makes relative anchors + * absolute) + */ + private function fixAnchors($html, $source = null) + { + $source = $source ?: $this->getURI(); + + foreach ($html->find('a') as $anchor) { + switch (substr($anchor->href, 0, 1)) { + case 'h': // http or https, no actions required + break; + case '/': // some relative path + $anchor->href = $this->findDomain($source) . $anchor->href; + break; + case '#': // it's an ID + default: // probably something like ? or &, skip empty ones + if (!isset($anchor->href)) { + break; + } + $anchor->href = $source . $anchor->href; + } + } + + return $html; + } + + /** + * Loads the full article of a given anchor (if the anchor is from the same + * wiki domain) + */ + private function followAnchor($anchor) + { + if (strrpos($anchor, $this->findDomain($this->getInput('source')) === false)) { + return null; + } + + $html = getSimpleHTMLDOMCached($anchor); + if (!$html) { // Cannot load article + return null; + } + + return $this->fixAnchors($html, $anchor); + } + + /** + * Finds the domain for a given URI + */ + private function findDomain($uri) + { + $matches = []; + preg_match('/(http[s]{0,1}:\/\/.+?(?=\/))/', $uri, $matches); + return $matches[1]; + } + + /* This function is a copy from CNETBridge */ + private function stripWithDelimiters($string, $start, $end) + { + while (strpos($string, $start) !== false) { + $section_to_remove = substr($string, strpos($string, $start)); + $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end)); + $string = str_replace($section_to_remove, '', $string); + } + + return $string; + } + + /* This function is based on CNETBridge */ + private function cleanArticle($article_html) + { + $article_html = $this->stripWithDelimiters($article_html, '<script', '</script>'); + return $article_html; + } } diff --git a/bridges/MondeDiploBridge.php b/bridges/MondeDiploBridge.php index ad3967df..7c897f8f 100644 --- a/bridges/MondeDiploBridge.php +++ b/bridges/MondeDiploBridge.php @@ -1,29 +1,32 @@ <?php -class MondeDiploBridge extends BridgeAbstract { - const MAINTAINER = 'Pitchoule'; - const NAME = 'Monde Diplomatique'; - const URI = 'https://www.monde-diplomatique.fr'; - const CACHE_TIMEOUT = 21600; //6h - const DESCRIPTION = 'Returns most recent results from MondeDiplo.'; +class MondeDiploBridge extends BridgeAbstract +{ + const MAINTAINER = 'Pitchoule'; + const NAME = 'Monde Diplomatique'; + const URI = 'https://www.monde-diplomatique.fr'; + const CACHE_TIMEOUT = 21600; //6h + const DESCRIPTION = 'Returns most recent results from MondeDiplo.'; - private function cleanText($text) { - return trim(str_replace(array(' ', ' '), ' ', $text)); - } + private function cleanText($text) + { + return trim(str_replace([' ', ' '], ' ', $text)); + } - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - foreach($html->find('div.unarticle') as $article) { - $element = $article->parent(); - $title = $element->find('h3', 0)->plaintext; - $datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext; - $item = array(); - $item['uri'] = urljoin(self::URI, $element->href); - $item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs); - $item['content'] = $this->cleanText(str_replace(array($title, $datesAuteurs), '', $element->plaintext)); + foreach ($html->find('div.unarticle') as $article) { + $element = $article->parent(); + $title = $element->find('h3', 0)->plaintext; + $datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext; + $item = []; + $item['uri'] = urljoin(self::URI, $element->href); + $item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs); + $item['content'] = $this->cleanText(str_replace([$title, $datesAuteurs], '', $element->plaintext)); - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/MozillaBugTrackerBridge.php b/bridges/MozillaBugTrackerBridge.php index d284e460..cf6d7f73 100644 --- a/bridges/MozillaBugTrackerBridge.php +++ b/bridges/MozillaBugTrackerBridge.php @@ -1,147 +1,158 @@ <?php -class MozillaBugTrackerBridge extends BridgeAbstract { - const NAME = 'Mozilla Bug Tracker'; - const URI = 'https://bugzilla.mozilla.org'; - const DESCRIPTION = 'DEPRECATED: Use BugzillaBridge instead. +class MozillaBugTrackerBridge extends BridgeAbstract +{ + const NAME = 'Mozilla Bug Tracker'; + const URI = 'https://bugzilla.mozilla.org'; + const DESCRIPTION = 'DEPRECATED: Use BugzillaBridge instead. Returns feeds for bug comments'; - const MAINTAINER = 'AntoineTurmel'; - const PARAMETERS = array( - 'Bug comments' => array( - 'id' => array( - 'name' => 'Bug tracking ID', - 'type' => 'number', - 'required' => true, - 'title' => 'Insert bug tracking ID', - 'exampleValue' => 121241 - ), - 'limit' => array( - 'name' => 'Number of comments to return', - 'type' => 'number', - 'required' => false, - 'title' => 'Specify number of comments to return', - 'defaultValue' => -1 - ), - 'sorting' => array( - 'name' => 'Sorting', - 'type' => 'list', - 'required' => false, - 'title' => 'Defines the sorting order of the comments returned', - 'defaultValue' => 'of', - 'values' => array( - 'Oldest first' => 'of', - 'Latest first' => 'lf' - ) - ) - ) - ); - - private $bugid = ''; - private $bugdesc = ''; - - public function getIcon() { - return self::URI . '/extensions/BMO/web/images/favicon.ico'; - } - - public function collectData(){ - $limit = $this->getInput('limit'); - $sorting = $this->getInput('sorting'); - - // We use the print preview page for simplicity - $html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple', - 86400, - null, - null, - true, - true, - DEFAULT_TARGET_CHARSET, - false, // Do NOT remove line breaks - DEFAULT_BR_TEXT, - DEFAULT_SPAN_TEXT); - - if($html === false) - returnServerError('Failed to load page!'); - - // Fix relative URLs - defaultLinkTo($html, self::URI); - - // Store header information into private members - $this->bugid = trim($html->find('#field-value-bug_id', 0)->plaintext); - $this->bugdesc = $html->find('h1#field-value-short_desc', 0)->plaintext; - - // Get and limit comments - $comments = $html->find('div.change-set'); - - if($limit > 0 && count($comments) > $limit) { - $comments = array_slice($comments, count($comments) - $limit, $limit); - } - - if ($sorting === 'lf') { - $comments = array_reverse($comments, true); - } - - foreach($comments as $comment) { - $comment = $this->inlineStyles($comment); - - $item = array(); - $item['uri'] = $comment->find('h3.change-name', 0)->find('a', 0)->href; - $item['author'] = $comment->find('td.change-author', 0)->plaintext; - $item['title'] = $comment->find('h3.change-name', 0)->plaintext; - $item['timestamp'] = strtotime($comment->find('span.rel-time', 0)->title); - $item['content'] = ''; - - if ($comment->find('.comment-text', 0)) { - $item['content'] = $comment->find('.comment-text', 0)->outertext; - } - - if ($comment->find('div.activity', 0)) { - $item['content'] .= $comment->find('div.activity', 0)->innertext; - } - - $this->items[] = $item; - } - } - - public function getURI(){ - switch($this->queriedContext) { - case 'Bug comments': - return parent::getURI() - . '/show_bug.cgi?id=' - . $this->getInput('id'); - break; - default: return parent::getURI(); - } - } - - public function getName(){ - switch($this->queriedContext) { - case 'Bug comments': - return $this->bugid - . ' - ' - . $this->bugdesc - . ' - ' - . parent::getName(); - break; - default: return parent::getName(); - } - } - - /** - * Adds styles as attributes to tags with known classes - * - * @param object $html A simplehtmldom object - * @return object Returns the original object with styles added as - * attributes. - */ - private function inlineStyles($html){ - foreach($html->find('.bz_closed') as $element) { - $element->style = 'text-decoration:line-through;'; - } - - foreach($html->find('pre') as $element) { - $element->style = 'white-space: pre-wrap;'; - } - - return $html; - } + const MAINTAINER = 'AntoineTurmel'; + const PARAMETERS = [ + 'Bug comments' => [ + 'id' => [ + 'name' => 'Bug tracking ID', + 'type' => 'number', + 'required' => true, + 'title' => 'Insert bug tracking ID', + 'exampleValue' => 121241 + ], + 'limit' => [ + 'name' => 'Number of comments to return', + 'type' => 'number', + 'required' => false, + 'title' => 'Specify number of comments to return', + 'defaultValue' => -1 + ], + 'sorting' => [ + 'name' => 'Sorting', + 'type' => 'list', + 'required' => false, + 'title' => 'Defines the sorting order of the comments returned', + 'defaultValue' => 'of', + 'values' => [ + 'Oldest first' => 'of', + 'Latest first' => 'lf' + ] + ] + ] + ]; + + private $bugid = ''; + private $bugdesc = ''; + + public function getIcon() + { + return self::URI . '/extensions/BMO/web/images/favicon.ico'; + } + + public function collectData() + { + $limit = $this->getInput('limit'); + $sorting = $this->getInput('sorting'); + + // We use the print preview page for simplicity + $html = getSimpleHTMLDOMCached( + $this->getURI() . '&format=multiple', + 86400, + null, + null, + true, + true, + DEFAULT_TARGET_CHARSET, + false, // Do NOT remove line breaks + DEFAULT_BR_TEXT, + DEFAULT_SPAN_TEXT + ); + + if ($html === false) { + returnServerError('Failed to load page!'); + } + + // Fix relative URLs + defaultLinkTo($html, self::URI); + + // Store header information into private members + $this->bugid = trim($html->find('#field-value-bug_id', 0)->plaintext); + $this->bugdesc = $html->find('h1#field-value-short_desc', 0)->plaintext; + + // Get and limit comments + $comments = $html->find('div.change-set'); + + if ($limit > 0 && count($comments) > $limit) { + $comments = array_slice($comments, count($comments) - $limit, $limit); + } + + if ($sorting === 'lf') { + $comments = array_reverse($comments, true); + } + + foreach ($comments as $comment) { + $comment = $this->inlineStyles($comment); + + $item = []; + $item['uri'] = $comment->find('h3.change-name', 0)->find('a', 0)->href; + $item['author'] = $comment->find('td.change-author', 0)->plaintext; + $item['title'] = $comment->find('h3.change-name', 0)->plaintext; + $item['timestamp'] = strtotime($comment->find('span.rel-time', 0)->title); + $item['content'] = ''; + + if ($comment->find('.comment-text', 0)) { + $item['content'] = $comment->find('.comment-text', 0)->outertext; + } + + if ($comment->find('div.activity', 0)) { + $item['content'] .= $comment->find('div.activity', 0)->innertext; + } + + $this->items[] = $item; + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Bug comments': + return parent::getURI() + . '/show_bug.cgi?id=' + . $this->getInput('id'); + break; + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Bug comments': + return $this->bugid + . ' - ' + . $this->bugdesc + . ' - ' + . parent::getName(); + break; + default: + return parent::getName(); + } + } + + /** + * Adds styles as attributes to tags with known classes + * + * @param object $html A simplehtmldom object + * @return object Returns the original object with styles added as + * attributes. + */ + private function inlineStyles($html) + { + foreach ($html->find('.bz_closed') as $element) { + $element->style = 'text-decoration:line-through;'; + } + + foreach ($html->find('pre') as $element) { + $element->style = 'white-space: pre-wrap;'; + } + + return $html; + } } diff --git a/bridges/MozillaSecurityBridge.php b/bridges/MozillaSecurityBridge.php index ab798f00..1b7e7de3 100644 --- a/bridges/MozillaSecurityBridge.php +++ b/bridges/MozillaSecurityBridge.php @@ -1,32 +1,34 @@ <?php -class MozillaSecurityBridge extends BridgeAbstract { - const MAINTAINER = 'm0le.net'; - const NAME = 'Mozilla Security Advisories'; - const URI = 'https://www.mozilla.org/en-US/security/advisories/'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Mozilla Security Advisories'; - const WEBROOT = 'https://www.mozilla.org'; +class MozillaSecurityBridge extends BridgeAbstract +{ + const MAINTAINER = 'm0le.net'; + const NAME = 'Mozilla Security Advisories'; + const URI = 'https://www.mozilla.org/en-US/security/advisories/'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Mozilla Security Advisories'; + const WEBROOT = 'https://www.mozilla.org'; - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $html = defaultLinkTo($html, self::WEBROOT); + $html = defaultLinkTo($html, self::WEBROOT); - $item = array(); - $articles = $html->find('div[id="main-content"] h2'); + $item = []; + $articles = $html->find('div[id="main-content"] h2'); - foreach ($articles as $element) { - //Limit total amount of requests - if(count($this->items) >= 20) { - break; - } - $item['title'] = $element->innertext; - $item['timestamp'] = strtotime($element->innertext); - $item['content'] = $element->next_sibling()->innertext; - $item['uri'] = self::URI . '?' . $item['timestamp']; - $item['uid'] = self::URI . '?' . $item['timestamp']; - $this->items[] = $item; - } - } + foreach ($articles as $element) { + //Limit total amount of requests + if (count($this->items) >= 20) { + break; + } + $item['title'] = $element->innertext; + $item['timestamp'] = strtotime($element->innertext); + $item['content'] = $element->next_sibling()->innertext; + $item['uri'] = self::URI . '?' . $item['timestamp']; + $item['uid'] = self::URI . '?' . $item['timestamp']; + $this->items[] = $item; + } + } } diff --git a/bridges/MsnMondeBridge.php b/bridges/MsnMondeBridge.php index 817f13ad..844aa4a2 100644 --- a/bridges/MsnMondeBridge.php +++ b/bridges/MsnMondeBridge.php @@ -1,40 +1,46 @@ <?php -class MsnMondeBridge extends FeedExpander { - const MAINTAINER = 'kranack'; - const NAME = 'MSN Actu Monde'; - const DESCRIPTION = 'Returns the 10 newest posts from MSN Actualités (full text)'; - const URI = 'https://www.msn.com/fr-fr/actualite'; - const FEED_URL = 'https://rss.msn.com/fr-fr'; - const JSON_URL = 'https://assets.msn.com/content/view/v2/Detail/fr-fr/'; - const LIMIT = 10; +class MsnMondeBridge extends FeedExpander +{ + const MAINTAINER = 'kranack'; + const NAME = 'MSN Actu Monde'; + const DESCRIPTION = 'Returns the 10 newest posts from MSN Actualités (full text)'; + const URI = 'https://www.msn.com/fr-fr/actualite'; + const FEED_URL = 'https://rss.msn.com/fr-fr'; + const JSON_URL = 'https://assets.msn.com/content/view/v2/Detail/fr-fr/'; + const LIMIT = 10; - public function getName() { - return 'MSN Actualités'; - } + public function getName() + { + return 'MSN Actualités'; + } - public function getURI() { - return self::URI; - } + public function getURI() + { + return self::URI; + } - public function collectData() { - $this->collectExpandableDatas(self::FEED_URL, self::LIMIT); - } + public function collectData() + { + $this->collectExpandableDatas(self::FEED_URL, self::LIMIT); + } - protected function parseItem($newsItem) { - $item = parent::parseItem($newsItem); - if (!preg_match('#fr-fr/actualite.*/ar-(?<id>[\w]*)\?#', $item['uri'], $matches)) { - return; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + if (!preg_match('#fr-fr/actualite.*/ar-(?<id>[\w]*)\?#', $item['uri'], $matches)) { + return; + } - $json = json_decode(getContents(self::JSON_URL . $matches['id']), true); - $item['content'] = $json['body']; - if (!empty($json['authors'])) - $item['author'] = reset($json['authors'])['name']; - $item['timestamp'] = $json['createdDateTime']; - foreach($json['tags'] as $tag) { - $item['categories'][] = $tag['label']; - } - return $item; - } + $json = json_decode(getContents(self::JSON_URL . $matches['id']), true); + $item['content'] = $json['body']; + if (!empty($json['authors'])) { + $item['author'] = reset($json['authors'])['name']; + } + $item['timestamp'] = $json['createdDateTime']; + foreach ($json['tags'] as $tag) { + $item['categories'][] = $tag['label']; + } + return $item; + } } diff --git a/bridges/MspabooruBridge.php b/bridges/MspabooruBridge.php index 1c830c0a..76b9a600 100644 --- a/bridges/MspabooruBridge.php +++ b/bridges/MspabooruBridge.php @@ -1,14 +1,15 @@ <?php -class MspabooruBridge extends GelbooruBridge { +class MspabooruBridge extends GelbooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Mspabooru'; + const URI = 'https://mspabooru.com/'; + const DESCRIPTION = 'Returns images from given page'; - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Mspabooru'; - const URI = 'https://mspabooru.com/'; - const DESCRIPTION = 'Returns images from given page'; - - protected function buildThumbnailURI($element){ - return $this->getURI() . 'thumbnails/' . $element->directory - . '/thumbnail_' . $element->image; - } + protected function buildThumbnailURI($element) + { + return $this->getURI() . 'thumbnails/' . $element->directory + . '/thumbnail_' . $element->image; + } } diff --git a/bridges/MydealsBridge.php b/bridges/MydealsBridge.php index 5fd27670..257ef561 100644 --- a/bridges/MydealsBridge.php +++ b/bridges/MydealsBridge.php @@ -1,2081 +1,2080 @@ <?php -class MydealsBridge extends PepperBridgeAbstract { +class MydealsBridge extends PepperBridgeAbstract +{ + const NAME = 'Mydeals bridge'; + const URI = 'https://www.mydealz.de/'; + const DESCRIPTION = 'Zeigt die Deals von mydeals.de'; + const MAINTAINER = 'sysadminstory'; + const PARAMETERS = [ + 'Suche nach Stichworten' => [ + 'q' => [ + 'name' => 'Stichworten', + 'type' => 'text', + 'exampleValue' => 'lamp', + 'required' => true + ], + 'hide_expired' => [ + 'name' => 'Abgelaufenes ausblenden', + 'type' => 'checkbox', + ], + 'hide_local' => [ + 'name' => 'Lokales ausblenden', + 'type' => 'checkbox', + 'title' => 'Deals im physischen Geschäft ausblenden', + ], + 'priceFrom' => [ + 'name' => 'Minimaler Preis', + 'type' => 'text', + 'title' => 'Minmaler Preis in Euros', + 'required' => false + ], + 'priceTo' => [ + 'name' => 'Maximaler Preis', + 'type' => 'text', + 'title' => 'maximaler Preis in Euro', + 'required' => false + ], + ], - const NAME = 'Mydeals bridge'; - const URI = 'https://www.mydealz.de/'; - const DESCRIPTION = 'Zeigt die Deals von mydeals.de'; - const MAINTAINER = 'sysadminstory'; - const PARAMETERS = array( - 'Suche nach Stichworten' => array ( - 'q' => array( - 'name' => 'Stichworten', - 'type' => 'text', - 'exampleValue' => 'lamp', - 'required' => true - ), - 'hide_expired' => array( - 'name' => 'Abgelaufenes ausblenden', - 'type' => 'checkbox', - ), - 'hide_local' => array( - 'name' => 'Lokales ausblenden', - 'type' => 'checkbox', - 'title' => 'Deals im physischen Geschäft ausblenden', - ), - 'priceFrom' => array( - 'name' => 'Minimaler Preis', - 'type' => 'text', - 'title' => 'Minmaler Preis in Euros', - 'required' => false - ), - 'priceTo' => array( - 'name' => 'Maximaler Preis', - 'type' => 'text', - 'title' => 'maximaler Preis in Euro', - 'required' => false - ), - ), - - 'Deals pro Gruppen' => array( - 'group' => array( - 'name' => 'Gruppen', - 'type' => 'list', - 'title' => 'Gruppe, deren Deals angezeigt werden müssen', - 'values' => array( - '1Password' => '1password', - '3D Drucker' => '3d-drucker', - '4K Fernseher' => '4k-fernseher', - '4K Monitore' => '4k-monitor', - '4K Ultra HD Blu-ray' => 'ultra-hd-blu-ray', - '8K Fernseher' => '8k-fernseher', - '32 Zoll Fernseher' => '32-zoll-fernseher', - '55 Zoll Fernseher' => '55-zoll-fernseher', - '65 Zoll Fernseher' => '65-zoll-fernseher', - '75 Zoll Fernseher' => '75-zoll-fernseher', - '1151 Mainboard' => '1151-mainboard', - 'Abus' => 'abus', - 'ABUS Fahrradschlösser' => 'abus-fahrradschloss', - 'Accessoires' => 'accessoires', - 'Acer' => 'acer', - 'Acer Aspire' => 'acer-aspire', - 'Acer Laptops' => 'acer-laptop', - 'Acer Monitore' => 'acer-monitor', - 'Acer Predator' => 'acer-predator', - 'Action Cameras' => 'actioncam', - 'Actionfiguren' => 'actionfiguren', - 'adidas' => 'adidas', - 'adidas Essentials' => 'adidas-neo', - 'adidas Iniki' => 'adidas-iniki', - 'adidas NMD' => 'adidas-nmd', - 'adidas Originals' => 'adidas-originals', - 'adidas Schuhe' => 'adidas-schuhe', - 'adidas Superstar' => 'adidas-superstar', - 'adidas Ultraboost' => 'adidas-ultraboost', - 'adidas ZX Flux' => 'adidas-zx-flux', - 'Adventskalender' => 'adventskalender', - 'AEG' => 'aeg', - 'AEG Waschmaschinen' => 'aeg-waschmaschine', - 'Age of Empires' => 'age-of-empires', - 'AiO Wasserkühlung' => 'aio-wasserkuehlung', - 'AKG' => 'akg', - 'Akkus' => 'akkus', - 'Akkuschrauber' => 'akkuschrauber', - 'Alfa Romeo' => 'alfa-romeo', - 'Alienware' => 'alienware', - 'Alkohol' => 'alkohol', - 'All Inclusive Reisen' => 'all-inclusive', - 'All in One PCs' => 'all-in-one-pcs', - 'AM4 Mainboard' => 'am4-mainboard', - 'Amazfit' => 'xiaomi-amazfit', - 'Amazfit Bip' => 'amazfit-bip', - 'Amazfit GTS' => 'amazfit-gts', - 'Amazon Echo' => 'amazon-echo', - 'Amazon Echo Dot' => 'amazon-echo-dot', - 'Amazon Echo Plus' => 'amazon-echo-plus', - 'Amazon Echo Show' => 'amazon-echo-show', - 'Amazon Echo Show 5' => 'amazon-echo-show-5', - 'Amazon Echo Show 8' => 'amazon-echo-show-8', - 'Amazon Echo Spot' => 'amazon-echo-spot', - 'Amazon Fire TV Cube' => 'fire-tv-cube', - 'Amazon Fire TV Stick' => 'fire-tv', - 'Amazon Fire TV Stick 4K' => 'fire-tv-stick-4k', - 'Amazon Tablets' => 'amazon-tablet', - 'Amazon Warehouse Deals' => 'amazon-warehouse-deals', - 'AMD' => 'amd', - 'AMD Radeon' => 'amd-radeon', - 'AMD Radeon VII' => 'vega-7', - 'AMD RX Vega' => 'amd-vega', - 'AMD Ryzen' => 'amd-ryzen', - 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x', - 'American Express' => 'american-express', - 'amiibo' => 'amiibo', - 'Analoguhren' => 'analoguhren', - 'Android Apps' => 'android-apps', - 'Android Smartphones' => 'android-smartphones', - 'Angelzubehör' => 'angelsport', - 'Animal Crossing' => 'animal-crossing', - 'Animal Crossing: New Horizons' => 'animal-crossing-new-horizons', - 'Anime' => 'anime', - 'Ankündigungen' => 'ankundigungen', - 'Anno 1800' => 'anno-1800', - 'Anthem' => 'anthem', - 'Anzug' => 'anzug', - 'AOC' => 'aoc', - 'Apex Legends' => 'apex-legends', - 'Apotheke' => 'apotheke', - 'Apple' => 'apple', - 'Apple AirPods' => 'airpods', - 'Apple AirPods 2' => 'airpods-2', - 'Apple AirPods Max' => 'airpods-max', - 'Apple AirPods Pro' => 'airpods-pro', - 'Apple EarPods' => 'apple-earpods', - 'Apple HomePod' => 'homepod', - 'Apple HomePod mini' => 'apple-homepod-mini', - 'Apple Kopfhörer' => 'apple-kopfhoerer', - 'Apple Magic Mouse 2' => 'apple-magic-mouse-2', - 'Apple Pencil' => 'apple-pencil', - 'Apple Pencil 2' => 'apple-pencil-2', - 'Apple TV' => 'apple-tv', - 'Apple Watch' => 'apple-watch', - 'Apple Watch 3' => 'apple-watch-3', - 'Apple Watch 4' => 'apple-watch-4', - 'Apple Watch 5' => 'apple-watch-5', - 'Apple Watch 6' => 'apple-watch-6', - 'Apple Watch SE' => 'apple-watch-se', - 'Apps' => 'apps', - 'Aquaristik' => 'aquaristik', - 'Arbeitsspeicher' => 'arbeitsspeicher', - 'Arbeitszimmermöbel' => 'arbeitszimmer', - 'ASICS' => 'asics', - 'Assassin's Creed' => 'assassins-creed', - 'Assassin's Creed: Valhalla' => 'assassins-creed-valhalla', - 'Assassin's Creed Odyssey' => 'assassins-creed-odyssey', - 'Assassin's Creed Origins' => 'assassins-creed-origins', - 'ASTRO Gaming A50' => 'astro-gaming-a50', - 'ASUS' => 'asus', - 'ASUS Laptops' => 'asus-laptop', - 'Asus Mainboard' => 'asus-mainboard', - 'Asus Monitore' => 'asus-monitor', - 'ASUS ROG' => 'asus-rog', - 'ASUS Smartphones' => 'asus-smartphones', - 'Asus ZenBook' => 'asus-zenbook', - 'ASUS ZenFone 5' => 'asus-zenfone-5', - 'ASUS ZenFone 5Z' => 'asus-zenfone-5z', - 'Audi' => 'audi', - 'Audio & HiFi' => 'audio-hifi', - 'Audioverstärker' => 'audioverstaerker', - 'Audio Zubehör' => 'audio-zubehoer', - 'Aukey' => 'aukey', - 'Außenleuchten' => 'aussenleuchten', - 'Auto & Motorrad' => 'auto-motorrad', - 'Auto Bild' => 'auto-bild', - 'Auto Leasing' => 'auto-leasing', - 'Auto Leasing Gewerbe' => 'gewerbe-leasing', - 'Auto Leasing Privat' => 'privat-leasing', - 'Automatikuhren' => 'automatikuhr', - 'auto motor und sport' => 'auto-motor-sport', - 'Autoradio' => 'autoradio', - 'Auto Teile' => 'autoteile', - 'Autowäsche' => 'autowaesche', - 'Auto Zubehör' => 'auto', - 'AVM FRITZ!Box' => 'avm-fritz-box', - 'AVM FRITZ!Box 7490' => 'avm-fritz-box-7490', - 'AVM FRITZ!Box 7530' => 'avm-fritz-box-7530', - 'AVM FRITZ!Box 7580' => 'avm-fritz-box-7580', - 'AVM FRITZ!Box 7590' => 'avm-fritz-box-7590', - 'AVM FRITZ! DECT 301' => 'avm-fritz-dect-301', - 'AV Receiver' => 'av-receiver', - 'Baby & Kind' => 'kinder', - 'Baby-Erstausstattung' => 'baby-erstausstattung', - 'Babybetten' => 'babybetten', - 'Baby Born' => 'baby-born', - 'Babykleidung' => 'babybekleidung', - 'Babynahrung' => 'babynahrung', - 'Babyphone' => 'babyphone', - 'Backofen & Herd' => 'backofen-herd', - 'Backwaren' => 'backwaren', - 'Backzubehör' => 'backzubehoer', - 'Bademode' => 'bademode', - 'Badmöbel' => 'badezimmer', - 'Bahn-Tickets' => 'bahntickets', - 'Bahncard' => 'bahncard', - 'Balkonmöbel' => 'balkonmoebel', - 'Ballerinas' => 'ballerinas', - 'Bang & Olufsen' => 'bang-olufsen', - 'Bank' => 'bank', - 'Barbie' => 'barbie', - 'Barclaycard' => 'barclaycard', - 'Bartschneider' => 'bartschneider', - 'Batterien' => 'batterien', - 'Battle.net' => 'battle-net', - 'Battlefield' => 'battlefield', - 'Battlefield 1' => 'battlefield-1', - 'Battlefield 5' => 'battlefield-5', - 'Bauknecht' => 'bauknecht', - 'Bauknecht Waschmaschinen' => 'bauknecht-waschmaschine', - 'Baumarkt' => 'baumarkt', - 'Bayonetta' => 'bayonetta', - 'Bayonetta 2' => 'bayonetta-2', - 'Beamer' => 'beamer', - 'Beamer Leinwand' => 'beamer-leinwand', - 'Beats by Dre' => 'beats-by-dre', - 'Beats Solo3' => 'beats-solo3', - 'Beats Solo Pro' => 'beats-solo-pro', - 'Beats Studio3' => 'beats-studio3', - 'Beauty & Gesundheit' => 'beauty', - 'Beko' => 'beko', - 'Beleuchtung' => 'beleuchtung', - 'Belkin' => 'belkin', - 'Ben & Jerry's' => 'ben-jerrys', - 'Bench' => 'bench', - 'BenQ' => 'benq', - 'BenQ Monitore' => 'benq-monitor', - 'be quiet!' => 'be-quiet', - 'be quiet! Netzteile' => 'be-quiet-netzteil', - 'Besteck' => 'besteck', - 'Bethesda' => 'bethesda', - 'Betten' => 'betten', - 'Bettwäsche' => 'bettwaesche', - 'beyerdynamic' => 'beyerdynamic', - 'Beyerdynamic MMX 300' => 'beyerdynamic-mmx-300', - 'BHs' => 'bhs', - 'Bier' => 'bier', - 'Biking & Urban Sport' => 'biking-urban-sport', - 'Bildbearbeitungsprogramme' => 'bildbearbeitungsprogramme', - 'Birkenstock' => 'birkenstock', - 'Black & Decker' => 'black-and-decker', - 'Blackberry Smartphones' => 'blackberry', - 'Black Desert Online' => 'black-desert-online', - 'Blazer' => 'blazer', - 'Blood & Truth' => 'blood-truth', - 'Blu-ray' => 'blu-ray', - 'Blu-ray Player' => 'blu-ray-player', - 'Bluetooth Kopfhörer' => 'bluetooth-kopfhoerer', - 'Bluetooth Lautsprecher' => 'bluetooth-lautsprecher', - 'Blumen' => 'blumen', - 'Blusen' => 'blusen', - 'BMW' => 'bmw', - 'Bodenbelag' => 'bodenbelag', - 'Boho-Chic wohnen' => 'boho-chich-wohnen', - 'Bohrer' => 'bohrer', - 'Bohrhämmer' => 'bohrhaemmer', - 'Bohrmaschinen' => 'bohrmaschinen', - 'Bollerwagen' => 'bollerwagen', - 'Bombay Gin' => 'bombay', - 'Borderlands' => 'borderlands', - 'Borderlands 3' => 'borderlands-3', - 'Bosch' => 'bosch', - 'Bosch Akkuschrauber' => 'bosch-akkuschrauber', - 'Bosch Geschirrspüler' => 'bosch-geschirrspueler', - 'Bosch Kühlschränke' => 'bosch-kuehlschrank', - 'Bosch Waschmaschinen' => 'bosch-waschmaschine', - 'Bose' => 'bose', - 'Bose Headphones 700' => 'bose-headphones-700', - 'Bose Home Speaker 500' => 'bose-home-speaker-500', - 'Bose Kopfhörer' => 'bose-kopfhoerer', - 'Bose QuietComfort' => 'bose-quietcomfort', - 'Bose QuietComfort 35 II' => 'bose-quiet-comfort-35-ii', - 'Bose Solo 5' => 'bose-solo-5', - 'Bose SoundLink' => 'bose-soundlink', - 'Bose SoundTouch' => 'bose-soundtouch', - 'BOSS' => 'boss', - 'Bourbon' => 'bourbon', - 'Bowers & Wilkins' => 'bowers-wilkins', - 'Boxershorts' => 'boxershorts', - 'Boxspringbetten' => 'boxspringbetten', - 'Braun' => 'braun', - 'Braun Rasierer' => 'braun-rasierer', - 'Braun Series 3' => 'braun-series-3', - 'Braun Series 5' => 'braun-series-5', - 'Braun Series 7' => 'braun-series-7', - 'Braun Series 9' => 'braun-series-9', - 'Bridgekameras' => 'bridgekamera', - 'Brigitte' => 'brigitte', - 'Brillen & Kontaktlinsen' => 'brillen', - 'Brita' => 'brita', - 'Britax Römer' => 'britax-roemer', - 'Brotaufstrich' => 'brotaufstrich', - 'Brother Drucker' => 'brother-drucker', - 'Bücher' => 'buecher', - 'Bücher, Magazine & Zeitschriften' => 'buecher-zeitschriften', - 'bugatti' => 'bugatti', - 'Bügeleisen' => 'buegeleisen', - 'Bügeln' => 'buegeln', - 'Buggy' => 'buggy', - 'Burger' => 'burger', - 'BURNHARD' => 'burnhard', - 'Bürobedarf' => 'buerobedarf', - 'Bürostühle' => 'buerostuhl', - 'Bus & Bahn' => 'bus-bahn', - 'Business Mode' => 'business-mode', - 'c't – Magazin für Computertechnik' => 'ct-magazin-computertechnik', - 'Cafissimo' => 'cafissimo', - 'Call of Duty' => 'call-of-duty', - 'Call of Duty: Black Ops 4' => 'call-of-duty-black-ops-4', - 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war', - 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare', - 'Call of Duty: Modern Warfare' => 'call-of-duty-modern-warfare', - 'Call of Duty: Warzone' => 'call-of-duty-warzone', - 'Call of Duty: WW2' => 'call-of-duty-ww2', - 'Calvin Klein' => 'calvin-klein', - 'Camcorder' => 'camcorder', - 'Campen' => 'campen', - 'Canon' => 'canon', - 'Canon Drucker' => 'canon-drucker', - 'Canon EOS' => 'canon-eos', - 'Canon Kameras' => 'canon-kameras', - 'Canon PowerShot' => 'canon-powershot', - 'CANTON' => 'canton', - 'Caps' => 'caps', - 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker', - 'Capture One' => 'capture-one', - 'Carhartt' => 'carhartt', - 'Carsharing' => 'carsharing', - 'Casio' => 'casio', - 'Cheap Monday' => 'cheapmonday', - 'Chevrolet' => 'chevrolet', - 'China Handys' => 'china-handys', - 'Chip (Magazin)' => 'chip-magazin', - 'Chips' => 'chips', - 'Christbaumschmuck' => 'christbaumschmuck', - 'Christbaumständer' => 'christbaumstaender', - 'Chromebook' => 'chromebook', - 'Chronographen' => 'chronograph', - 'Chucks' => 'chucks', - 'Citroen' => 'citroen', - 'Coca-Cola' => 'coca-cola', - 'Comics' => 'comics', - 'Computer' => 'computer', - 'Computer & Tablets' => 'computer-tablet', - 'Computer Bild' => 'computer-bild', - 'Controller' => 'controller', - 'Converse' => 'converse', - 'Convertibles' => 'convertibles', - 'Corsair' => 'corsair', - 'Corsair VOID PRO' => 'corsair-void-pro', - 'Couchtische' => 'couchtische', - 'Coupons' => 'coupons', - 'CPU-Kühler' => 'cpu-kuehler', - 'Craghoppers' => 'craghoppers', - 'Crocs' => 'crocs', - 'Crucial' => 'crucial', - 'Cupra' => 'cupra', - 'Cyberpunk 2077' => 'cyberpunk-2077', - 'cybex' => 'cybex', - 'D-Link' => 'd-link', - 'DAB Radios' => 'dab-radios', - 'Dacia' => 'dacia', - 'Damenbekleidung' => 'fashion-frauen', - 'Damenschuhe' => 'damenschuhe', - 'Dampfbügelstation' => 'dampfbuegelstation', - 'Dampfgarer' => 'dampfgarer', - 'Dampfreiniger' => 'dampfreiniger', - 'Dark Souls' => 'dark-souls', - 'Dashcam' => 'dashcam', - 'Datentarif' => 'datentarif', - 'Daypack' => 'daypack', - 'Days Gone' => 'days-gone', - 'DC Shoes' => 'dc-shoes', - 'DDR3 RAM' => 'ddr3-ram', - 'DDR4 RAM' => 'ddr4-ram', - 'De'Longhi' => 'delonghi', - 'Death Stranding' => 'death-stranding', - 'Deckenlampen' => 'deckenlampen', - 'DECT Telefone' => 'telefone', - 'Dekoration' => 'dekoration', - 'Dell' => 'dell', - 'Dell Laptops' => 'dell-laptop', - 'Dell Monitore' => 'dell-monitor', - 'Dell XPS' => 'dell-xps', - 'Denon' => 'denon', - 'Deo' => 'deo', - 'Depot' => 'depot', - 'DER SPIEGEL' => 'der-spiegel', - 'Designermöbel' => 'designermoebel', - 'Desigual' => 'desigual', - 'Desinfektionsmittel' => 'desinfektionsmittel', - 'Desktop PCs' => 'desktop-pc', - 'Dessous' => 'dessous', - 'Destiny' => 'destiny', - 'Destiny 2' => 'destiny-2', - 'Deus Ex' => 'deus-ex', - 'Deus Ex: Mankind' => 'deus-ex-mankind', - 'Deuter' => 'deuter', - 'DeutschlandCard' => 'deutschlandcard', - 'devolo' => 'devolo', - 'DeWalt' => 'dewalt', - 'Die drei Fragezeichen' => 'die-drei-fragezeichen', - 'Die Eiskönigin' => 'die-eiskoenigin', - 'Dienstleistungen & Verträge' => 'dienstleistungen-vertraege', - 'Dies & Das' => 'dies-das', - 'Diesel' => 'diesel', - 'Die Sims' => 'die-sims', - 'Die Sims 4' => 'die-sims-4', - 'Die Zeit' => 'die-zeit', - 'Digitalreceiver' => 'digitalreceiver', - 'Digitaluhren' => 'digitaluhr', - 'Direktflüge' => 'direktfluege', - 'Dirt Devil' => 'dirt-devil', - 'Dishonored' => 'dishonored', - 'Dishonored 2: Das Vermächtnis der Maske' => 'dishonored-2', - 'Disney' => 'disney', - 'Disney+' => 'disney-plus', - 'DJI' => 'dji', - 'DJI Osmo Pocket' => 'dji-osmo-pocket', - 'Dockers' => 'dockers', - 'Dolce Gusto' => 'dolce-gusto', - 'DOOM Eternal' => 'doom-eternal', - 'Douglas Adventskalender' => 'douglas-adventskalender', - 'Dr. Martens' => 'dr-martens', - 'Dragon Ball' => 'dragon-ball', - 'Dragon Ball FighterZ' => 'dragon-ball-fighterz', - 'Dragon Ball Z: Kakarot' => 'dragon-ball-z-kakarot', - 'Dragon Quest Builders' => 'dragon-quest-builders', - 'Dragon Quest Builders 2' => 'dragon-quest-builders-2', - 'Dreame Staubsauger' => 'xiaomi-staubsauger', - 'Dreame T20' => 'dreame-t20', - 'Dreame V9' => 'xiaomi-dreame-v9', - 'Dreame V10' => 'xiaomi-dreame-v10', - 'Dreame V11' => 'xiaomi-dreame-v11', - 'Drohnen' => 'drohnen', - 'Drucker' => 'drucker', - 'Druckerpatronen' => 'druckerpatronen', - 'Druckerzubehör' => 'druckerzubehoer', - 'DSL & Kabel' => 'dsl', - 'Dunstabzugshauben' => 'dunstabzugshauben', - 'Durex' => 'durex', - 'Duscharmaturen' => 'duscharmaturen', - 'Duschgel' => 'duschgel', - 'Duschköpfe' => 'duschkoepfe', - 'DVD' => 'dvd', - 'Dyson' => 'dyson', - 'Dyson Staubsauger' => 'dyson-staubsauger', - 'Dyson V6' => 'dyson-v6', - 'Dyson V7' => 'dyson-v7', - 'Dyson V8' => 'dyson-v8', - 'Dyson V10' => 'dyson-v10', - 'Dyson V11' => 'dyson-v11', - 'Dyson V11 Absolute' => 'dyson-v11-absolute', - 'Dyson V11 Animal' => 'dyson-v11-animal', - 'E-Bikes' => 'e-bikes', - 'E-Scooter' => 'e-scooter', - 'E-Scooter Sharing' => 'e-scooter-sharing', - 'E-Zigaretten' => 'e-zigaretten', - 'Eastpak' => 'eastpak', - 'eBook Reader' => 'ebook-reader', - 'eBooks' => 'ebooks', - 'Ecovacs' => 'ecovacs', - 'Ecovacs Deebot 900' => 'ecovacs-deebot-900', - 'Ecovacs Deebot OZMO 930' => 'ecovacs-deebot-ozmo-930', - 'Edifier' => 'edifier', - 'Edifier R1280DB' => 'edifier-r1280db', - 'Edifier R1280T' => 'edifier-r1280t', - 'Einhell' => 'einhell', - 'Eis' => 'eis', - 'Elektrische Zahnbürsten' => 'elektrische-zahnbuersten', - 'Elektrogrills' => 'elektrogrill', - 'Elektroheizungen' => 'elektroheizungen', - 'Elektronik' => 'elektronik', - 'Elektronik Zubehör' => 'elektronikzubehoer', - 'Elektrorasierer' => 'elektrorasierer', - 'Elektroroller' => 'elektroroller', - 'Elektrowerkzeuge' => 'elektrowerkzeug', - 'Elephone' => 'elephone', - 'ELLE' => 'elle', - 'Emsa' => 'emsa', - 'Energy Drinks' => 'energy-drinks', - 'Entsafter' => 'entsafter', - 'Epilierer' => 'epilierer', - 'Epson' => 'epson', - 'Epson Drucker' => 'epson-drucker', - 'Erotik' => 'erotik', - 'Error Fare' => 'error-fare', - 'Espressomaschinen' => 'espressomaschinen', - 'Esprit' => 'esprit', - 'Esstische' => 'esstisch', - 'Esszimmer' => 'esszimmer', - 'Eterna' => 'eterna', - 'EUROtronic Comet DECT' => 'eurotronic-comet-dect', - 'Externe Festplatten' => 'externe-festplatten', - 'F1 2017' => 'f1-2017', - 'F1 2019' => 'f1-2019', - 'F1 2020' => 'f1-2020', - 'Fahrräder' => 'fahrraeder', - 'Fahrradhelme' => 'fahrradhelme', - 'Fahrradrucksäcke' => 'fahrradrucksack', - 'Fahrradschlösser' => 'fahrradschloss', - 'Fahrradteile' => 'fahrradteile', - 'Fahrradträger' => 'fahrradtraeger', - 'Fahrradzubehör' => 'fahrradzubehoer', - 'Fahrzeuge' => 'fahrzeuge', - 'Falke' => 'falke', - 'Fallout' => 'fallout', - 'Fallout 4' => 'fallout-4', - 'Fallout 76' => 'fallout-76', - 'Family & Kids' => 'family-kids', - 'Far Cry' => 'far-cry', - 'Far Cry 5' => 'far-cry-5', - 'Far Cry New Dawn' => 'far-cry-new-dawn', - 'Fashion & Accessoires' => 'fashion-accessoires', - 'Fast Food' => 'fast-food', - 'Felgen' => 'felgen', - 'Fenstersauger' => 'fenstersauger', - 'Fernbus-Tickets' => 'fernbus', - 'Fernseher' => 'fernseher', - 'Fertiggerichte' => 'fertiggerichte', - 'Festplatten' => 'festplatten', - 'Festplattengehäuse' => 'festplattengehaeuse', - 'FFP2 Masken' => 'ffp2-masken', - 'Fiat' => 'fiat', - 'FIFA' => 'fifa', - 'FIFA 17' => 'fifa-17', - 'FIFA 18' => 'fifa-18', - 'FIFA 19' => 'fifa-19', - 'FIFA 20' => 'fifa-20', - 'FIFA 21' => 'fifa-21', - 'FILA' => 'fila', - 'Filme & Serien' => 'filme-serien', - 'Filterkaffeemaschinen' => 'filterkaffeemaschinen', - 'Final Fantasy' => 'final-fantasy', - 'Final Fantasy 7' => 'final-fantasy-7', - 'Finanzen- und Steuersoftware' => 'finanzen-und-steuersoftware', - 'Finish' => 'finish', - 'Fisch & Meeresfrüchte' => 'fisch-meeresfruechte', - 'Fischertechnik' => 'fischertechnik', - 'Fisher-Price' => 'fisher-price', - 'Fiskars' => 'fiskars', - 'Fissler' => 'fissler', - 'fitbit' => 'fitbit', - 'Fitness & Running' => 'fitness', - 'Fitness Apps' => 'fitness-apps', - 'Fitnessstudio' => 'fitnessstudio', - 'Fitnesstracker' => 'fitnesstracker', - 'Fjällräven' => 'fjaellraeven', - 'Fleisch & Wurst' => 'fleisch-wurst', - 'Fliesenschneider' => 'fliesenschneider', - 'Flüge' => 'fluege', - 'Flurmöbel' => 'flurmoebel', - 'FOCUS' => 'focus', - 'Ford' => 'ford', - 'For Honor' => 'for-honor', - 'Formel 1 Games' => 'formel-1', - 'Fortnite' => 'fortnite', - 'Forza' => 'forza', - 'Forza Horizon' => 'forza-horizon', - 'Forza Horizon 4' => 'forza-horizon-4', - 'Forza Motorsport' => 'forza-motorsport', - 'Forza Motorsport 7' => 'forza-7', - 'Fossil' => 'fossil', - 'Foto & Kamera' => 'foto-video', - 'Foto Apps' => 'foto-apps', - 'Fotobücher' => 'fotobuecher', - 'Fototapete' => 'fototapete', - 'Fragen & Gesuche' => 'gesuche', - 'Frankfurter Allgemeine Zeitung (F.A.Z.)' => 'frankfurter-allgemeine-zeitung', - 'FreeSync Monitore' => 'freesync-monitor', - 'Freizeitpark-Tickets' => 'freizeitpark', - 'Freizeitsport' => 'freizeitsport', - 'Fritteusen' => 'fritteusen', - 'Frontlader' => 'frontlader', - 'Frühlingsdeko' => 'fruehlingsdeko', - 'Frühstücksflocken' => 'fruehstuecksflocken', - 'Fruit of the Loom' => 'fruit-of-the-loom', - 'Fujifilm' => 'fujifilm', - 'Füller' => 'fueller', - 'Full HD-Beamer' => 'full-hd-beamer', - 'Fun Factory' => 'fun-factory', - 'FurReal Friends' => 'furreal-friends', - 'Fußball' => 'fussball', - 'Fußball-Trikots' => 'fussball-trikots', - 'Fußballschuhe' => 'fussballschuhe', - 'G-Star' => 'g-star', - 'G-Sync Monitore' => 'g-sync-monitor', - 'Game of Thrones' => 'game-of-thrones', - 'Gaming' => 'gaming', - 'Gaming Headsets' => 'gaming-headset', - 'Gaming Laptops' => 'gaming-laptop', - 'Gaming Mäuse' => 'gaming-maus', - 'Gaming Monitore' => 'gaming-monitor', - 'Gaming PCs' => 'gaming-pc', - 'Gaming Stühle' => 'gaming-stuhl', - 'Gaming Tastaturen' => 'gaming-tastatur', - 'Gaming Zubehör' => 'spielekonsolen-zubehoer', - 'Ganzjahresreifen' => 'ganzjahresreifen', - 'GAP' => 'gap', - 'Gardena' => 'gardena', - 'Garderobe' => 'garderobe', - 'Garmin' => 'garmin', - 'Garmin Fenix' => 'garmin-fenix', - 'Garten' => 'garten', - 'Garten & Baumarkt' => 'garten-baumarkt', - 'Gartenarbeit' => 'gartenarbeit', - 'Gartenbank' => 'gartenbank', - 'Gartenliegen' => 'sonnenliegen', - 'Gartenmöbel' => 'gartenmoebel', - 'Gartenstühle' => 'gartenstuehle', - 'Gartentische' => 'gartentische', - 'Gasgrills' => 'gasgrill', - 'Gastarif' => 'gastarif', - 'Gears 5' => 'gears-5', - 'Gears of War' => 'gears-of-war', - 'Gefrierschränke' => 'gefrierschrank', - 'Geld-zurück-Aktionen' => 'geld-zurueck', - 'Geldbörsen' => 'geldboersen', - 'Gemüse' => 'gemuese', - 'Geox' => 'geox', - 'Geschirr' => 'geschirr', - 'Geschirrspüler' => 'geschirrspueler', - 'Gesellschaftsspiele' => 'gesellschaftsspiele', - 'Gesichtspflege' => 'gesichtspflege', - 'Gesundheit' => 'gesundheit', - 'Getränke' => 'getraenke', - 'Gewinnspiele' => 'gewinnspiele', - 'GHD' => 'ghd', - 'Ghost of Tsushima' => 'ghost-of-tsushima', - 'GIGABYTE' => 'gigabyte', - 'Gigaset' => 'gigaset', - 'Gillette' => 'gillette', - 'Gillette Rasierer' => 'gillette-rasierer', - 'Gin' => 'gin', - 'Girokonto' => 'konto', - 'Glamour' => 'glamour', - 'Glamourös wohnen' => 'glamouroes-wohnen', - 'Gläser' => 'glaeser', - 'Glätteisen' => 'glaetteisen', - 'Gleitgel' => 'gleitgel', - 'Glühwein' => 'gluehwein', - 'God of War' => 'god-of-war', - 'Google Chromecast' => 'chromecast', - 'Google Chromecast mit Google TV' => 'chromecast-mit-google-tv', - 'Google Chromecast Ultra' => 'chromecast-ultra', - 'Google Home' => 'google-home', - 'Google Home Max' => 'google-home-max', - 'Google Home Mini' => 'google-home-mini', - 'Google Nest Hub' => 'google-nest-hub', - 'Google Pixel' => 'google-pixel', - 'Google Pixel 2' => 'google-pixel-2', - 'Google Pixel 3' => 'google-pixel-3', - 'Google Pixel 4' => 'google-pixel-4', - 'Google Pixel 4 XL' => 'google-pixel-4xl', - 'Google Pixel 4a' => 'google-pixel-4a', - 'Google Pixel 4a 5G' => 'google-pixel-4a-5g', - 'Google Pixel 5' => 'google-pixel-5', - 'Google Smartphones' => 'google-smartphones', - 'Google Stadia Konsolen' => 'google-stadia', - 'GoPro Action Cameras' => 'gopro', - 'GoPro HERO 7' => 'gopro-hero-7', - 'GoPro HERO 8' => 'gopro-hero-8', - 'GoPro HERO 9' => 'gopro-hero-9', - 'Gorenje' => 'gorenje', - 'Grafikkarten' => 'grafikkarten', - 'Gran Turismo' => 'gran-turismo', - 'Gran Turismo Sport' => 'gran-turismo-sport', - 'Grazia' => 'grazia', - 'Grills' => 'grill', - 'Grillzubehör' => 'grillzubehoer', - 'Grundig' => 'grundig', - 'GTA' => 'gta', - 'GTA V' => 'gta-v', - 'GTX 1060' => 'gtx-1060', - 'GTX 1070' => 'gtx-1070', - 'GTX 1080' => 'gtx-1080', - 'GTX 1080 Ti' => 'gtx-1080-ti', - 'GTX 1660' => 'gtx-1660', - 'GTX 1660 Ti' => 'gtx-1660-ti', - 'Gucci' => 'gucci', - 'Gummistiefel' => 'gummistiefel', - 'Gürtel' => 'guertel', - 'Gutscheinfehler' => 'gutscheinfehler', - 'Haarentfernung' => 'haarentfernung', - 'Haargel' => 'haargel', - 'Haarpflege' => 'haarpflege', - 'Haarschneidemaschinen' => 'haarschneidemaschinen', - 'Haarspray' => 'haarspray', - 'Haartrockner' => 'haartrockner', - 'Haftpflichtversicherung' => 'haftpflichtversicherung', - 'Hama' => 'hama', - 'Handelsblatt' => 'handelsblatt', - 'Handmixer' => 'handmixer', - 'Handtaschen' => 'handtaschen', - 'Handtücher' => 'handtuecher', - 'Handwerkzeuge' => 'handwerkzeug', - 'Handy & Smartphone Zubehör' => 'smartphone-zubehoer', - 'Handyhalterung' => 'handyhalterung', - 'Handyhüllen' => 'handyhuelle', - 'Handys mit Vertrag' => 'handys-mit-vertrag', - 'Handys ohne Vertrag' => 'handys-ohne-vertrag', - 'Handyversicherung' => 'handyversicherung', - 'Handyverträge' => 'handyvertraege', - 'Handyverträge 3 Monate Kündigungsfrist' => 'handyvertraege-3-monate-kuendigungsfrist', - 'Handyverträge monatlich kündbar' => 'handyvertraege-monatlich-kuendbar', - 'Hängematten' => 'haengematten', - 'Hanteln' => 'hanteln', - 'Haribo' => 'haribo', - 'Harman Kardon' => 'harman-kardon', - 'Harry Potter' => 'harry-potter', - 'Hasbro' => 'hasbro', - 'Haushaltsartikel' => 'haushaltsartikel', - 'Haushaltsgeräte' => 'haushaltsgeraete', - 'Haushaltswaren' => 'haushaltswaren', - 'Hausratversicherung' => 'hausratsversicherung', - 'Hausschuhe' => 'hausschuhe', - 'Haustier' => 'haustier', - 'Hautpflege' => 'hautpflege', - 'Head & Shoulders' => 'head-and-shoulders', - 'Heckenscheren' => 'heckenschere', - 'Heimkino' => 'heimkino', - 'Heimtextilien' => 'heimtextilien', - 'Heißluftfritteusen' => 'heissluftfriteuse', - 'Heizkörperthermostat' => 'heizkoerperthermostat', - 'Heizungen' => 'heizungen', - 'Hemden' => 'hemden', - 'Hendrick's Gin' => 'hendricks-gin', - 'Herbstdeko' => 'herbstdeko', - 'Herrenbekleidung' => 'fashion-maenner', - 'Herrenschuhe' => 'herrenschuhe', - 'HiPP' => 'hipp', - 'Hisense' => 'hisense', - 'Hochbetten' => 'hochbetten', - 'Hochdruckreiniger' => 'hochdruckreiniger', - 'Hochstuhl' => 'hochstuhl', - 'Hollywoodschaukel' => 'hollywoodschaukel', - 'Home & Living' => 'home-living', - 'homee' => 'homee', - 'Honda' => 'honda', - 'Honor' => 'honor', - 'Honor 5' => 'honor-5', - 'Honor 6' => 'honor-6', - 'Honor 7X' => 'honor-7', - 'Honor 8' => 'honor-8', - 'Honor 9' => 'honor-9', - 'Honor 20' => 'honor-20', - 'Honor 20 Lite' => 'honor-20-lite', - 'Honor Band 4' => 'honor-band-4', - 'Honor Band 5' => 'honor-band-5', - 'Honor Play' => 'honor-play', - 'Honor Smartphones' => 'honor-smartphones', - 'Honor View 10' => 'honor-view-10', - 'Honor View 20' => 'honor-view-20', - 'Hoodies' => 'hoodies', - 'Hörbücher' => 'hoerbuecher', - 'Horizon Zero Dawn' => 'horizon-zero-dawn', - 'Hörspiele' => 'hoerspiele', - 'Hörzu' => 'hoerzu', - 'Hosen' => 'hosen', - 'Hotels & Unterkünfte' => 'hotel', - 'Hot Wheels' => 'hot-wheels', - 'Hoverboards' => 'hoverboards', - 'HP' => 'hp', - 'HP Drucker' => 'hp-drucker', - 'HP Laptops' => 'hp-laptop', - 'HP OMEN' => 'hp-omen', - 'HP Pavilion' => 'hp-pavilion', - 'HTC 10' => 'htc-10', - 'HTC Desire 12' => 'htc-desire', - 'HTC Smartphones' => 'htc-smartphones', - 'HTC U11' => 'htc-u11', - 'HTC Vive' => 'htc-vive', - 'Huawei' => 'huawei', - 'Huawei Kopfhörer' => 'huawei-kopfhoerer', - 'Huawei Mate 9' => 'huawei-mate-9', - 'Huawei Mate 10' => 'huawei-mate-10', - 'Huawei Mate 20' => 'huawei-mate-20', - 'Huawei Mate 20 Lite' => 'huawei-mate-20-lite', - 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro', - 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro', - 'Huawei MateBook' => 'huawei-matebook', - 'Huawei P10' => 'huawei-p10', - 'Huawei P20' => 'huawei-p20', - 'Huawei P30' => 'huawei-p30', - 'Huawei P30 Lite' => 'huawei-p30-lite', - 'Huawei P30 Pro' => 'huawei-p30-pro', - 'Huawei P40' => 'huawei-p40', - 'Huawei P40 Lite' => 'huawei-p40-lite', - 'Huawei P40 Pro' => 'huawei-p40-pro', - 'Huawei P Smart' => 'huawei-p-smart', - 'Huawei Smartphones' => 'huawei-smartphones', - 'Huawei Tablets' => 'huawei-mediapad', - 'Huawei Watch GT2' => 'huawei-watch-gt2', - 'Huawei Y7' => 'huawei-y7', - 'Hunde' => 'hunde', - 'Hundefutter' => 'hundefutter', - 'Hüte & Mützen' => 'huete-muetzen', - 'Hyrule Warriors' => 'hyrule-warriors', - 'Hyrule Warriors: Zeit der Verheerung' => 'hyrule-warriors-zeit-der-verheerung', - 'Hyundai' => 'hyundai', - 'iMac' => 'imac', - 'Immortals Fenyx Rising' => 'immortals-fenyx-rising', - 'In-Ear Kopfhörer' => 'in-ear-kopfhoerer', - 'Industrial Style' => 'industrial-style', - 'Inline Skates' => 'inline-skates', - 'Instax Mini' => 'instax-mini', - 'Intel Core i9-9900K' => 'intel-core-i9-9900k', - 'Intel i3' => 'intel-i3', - 'Intel i5' => 'intel-i5', - 'Intel i7' => 'intel-i7', - 'Intel i9' => 'intel-i9', - 'Intenso' => 'intenso', - 'Internet Security' => 'internet-security', - 'Intimpflege' => 'intimpflege', - 'iOS Apps' => 'ios-apps', - 'iPad' => 'ipad', - 'iPad 2019' => 'ipad-2019', - 'iPad 2020' => 'ipad-2020', - 'iPad Air' => 'ipad-air-2', - 'iPad Air 2019' => 'ipad-air-2019', - 'iPad Air 2020' => 'ipad-air-2020', - 'iPad mini' => 'ipad-mini', - 'iPad Pro' => 'ipad-pro', - 'iPad Pro 11' => 'ipad-pro-11', - 'iPad Pro 12.9' => 'ipad-pro-12-9', - 'iPad Pro 2020' => 'ipad-pro-2020', - 'iPhone' => 'iphone', - 'iPhone 6' => 'iphone-6', - 'iPhone 6 Plus' => 'iphone-6-plus', - 'iPhone 6s' => 'iphone-6s', - 'iPhone 6s Plus' => 'iphone-6s-plus', - 'iPhone 7' => 'iphone-7', - 'iPhone 7 Plus' => 'iphone-7-plus', - 'iPhone 8' => 'iphone-8', - 'iPhone 8 Plus' => 'iphone-8-plus', - 'iPhone 11' => 'iphone-11', - 'iPhone 11 Pro' => 'iphone-11-pro', - 'iPhone 11 Pro Max' => 'iphone-11-pro-max', - 'iPhone 12' => 'iphone-12', - 'iPhone 12 mini' => 'iphone-12-mini', - 'iPhone 12 Pro' => 'iphone-12-pro', - 'iPhone 12 Pro Max' => 'iphone-12-pro-max', - 'iPhone SE' => 'iphone-se', - 'iPhone X' => 'iphone-x', - 'iPhone Xr' => 'iphone-xr', - 'iPhone Xs' => 'iphone-xs', - 'iPhone Xs Max' => 'iphone-xs-max', - 'iPhone Zubehör' => 'iphone-zubehoer', - 'Irish Whiskey' => 'irish-whiskey', - 'iRobot' => 'irobot', - 'iRobot Roomba' => 'irobot-roomba', - 'iRobot Roomba 980' => 'irobot-roomba-980', - 'iRobot Roomba i7' => 'irobot-roomba-i7', - 'Isomatten' => 'isomatten', - 'iTunes Guthaben' => 'itunes-guthaben', - 'Jabra Elite 75t' => 'jabra-elite-75t', - 'Jabra Elite 85h' => 'jabra-elite-85h', - 'Jabra Elite 85t' => 'jabra-elite-85t', - 'Jabra Elite Active 75t' => 'jabra-elite-active-75t', - 'Jabra Kopfhörer' => 'jabra-kopfhoerer', - 'JACK & JONES' => 'jack-jones', - 'Jacken' => 'jacken', - 'JACK WOLFSKIN' => 'jack-wolfskin', - 'Jagdzubehör' => 'jagdzubehoer', - 'JBL' => 'jbl', - 'JBL Charge 4' => 'jbl-charge-4', - 'JBL Flip' => 'jbl-flip', - 'JBL GO' => 'jbl-go', - 'Jeans' => 'jeans', - 'Jim Beam' => 'jim-beam', - 'Jogginghosen' => 'jogginghosen', - 'Joghurt' => 'joghurt', - 'Johnnie Walker' => 'johnnie-walker', - 'Jura Kaffeemaschinen' => 'jura', - 'Just Cause' => 'just-cause', - 'Just Cause 4' => 'just-cause-4', - 'Kaffee' => 'kaffee', - 'Kaffeekapseln' => 'kaffeekapseln', - 'Kaffeemaschinen' => 'kaffeemaschinen', - 'Kaffeemühlen' => 'kaffeemuehlen', - 'Kaffeepadmaschinen' => 'kaffeepadmaschinen', - 'Kaffeepads' => 'kaffeepads', - 'Kaffeevollautomaten' => 'kaffeevollautomaten', - 'Kameras' => 'kamera', - 'Kamera Zubehör' => 'kamerazubehoer', - 'Kamine' => 'kamine', - 'Kapselmaschinen' => 'kapselmaschinen', - 'Kärcher' => 'kaercher', - 'Kärcher Fenstersauger' => 'kaercher-fenstersauger', - 'Kärcher Hochdruckreiniger' => 'kaercher-hochdruckreiniger', - 'Kartenspiele' => 'kartenspiel', - 'Käse' => 'kaese', - 'Katzen' => 'katzen', - 'Katzenfutter' => 'katzenfutter', - 'Kaufen im Ausland' => 'kaufen-ausland', - 'Ketchup' => 'ketchup', - 'KFZ Versicherung' => 'kfz-versicherung', - 'KIA' => 'kia', - 'kiddy' => 'kiddy', - 'Kinder Adventskalender' => 'kinder-adventskalender', - 'Kinderbekleidung' => 'kinderkleidung', - 'Kinderbetten' => 'kinderbett', - 'Kinderfahrräder' => 'kinderfahrrad', - 'Kinderschuhe' => 'kinderschuhe', - 'Kindersitz' => 'kindersitz', - 'Kinderwagen' => 'kinderwagen', - 'Kinderwagen & Autositze' => 'baby-transport', - 'Kinderzimmermöbel' => 'kinderzimmer', - 'Kindle' => 'kindle', - 'Kindle Oasis' => 'kindle-oasis', - 'Kindle Paperwhite' => 'kindle-paperwhite', - 'Kingdom Come: Deliverance' => 'kingdom-come-deliverance', - 'Kingdom Hearts' => 'kingdom-hearts', - 'Kingdom Hearts 3' => 'kingdom-hearts-3', - 'Kingston HyperX Cloud Flight' => 'kingston-hyperx-cloud-flight', - 'Kingston HyperX Cloud II' => 'hyperx-cloud-ii', - 'Kino' => 'kino', - 'KitchenAid' => 'kitchenaid', - 'Kleider' => 'kleider', - 'Kleiderschränke' => 'kleiderschraenke', - 'Kleidung' => 'kleidung', - 'Klemmbausteine' => 'klemmbausteine', - 'Klimaanlagen' => 'klimaanlagen', - 'Klimatechnik' => 'klimatechnik', - 'Klipsch' => 'klipsch', - 'Kochgeräte' => 'kochgeraete', - 'Kodak' => 'kodak', - 'Koffer' => 'koffer', - 'Kohlenmonoxidmelder' => 'kohlenmonoxidmelder', - 'Kolonialstil' => 'kolonialstil', - 'Kommoden & Sideboards' => 'kommoden-sideboards', - 'Kondome' => 'kondome', - 'König der Löwen Musical' => 'koenig-der-loewen-musical', - 'Kontaktgrills' => 'kontaktgrill', - 'Konto & Kreditkarten' => 'konto-kreditkarten', - 'Konzert-Tickets' => 'konzerte', - 'Kopfhörer' => 'kopfhoerer', - 'Körperpflege & Hygiene' => 'koerperpflege', - 'Kosmetik' => 'kosmetik', - 'Kostüme' => 'kostuem', - 'Kraftstoffe & Betriebsstoffe' => 'kraftstoffe-betriebsstoffe', - 'Krafttraining' => 'krafttraining', - 'Kredit' => 'kredit', - 'Kreditkarten' => 'kreditkarten', - 'Kreissägen' => 'kreissaegen', - 'Kreuzfahrten' => 'kreuzfahrten', - 'Krups' => 'krups', - 'Küche' => 'kueche', - 'Küchengeräte' => 'kuechengeraete', - 'Küchenhelfer' => 'kuechenhelfer', - 'Küchenmaschinen' => 'kuechenmaschinen', - 'Küchenmesser' => 'messer', - 'Küchenutensilien' => 'kuechenutensilien', - 'Kugelschreiber' => 'kugelschreiber', - 'Kühl-Gefrierkombinationen' => 'kuehl-gefrierkombination', - 'Kühlboxen' => 'kuehlboxen', - 'Kühlschränke' => 'kuehlschrank', - 'Kultur & Freizeit' => 'kultur-freizeit', - 'Kunst & Hobby' => 'hobby', - 'Kurse & Trainings' => 'kurse-trainings', - 'Lacoste' => 'lacoste', - 'Ladegeräte' => 'ladegeraete', - 'Lampen' => 'lampen', - 'Landhausstil' => 'landhausstil', - 'Landwirtschafts-Simulator' => 'landwirtschafts-simulator', - 'Laptops' => 'laptop', - 'Laserdrucker' => 'laserdrucker', - 'Last Minute Reisen' => 'last-minute', - 'Lattenroste' => 'lattenroste', - 'Laubsauger' => 'laubsauger', - 'Laufräder' => 'laufraeder', - 'Laufschuhe' => 'laufschuhe', - 'Laufsport' => 'laufsport', - 'Lautsprecher' => 'lautsprecher', - 'Lavazza' => 'lavazza', - 'Lay-Z-Spa Whirlpools' => 'lay-z-spa-whirlpools', - 'Lebensmittel' => 'lebensmittel', - 'Lebensmittel & Haushalt' => 'food', - 'LED Lampen' => 'led-lampen', - 'LEGO' => 'lego', - 'LEGO Adventskalender' => 'lego-adventskalender', - 'LEGO Architecture' => 'lego-architecture', - 'LEGO Batman' => 'lego-batman', - 'LEGO City' => 'lego-city', - 'LEGO Creator' => 'lego-creator', - 'LEGO Dimensions' => 'lego-dimensions', - 'LEGO DUPLO' => 'lego-duplo', - 'LEGO Friends' => 'lego-friends', - 'LEGO Harry Potter' => 'lego-harry-potter', - 'LEGO Marvel Super Heroes' => 'lego-marvel-super-heroes', - 'LEGO Nexo Knights' => 'lego-nexo-knights', - 'LEGO NINJAGO' => 'lego-ninjago', - 'LEGO Star Wars' => 'lego-star-wars', - 'LEGO Star Wars Millennium Falcon' => 'lego-star-wars-millennium-falcon', - 'LEGO Super Mario' => 'lego-super-mario', - 'LEGO Technic' => 'lego-technic', - 'LEGO The Simpsons' => 'lego-simpsons', - 'Leifheit' => 'leifheit', - 'Lenovo' => 'lenovo', - 'Lenovo Laptops' => 'lenovo-laptop', - 'Lenovo Tablets' => 'lenovo-tablet', - 'Lenovo ThinkPad' => 'lenovo-thinkpad', - 'Lenovo Yoga' => 'lenovo-yoga', - 'Leonardo' => 'leonardo', - 'Leuchtmittel' => 'leuchten', - 'Levi's' => 'levis', - 'Lexar' => 'lexar', - 'Lexmark' => 'lexmark', - 'LG' => 'lg', - 'LG Fernseher' => 'lg-fernsher', - 'LG G5' => 'lg-g5', - 'LG G6' => 'lg-g6', - 'LG G7 ThinQ' => 'lg-g7-thinq', - 'LG OLED Fernseher' => 'lg-oled-tv', - 'LG Smartphones' => 'lg-smartphones', - 'LG V30' => 'lg-v30', - 'Lichterketten' => 'lichterketten', - 'Liebeskind' => 'liebeskind', - 'Lieferservice' => 'lieferservice', - 'Lindt' => 'lindt', - 'Lindt Adventskalender' => 'lindt-adventskalender', - 'Logitech' => 'logitech', - 'Logitech G413' => 'logitech-g413', - 'Logitech G430' => 'logitech-g430', - 'Logitech G502 Proteus Spectrum' => 'logitech-g502', - 'Logitech G513' => 'logitech-g513', - 'Logitech G533' => 'logitech-g533', - 'Logitech G633 Artemis Spectrum' => 'logitech-g633', - 'Logitech G703' => 'logitech-g703', - 'Logitech G903' => 'logitech-g903', - 'Logitech G910 Orion Spectrum' => 'logitech-g910', - 'Logitech G915' => 'logitech-g915', - 'Logitech G933 Artemis Spectrum' => 'logitech-g933', - 'Logitech Harmony' => 'logitech-harmony', - 'Logitech Mäuse' => 'logitech-maeuse', - 'Logitech MX Master' => 'logitech-mx-master', - 'Logitech MX Master 2S' => 'logitech-mx-master-2s', - 'Logitech Tastaturen' => 'logitech-tastaturen', - 'Logitech Z333' => 'logitech-z333', - 'Logitech Z337' => 'logitech-z337', - 'Logitech Z906' => 'logitech-z906', - 'Luftbefeuchter' => 'luftbefeuchter', - 'Luftentfeuchter' => 'luftentfeuchter', - 'Luftmatratzen' => 'luftmatratzen', - 'Luftreiniger' => 'luftreiniger', - 'Luigi's Mansion' => 'luigis-mansion', - 'Luigi's Mansion 3' => 'luigis-mansion-3', - 'Lustiges Taschenbuch' => 'lustiges-taschenbuch', - 'M.2 SSD' => 'm2-ssd', - 'MacBook' => 'macbook', - 'MacBook Air' => 'macbook-air', - 'MacBook Pro' => 'macbook-pro', - 'MacBook Pro 13' => 'macbook-pro-13', - 'MacBook Pro 15' => 'macbook-pro-15', - 'MacBook Pro 16' => 'macbook-pro-16', - 'Mac mini' => 'mac-mini', - 'Mac Software' => 'mac-software', - 'Madden NFL' => 'madden-nfl', - 'Magazine' => 'magazine', - 'Magnat' => 'magnat', - 'Magnum Eis' => 'magnum-eis', - 'Mähroboter' => 'maehroboter', - 'Mainboards' => 'mainboards', - 'Make Up Adventskalender' => 'make-up-adventskalender', - 'Makita' => 'makita', - 'Makita Akkuschrauber' => 'makita-akkuschrauber', - 'Malerwerkzeuge' => 'malerpinsel', - 'Mangas' => 'mangas', - 'Marantz' => 'marantz', - 'Mario Kart' => 'mario-kart', - 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe', - 'Marken' => 'marken', - 'Marvel' => 'marvel', - 'Marvel's Spider-Man: Miles Morales' => 'marvels-spider-man-miles-morales', - 'Mass Effect' => 'mass-effect', - 'Mass Effect: Andromeda' => 'mass-effect-andromeda', - 'Massivholzmöbel' => 'massivholzmoebel', - 'Mastercard' => 'mastercard', - 'Matratzen' => 'matratzen', - 'Maxi Cosi' => 'maxi-cosi', - 'Mazda' => 'mazda', - 'Medion' => 'medion', - 'Mercedes-Benz' => 'mercedes-benz', - 'Mesh WLAN Router' => 'mesh-wlan-router', - 'Metabo' => 'metabo', - 'Metro (Spiel)' => 'metro', - 'Metro Exodus' => 'metro-exodus', - 'Michael Kors' => 'michael-kors', - 'microSD' => 'microsd', - 'microSDHC' => 'microsdhc', - 'microSDXC' => 'microsdxc', - 'Microsoft Flight Simulator' => 'microsoft-flight-simulator', - 'Microsoft Software' => 'microsoft-software', - 'Microsoft Surface Notebooks' => 'microsoft-surface-notebooks', - 'Microsoft Surface Pro 4' => 'surface-pro-4', - 'Microsoft Surface Pro 6' => 'surface-pro-6', - 'Microsoft Surface Pro 7' => 'microsoft-surface-pro-7', - 'Microsoft Surface Tablets' => 'microsoft-surface', - 'Miele' => 'miele', - 'Miele Geschirrspüler' => 'miele-geschirrspueler', - 'Miele Staubsauger' => 'miele-staubsauger', - 'Miele Waschmaschinen' => 'miele-waschmaschine', - 'Mietwagen' => 'mietwagen', - 'Mikrofone' => 'mikrofone', - 'Mikrowellen' => 'mikrowelle', - 'Milchaufschäumer' => 'milchaufschaeumer', - 'Milka' => 'milka', - 'Minecraft' => 'minecraft', - 'Mineralwasser' => 'mineralwasser', - 'Minions' => 'minions', - 'Mini PCs' => 'mini-pc', - 'Mitsubishi' => 'mitsubishi', - 'Mittelerde' => 'middle-earth', - 'Mittelerde: Mordors Schatten' => 'mittelerde-mordors-schatten', - 'Mittelerde: Schatten des Krieges' => 'mittelerde-schatten-des-krieges', - 'Mixer & Rührer' => 'mixer', - 'Möbel' => 'moebel-deko', - 'Modellbau' => 'modellbau', - 'Modern wohnen' => 'modern-wohnen', - 'Monitore' => 'monitor', - 'Monkey 47' => 'monkey-47', - 'Monopoly' => 'monopoly', - 'Monster Hunter' => 'monster-hunter', - 'Monster Hunter: World' => 'monster-hunter-world', - 'Mortal Kombat' => 'mortal-kombat', - 'Mortal Kombat 11' => 'mortal-kombat-11', - 'Motorola' => 'motorola', - 'Motorola Smartphones' => 'motorola-smartphones', - 'Motorradbekleidung' => 'motorradbekleidung', - 'Motorradhelm' => 'motorradhelm', - 'Motorrad Zubehör' => 'motorrad', - 'Moto Z' => 'moto-z', - 'Mountainbikes' => 'mountainbikes', - 'MSI' => 'msi', - 'Mülleimer' => 'muelleimer', - 'Multifunktionsdrucker' => 'multifunktionsdrucker', - 'Multiroom Speaker' => 'multiroom', - 'Mund- & Zahnpflege' => 'mund-zahnpflege', - 'Mundschutzmasken' => 'mundschutzmasken', - 'Museums-Tickets' => 'museum', - 'Musical Tickets' => 'musical', - 'Musik' => 'musik', - 'Musik Apps' => 'musik-apps', - 'Musikinstrumente' => 'musikinstrumente', - 'Musik Streaming' => 'musik-streaming', - 'Müsli' => 'muesli', - 'Mustang' => 'mustang', - 'Mützen' => 'muetzen', - 'Nachtwäsche' => 'nachtwaesche', - 'Nähbedarf' => 'naehen', - 'Nähmaschinen' => 'naehmaschine', - 'Nahrungsergänzungsmittel' => 'nahrungsergaenzungsmittel', - 'Nahverkehr' => 'nahverkehr', - 'Naketano' => 'naketano', - 'NAS' => 'nas', - 'Nassrasierer' => 'rasierer', - 'Navigationsgeräte' => 'navigationsgeraete', - 'Neato' => 'neato', - 'Neato Robotics Botvac D7 Connected' => 'neato-botvac-d7', - 'Need for Speed' => 'need-for-speed', - 'Need for Speed Heat' => 'need-for-speed-heat', - 'Need for Speed Payback' => 'need-for-speed-payback', - 'Nerf' => 'nerf', - 'Nescafé' => 'nescafe', - 'Nespresso' => 'nespresso', - 'Nespresso Kaffeemaschinen' => 'nespresso-kaffeemaschinen', - 'Netflix' => 'netflix', - 'NETGEAR' => 'netgear', - 'NETGEAR Nighthawk' => 'netgear-nighthawk', - 'NETGEAR Orbi' => 'netgear-orbi', - 'NETGEAR Router' => 'netgear-router', - 'Netzteile' => 'netzteile', - 'Netzwerk' => 'netzwerk', - 'New Balance' => 'new-balance', - 'Nike' => 'nike', - 'Nike Air Force 1' => 'nike-air-force', - 'Nike Air Max' => 'nike-air-max', - 'Nike Air Max 270' => 'nike-air-max-270', - 'Nike Air Max 720' => 'nike-air-max-720', - 'Nike Air Max Thea' => 'nike-air-max-thea', - 'Nike Air Presto' => 'nike-presto', - 'Nike Free' => 'nike-free', - 'Nike Huarache' => 'nike-huarache', - 'Nike Roshe Run' => 'nike-roshe-run', - 'Nike Schuhe' => 'nike-schuhe', - 'Nikon' => 'nikon', - 'Nikon DSLR' => 'nikon-dslr', - 'Ni No Kuni' => 'ni-no-kuni', - 'Ni No Kuni: Der Fluch der Weißen Königin' => 'ni-no-kuni-der-fluch-der-weissen-koenigin', - 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-ii', - 'Nintendo' => 'nintendo', - 'Nintendo 2DS Konsolen' => 'nintendo-2ds', - 'Nintendo 3DS Konsolen' => 'nintendo-3ds', - 'Nintendo 3DS Spiele' => 'nintendo-3ds-spiele', - 'Nintendo 3DS Zubehör' => 'nintendo-3ds-zubehoer', - 'Nintendo Classic Mini NES Konsolen' => 'nintendo-classic-mini-nes', - 'Nintendo Classic Mini SNES Konsolen' => 'nintendo-classic-mini-snes', - 'Nintendo eShop Guthaben' => 'nintendo-eshop-guthaben', - 'Nintendo Switch Controller' => 'nintendo-switch-controller', - 'Nintendo Switch Konsolen' => 'nintendo-switch', - 'Nintendo Switch Lite Konsolen' => 'nintendo-switch-lite', - 'Nintendo Switch Pro Controller' => 'nintendo-switch-pro-controller', - 'Nintendo Switch Spiele' => 'nintendo-switch-spiele', - 'Nintendo Switch Zubehör' => 'nintendo-switch-zubehoer', - 'Nintendo Zubehör' => 'nintendo-zubehoer', - 'Nissan' => 'nissan', - 'Nivea' => 'nivea', - 'Nokia' => 'nokia', - 'Nokia Handys' => 'nokia-handys', - 'Nudeln' => 'nudeln', - 'Nuki Smart Locks' => 'nuki-smart-lock', - 'Nüsse' => 'nuesse', - 'Nutella' => 'nutella', - 'Nvidia' => 'nvidia', - 'Nvidia GeForce' => 'nvidia-geforce', - 'Nvidia SHIELD TV' => 'nvidia-shield', - 'o2' => 'o2-netz', - 'Objektive' => 'objektiv', - 'Obst' => 'obst', - 'Obst & Gemüse' => 'obst-gemuese', - 'Oculus Quest' => 'oculus-quest', - 'Oculus Rift' => 'oculus-rift', - 'Office Programme' => 'office-programme', - 'OLED Fernseher' => 'oled-fernseher', - 'Olympus' => 'olympus', - 'On-Ear Kopfhörer' => 'on-ear-kopfhoerer', - 'OnePlus 3' => 'oneplus-3', - 'OnePlus 5' => 'oneplus-5', - 'OnePlus 6' => 'oneplus-6', - 'OnePlus 7' => 'oneplus-7', - 'OnePlus 7 Pro' => 'oneplus-7-pro', - 'OnePlus 7T' => 'oneplus-7t', - 'OnePlus 7T Pro' => 'oneplus-7t-pro', - 'OnePlus 8' => 'oneplus-8', - 'OnePlus 8 Pro' => 'one-plus-8-pro', - 'OnePlus 8T' => 'oneplus-8t', - 'OnePlus Nord' => 'oneplus-nord', - 'OnePlus Smartphones' => 'oneplus-smartphones', - 'Onkyo' => 'onkyo', - 'Opel' => 'opel', - 'OPPO Find X2 Lite' => 'oppo-find-x2-lite', - 'OPPO Find X2 Neo' => 'oppo-find-x2-neo', - 'OPPO Find X2 Pro' => 'oppo-find-x2-pro', - 'OPPO Reno2' => 'oppo-reno2', - 'OPPO Reno2 Z' => 'oppo-reno2-z', - 'OPPO Reno4 5G' => 'oppo-reno4-5g', - 'OPPO Reno4 Pro 5G' => 'oppo-reno4-pro-5g', - 'OPPO Reno4 Z 5G' => 'oppo-reno4-z-5g', - 'OPPO Smartphones' => 'oppo-smartphones', - 'Oral-B' => 'oral-b', - 'Oral-B Elektrische Zahnbürsten' => 'oral-b-elektrische-zahnbuersten', - 'Origin' => 'origin', - 'Osram' => 'osram', - 'Osram Smart+' => 'osram-smart-plus', - 'Osterdeko' => 'osterdeko', - 'Outdoor & Camping' => 'outdoor', - 'Outdoorbekleidung' => 'outdoorbekleidung', - 'Outdoorjacken' => 'outdoorjacken', - 'Outdoor Spielzeuge' => 'outdoor-spielzeug', - 'Over-Ear Kopfhörer' => 'over-ear-kopfhoerer', - 'Pampers' => 'pampers', - 'Panama Jack' => 'panama-jack', - 'Panasonic' => 'panasonic', - 'Panasonic Fernseher' => 'panasonic-fernseher', - 'Panasonic Kameras' => 'panasonic-kameras', - 'Panasonic Lumix' => 'panasonic-lumix', - 'Paper Mario: The Origami King' => 'paper-mario-the-origami-king', - 'Papiertapete' => 'papiertapete', - 'Parfum' => 'parfum', - 'Parfum Damen' => 'parfum-damen', - 'Parfum Herren' => 'parfum-herren', - 'Pauschalreisen' => 'pauschalreise', - 'Pavillons' => 'pavillons', - 'Paw Patrol' => 'paw-patrol', - 'PAYBACK' => 'payback', - 'Payday' => 'payday', - 'Payday 2' => 'payday-2', - 'paydirekt' => 'paydirekt', - 'PC Gaming Systeme' => 'pc-gaming-systeme', - 'PC Gaming Zubehör' => 'pc-gaming-zubehoer', - 'PC Gehäuse' => 'pc-gehaeuse', - 'PC Komponenten' => 'hardware', - 'PC Lautsprecher' => 'pc-lautsprecher', - 'PC Mäuse' => 'pc-maus', - 'PC Spiele' => 'pc-spiele', - 'PC Zubehör' => 'pc-zubehoer', - 'Pendelleuchten' => 'pendelleuchten', - 'Pentax' => 'pentax', - 'Pepe Jeans' => 'pepe-jeans', - 'Peppa Wutz' => 'peppa-wutz', - 'PepperBonus' => 'pepperbonus', - 'Pestos' => 'pestos', - 'Peugeot' => 'peugeot', - 'Pfannen' => 'pfannen', - 'Pflanzen' => 'pflanzen', - 'Philips' => 'philips', - 'Philips Fernseher' => 'philips-fernseher', - 'Philips Hue' => 'philips-hue', - 'Philips Hue E14' => 'philips-hue-e14', - 'Philips Hue E27' => 'philips-hue-e27', - 'Philips Hue Go' => 'philips-hue-go', - 'Philips Hue GU10' => 'philips-hue-gu10', - 'Philips Hue LightStrip' => 'philips-hue-lightstrip', - 'Philips Hue Play Gradient LightStrip' => 'philips-hue-play-gradient-lightstrip', - 'Philips Hue Play HDMI Sync Box' => 'philips-hue-play-hdmi-sync-box', - 'Philips Hue Play Lightbar' => 'philips-hue-play', - 'Philips OneBlade' => 'philips-oneblade', - 'Philips Rasierer' => 'philips-rasierer', - 'Philips Sonicare' => 'philips-sonicare', - 'Philips Staubsauger' => 'philips-staubsauger', - 'Philips Wecker' => 'philips-wecker', - 'Photoshop' => 'photoshop', - 'Pioneer' => 'pioneer', - 'Pizza' => 'pizza', - 'Plattenspieler' => 'plattenspieler', - 'Playboy' => 'playboy', - 'Playerunknown's Battlegrounds' => 'playerunknowns-battlegrounds', - 'PLAYMOBIL' => 'playmobil', - 'PLAYMOBIL Adventskalender' => 'playmobil-adventskalender', - 'PlayStation' => 'playstation', - 'PlayStation 4 Controller' => 'playstation-4-controller', - 'PlayStation 4 Konsolen' => 'playstation-4', - 'PlayStation 4 Pro Konsolen' => 'playstation-4-pro', - 'PlayStation 4 Spiele' => 'playstation-4-spiele', - 'PlayStation 5 Konsolen' => 'playstation-5', - 'PlayStation 5 Spiele' => 'playstation-5-spiele', - 'PlayStation Classic Konsolen' => 'playstation-classic', - 'PlayStation Now' => 'playstation-now', - 'PlayStation Plus' => 'playstation-plus', - 'PlayStation Zubehör' => 'playstation-zubehoer', - 'Plüschtiere' => 'plueschtiere', - 'Plus Size Mode' => 'plus-size-mode', - 'POCO F2 Pro' => 'poco-f2-pro', - 'POCO X3' => 'poco-x3', - 'Pokémon' => 'pokemon', - 'Pokémon: Let's Go' => 'pokemon-lets-go', - 'Pokémon Schwert und Schild' => 'pokemon-schwert-schild', - 'Pokémon Tekken' => 'pokemon-tekken', - 'Pokémon Ultrasonne & Ultramond' => 'pokemon-ultrasonne-ultramond', - 'Poloshirts' => 'poloshirts', - 'Polsterbetten' => 'polsterbetten', - 'Polyrattan Möbel' => 'polyrattan', - 'Pools' => 'pools', - 'Powerbanks' => 'powerbanks', - 'Powerbeats Pro' => 'powerbeats', - 'Preisfehler' => 'preisfehler', - 'Prepaid-Tarife' => 'prepaid-tarife', - 'Prime Gaming' => 'twitch-prime', - 'Pro Evolution Soccer' => 'pro-evolution-soccer', - 'Pro Evolution Soccer 2018' => 'pes-2018', - 'Pro Evolution Soccer 2019' => 'pes-2019', - 'Pro Evolution Soccer 2020' => 'pes-2020', - 'Proteine' => 'whey-proteine', - 'Prozessoren' => 'prozessoren', - 'PSN Guthaben' => 'psn-guthaben', - 'Puky' => 'puky', - 'Pullover' => 'pullover', - 'PUMA' => 'puma', - 'Pumps' => 'pumps', - 'Puppen' => 'puppen', - 'Puppenhäuser' => 'puppenhaeuser', - 'Puzzles' => 'puzzle', - 'Qeridoo' => 'qeridoo', - 'Qeridoo Fahrradanhänger' => 'qeridoo-fahrradanhaenger', - 'Qeridoo KidGoo 2' => 'qeridoo-kidgoo-2', - 'Qeridoo Sportrex 2' => 'qeridoo-sportrex-2', - 'Quiksilver' => 'quiksilver', - 'Raclettes' => 'raclettes', - 'Radios' => 'radios', - 'Radsport' => 'radsport', - 'Rasenmäher' => 'rasenmaeher', - 'Rasentrimmer' => 'rasentrimmer', - 'Rasierklingen' => 'rasierklingen', - 'Raspberry Pi' => 'raspberry-pi', - 'Rasur, Enthaarung & Trimmen' => 'rasur-enthaarung', - 'Rauchmelder' => 'rauchmelder', - 'Ravensburger' => 'ravensburger', - 'Ray-Ban' => 'ray-ban', - 'Razer DeathAdder' => 'razer-deathadder', - 'RC Autos' => 'rc-autos', - 'Red Bull' => 'red-bull', - 'Red Dead Redemption' => 'red-dead-redemption', - 'Red Dead Redemption 2' => 'red-dead-redemption-2', - 'Reebok' => 'reebok', - 'Regale' => 'regale', - 'Reifen' => 'reifen', - 'Reinigungsmittel' => 'reinigungsmittel', - 'Reise Apps' => 'reise-apps', - 'Reisen' => 'reisen', - 'Reiskocher' => 'reiskocher', - 'Remington' => 'remington', - 'Renault' => 'renault', - 'Rennräder' => 'rennraeder', - 'Repeater' => 'repeater', - 'Resident Evil' => 'resident-evil', - 'Resident Evil 2' => 'resident-evil-2', - 'Resident Evil 7' => 'resident-evil-7', - 'Restaurant' => 'restaurant', - 'Retro Stil' => 'retro-stil', - 'Rimowa' => 'rimowa', - 'Ring Fit Adventure' => 'ring-fit-adventure', - 'Rituals' => 'rituals', - 'Rituals Adventskalender' => 'rituals-adventskalender', - 'Roborock' => 'xiaomi-roborock', - 'Roborock S5 Max' => 'roborock-s5-max', - 'Roborock S6' => 'roborock-s6', - 'Roborock S6 MaxV' => 'roborock-s6-maxv', - 'ROCCAT' => 'roccat', - 'ROCCAT Tyon' => 'roccat-tyon', - 'Röcke' => 'roecke', - 'Rocket League' => 'rocket-league', - 'Roidmi Staubsauger' => 'roidmi-staubsauger', - 'Rollei' => 'rollei', - 'Rösle' => 'roesle', - 'Router' => 'router', - 'Roxy' => 'roxy', - 'RTX 2060' => 'rtx-2060', - 'RTX 2070' => 'rtx-2070', - 'RTX 2080' => 'rtx-2080', - 'RTX 2080 Ti' => 'rtx-2080-ti', - 'RTX 3070' => 'rtx-3070', - 'RTX 3080' => 'rtx-3080', - 'RTX 3090' => 'rtx-3090', - 'Rucksäcke' => 'rucksaecke', - 'Russell Hobbs' => 'russell-hobbs', - 'RX 480' => 'rx-480', - 'RX 570' => 'rx-570', - 'RX 580' => 'rx-580', - 'RX 590' => 'rx-590', - 'RX 5700 XT' => 'rx-5700-xt', - 'RX 6800' => 'rx-6800', - 'RX 6800 XT' => 'rx-6800-xt', - 'RX 6900 XT' => 'rx-6900-xt', - 'RX Vega 56' => 'rx-vega-56', - 'RX Vega 64' => 'rx-vega-64', - 'Sägen' => 'saegen', - 'Salomon' => 'salomon', - 'Samsonite' => 'samsonite', - 'Samsung' => 'samsung', - 'Samsung Fernseher' => 'samsung-fernseher', - 'Samsung Galaxy A7' => 'samsung-galaxy-a7', - 'Samsung Galaxy A8' => 'samsung-galaxy-a8', - 'Samsung Galaxy A51' => 'samsung-galaxy-a51', - 'Samsung Galaxy A71' => 'samsung-galaxy-a71', - 'Samsung Galaxy Buds' => 'samsung-galaxy-buds', - 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus', - 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live', - 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro', - 'Samsung Galaxy Note9' => 'samsung-galaxy-note-9', - 'Samsung Galaxy Note20' => 'samsung-galaxy-note20', - 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note20-ultra', - 'Samsung Galaxy S7' => 'samsung-galaxy-s7', - 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge', - 'Samsung Galaxy S8' => 'samsung-galaxy-s8', - 'Samsung Galaxy S8+' => 'samsung-galaxy-s8-plus', - 'Samsung Galaxy S9' => 'samsung-galaxy-s9', - 'Samsung Galaxy S9+' => 'samsung-galaxy-s9-plus', - 'Samsung Galaxy S10' => 'samsung-galaxy-s10', - 'Samsung Galaxy S10+' => 'samsung-galaxy-s10-plus', - 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e', - 'Samsung Galaxy S20' => 'samsung-galaxy-s20', - 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe', - 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra', - 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus', - 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g', - 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g', - 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g', - 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4', - 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6', - 'Samsung Galaxy Watch' => 'samsung-galaxy-watch', - 'Samsung Galaxy Watch Active2' => 'samsung-galaxy-watch-active-2', - 'Samsung Gear' => 'samsung-gear', - 'Samsung Gear S3' => 'samsung-gear-s3', - 'Samsung Gear VR' => 'samsung-gear-vr', - 'Samsung Kopfhörer' => 'samsung-kopfhoerer', - 'Samsung Kühlschränke' => 'samsung-kuehlschrank', - 'Samsung Monitore' => 'samsung-monitor', - 'Samsung QLED Fernseher' => 'samsung-qled-fernseher', - 'Samsung Smartphones' => 'samsung-smartphone', - 'Samsung SSD' => 'samsung-ssd', - 'Samsung Tablets' => 'samsung-tablet', - 'Samsung The Frame Fernseher' => 'samsung-the-frame-fernseher', - 'Samsung Waschmaschinen' => 'samsung-waschmaschine', - 'Sandalen' => 'sandalen', - 'SanDisk' => 'sandisk', - 'SanDisk SSD' => 'sandisk-ssd', - 'Sanitär & Armaturen' => 'sanitaer-armaturen', - 'Saucen' => 'saucen', - 'Saugroboter' => 'saugroboter', - 'Scanner' => 'scanner', - 'Schallplatten' => 'schallplatten', - 'Scheppach' => 'scheppach', - 'Schlafsäcke' => 'schlafsack', - 'Schlafsofas' => 'schlafsofas', - 'Schlafzimmer' => 'schlafzimmer', - 'Schlagschrauber' => 'schlagschrauber', - 'Schlauchboote' => 'schlauchboote', - 'Schleich' => 'schleich', - 'Schlitten' => 'schlitten', - 'Schmuck' => 'schmuck', - 'Schneefräsen' => 'schneefraesen', - 'Schnellkochtöpfe' => 'schnellkochtoepfe', - 'Schnürhalbschuhe' => 'schnuerhalbschuhe', - 'Schokolade' => 'schokolade', - 'Schraubendreher' => 'schraubendreher', - 'Schreibgeräte' => 'schreibgeraete', - 'Schreibtische' => 'schreibtisch', - 'Schuhe' => 'schuhe', - 'Schuhschränke' => 'schuhschraenke', - 'Schulbedarf' => 'schulbedarf', - 'Schulranzen' => 'schulranzen', - 'Schutzfolien' => 'schutzfolien', - 'Schwangerschaft' => 'schwangerschaft', - 'Schwerlastregale' => 'schwerlastregale', - 'Scooter' => 'scooter', - 'Scotch Whisky' => 'scotch-whisky', - 'SDHC Speicherkarten' => 'sdhc-speicherkarten', - 'SD Karten' => 'sd-karten', - 'Seagate' => 'seagate', - 'Sea of Thieves' => 'sea-of-thieves', - 'Seat' => 'seat', - 'Sega Mega Drive Mini Konsolen' => 'sega-mega-drive-mini', - 'Seidensticker' => 'seidensticker', - 'Sekiro: Shadows Die Twice' => 'sekiro', - 'Senf' => 'senf', - 'Sennheiser' => 'sennheiser', - 'Senseo' => 'senseo', - 'Service-Verträge' => 'service-vertraege', - 'Sessel' => 'sessel', - 'Sextoys' => 'sextoys', - 'Shadow of the Tomb Raider' => 'shadow-of-the-tomb-raider', - 'Shampoo' => 'shampoo', - 'Sharkoon' => 'sharkoon', - 'Sharp' => 'sharp', - 'Shenmue' => 'shenmue', - 'Shenmue I & II' => 'shenmue-i-ii', - 'Shenmue III' => 'shenmue-iii', - 'Shishas' => 'shishas', - 'Shishas & Zubehör' => 'shishas-zubehoer', - 'Shoop' => 'shoop', - 'Shops: Erfahrungen' => 'shops', - 'Shorts' => 'shorts', - 'Sicherheitstechnik' => 'sicherheitstechnik', - 'Side-by-Side-Kühlschränke' => 'side-by-side-kuehlschrank', - 'Sid Meier's Civilization VI' => 'sid-meiers-civilization-vi', - 'Sid Meier’s Civilization' => 'sid-meiers-civilization', - 'Siemens' => 'siemens', - 'Siemens Geschirrspüler' => 'siemens-geschirrspueler', - 'Siemens Kühlschränke' => 'siemens-kuehlschrank', - 'Siemens Waschmaschinen' => 'siemens-waschmaschine', - 'Silit' => 'silit', - 'Skandi Stil' => 'skandi-stil', - 'Skateboards' => 'skateboard', - 'Skaten' => 'skaten', - 'Ski & Snowboard' => 'snowboard', - 'Skoda' => 'skoda', - 'Sky' => 'sky', - 'Sky Ticket' => 'sky-ticket', - 'Smarte Beleuchtung' => 'smarte-beleuchtung', - 'Smarte Wecker' => 'smarte-wecker', - 'Smart Home' => 'smart-home', - 'Smart Home Steckdosen' => 'smart-home-steckdosen', - 'Smart Locks' => 'smart-lock', - 'Smartphones' => 'smartphone', - 'Smartphones unter 200€' => 'smartphones-unter-200-euro', - 'Smart Speaker' => 'smart-speaker', - 'Smart Tech & Gadgets' => 'smart-tech', - 'Smartwatches' => 'smartwatch', - 'Smoothie Maker' => 'smoothie-maker', - 'Snacks & Knabberzeug' => 'snacks-knabberzeug', - 'Sneakers' => 'sneaker', - 'Socken' => 'socken', - 'SodaStream' => 'sodastream', - 'Sofas' => 'sofa', - 'Sofortbildkameras' => 'sofortbildkameras', - 'Softdrinks' => 'softdrinks', - 'Software' => 'software', - 'Software & Apps' => 'apps-software', - 'Solarleuchten' => 'solarleuchten', - 'Somat' => 'somat', - 'Sommerreifen' => 'sommerreifen', - 'Sonnenbrillen' => 'sonnenbrillen', - 'Sonnencreme' => 'sonnencreme', - 'Sonnenpflege' => 'sonnenpflege', - 'Sonnenschirme' => 'sonnenschirme', - 'Sonoff' => 'sonoff', - 'Sonos' => 'sonos', - 'Sonos Beam' => 'sonos-beam', - 'Sonos Move' => 'sonos-move', - 'Sonos One' => 'sonos-one', - 'Sonos PLAY:1' => 'sonos-play-1', - 'Sonos PLAY:3' => 'sonos-play-3', - 'Sonos Play:5 (Five)' => 'sonos-play-5', - 'Sonos Playbar' => 'sonos-playbar', - 'Sonos Playbase' => 'sonos-playbase', - 'Sonstiges' => 'diverses', - 'Sony' => 'sony', - 'Sony Alpha 7' => 'sony-alpha-7', - 'Sony Alpha 7 II' => 'sony-alpha-7-ii', - 'Sony Alpha 7 III' => 'sony-alpha-7-iii', - 'Sony Alpha 6000' => 'sony-alpha-6000', - 'Sony Alpha 6300' => 'sony-alpha-6300', - 'Sony Alpha 6400' => 'sony-alpha-6400', - 'Sony Alpha 6500' => 'sony-alpha-6500', - 'Sony DualSense Wireless-Controller' => 'playstation-5-controller', - 'Sony Fernseher' => 'sony-fernseher', - 'Sony Kameras' => 'sony-kameras', - 'Sony Kopfhörer' => 'sony-kopfhoerer', - 'Sony PlayStation VR' => 'sony-playstation-vr', - 'Sony PULSE 3D Wireless Headset' => 'sony-pulse-3d-wireless-headset', - 'Sony WF-1000XM3' => 'sony-wf-1000xm3', - 'Sony WH-1000XM3' => 'sony-wh-1000xm3', - 'Sony WH-1000XM4' => 'sony-wh-1000xm4', - 'Sony Xperia' => 'sony-xperia', - 'Sony Xperia X' => 'sony-xperia-x', - 'Sony Xperia XA' => 'sony-xperia-xa', - 'Sony Xperia XZ' => 'sony-xperia-xz', - 'Soundbar' => 'soundbar', - 'Soundbase' => 'soundbase', - 'Soundkarten' => 'soundkarten', - 'South Park: Die rektakuläre Zerreißprobe' => 'south-park-die-rektakulaere-zerreissprobe', - 'Spartipps' => 'spartipps', - 'Speicherkarten' => 'speicherkarten', - 'Speichermedien' => 'speichermedien', - 'Speiseöle' => 'speiseoele', - 'Spiegelreflexkameras' => 'spiegelreflexkamera', - 'Spiele & Brettspiele' => 'spiele-brettspiele', - 'Spiele Apps' => 'spiele-apps', - 'Spielekonsolen' => 'spielekonsolen', - 'Spielfiguren & Spielsets' => 'spielfiguren-spielsets', - 'Spielzeuge' => 'spielzeug', - 'Spirituosen' => 'spirituosen', - 'Sport & Outdoor' => 'sport', - 'Sportbekleidung' => 'sportbekleidung', - 'Sport Bild' => 'sport-bild', - 'Sportnahrung' => 'sportlernahrung', - 'Sporttasche' => 'sporttasche', - 'Spotify' => 'spotify', - 'Spülmaschinentabs' => 'spuelmaschinentabs', - 'Spyro Reignited Trilogy' => 'spyro-reignited-trilogy', - 'SSD' => 'ssd', - 'Stabmixer' => 'stabmixer', - 'Städtereisen' => 'staedtereise', - 'Standmixer' => 'standmixer', - 'Star Trek' => 'star-trek', - 'Star Wars' => 'star-wars', - 'Star Wars: Battlefront 2' => 'star-wars-battlefront-2', - 'Star Wars: Squadrons' => 'star-wars-squadrons', - 'Star Wars Battlefront' => 'star-wars-battlefront', - 'Star Wars Jedi: Fallen Order' => 'star-wars-jedi-fallen-order', - 'Staubsauger' => 'staubsauger', - 'Staubsaugerbeutel' => 'staubsaugerbeutel', - 'Staubsauger ohne Beutel' => 'staubsauger-ohne-beutel', - 'Steam' => 'steam', - 'Steckschlüssel' => 'steckschluessel', - 'SteelSeries' => 'steelseries', - 'Stehlampen' => 'stehlampen', - 'Steiff' => 'steiff', - 'Stern (Magazin)' => 'stern-magazin', - 'Stichsägen' => 'stichsaegen', - 'Stiefel' => 'stiefel', - 'Stiefeletten' => 'stiefeletten', - 'Stiftung Warentest' => 'stiftung-warentest-magazin', - 'Streaming-Dienste' => 'streaming-dienste', - 'Streaming Lautsprecher' => 'streaming-lautsprecher', - 'Strom & Gas' => 'strom-gas', - 'Stromtarif' => 'stromtarif', - 'Studentenrabatte' => 'studentenrabatte', - 'Stühle' => 'stuehle', - 'Subwoofer' => 'subwoofer', - 'SUP Boards' => 'sup-boards', - 'Superdry' => 'superdry', - 'Super Mario' => 'super-mario', - 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars', - 'Super Mario Maker 2' => 'super-mario-maker-2', - 'Super Mario Odyssey' => 'super-mario-odyssey', - 'Super Mario Party' => 'super-mario-party', - 'Supermarkt' => 'supermarkt', - 'Super Smash Bros. Ultimate' => 'super-smash-bros-ultimate', - 'Süßigkeiten' => 'suessigkeiten', - 'Synology' => 'synology', - 'Syoss' => 'syoss', - 'Systemkameras' => 'systemkamera', - 'T-Shirts' => 't-shirts', - 'Tablets' => 'tablet', - 'Tablet Zubehör' => 'tablet-zubehoer', - 'tado° Smartes Heizkörper-Thermostat' => 'tado-smartes-thermostat', - 'Tamaris' => 'tamaris', - 'Tangle Teezer' => 'tangle-teezer', - 'Tanqueray' => 'tanqueray', - 'Tapeten' => 'tapeten', - 'Taschen' => 'taschen', - 'Taschenlampen' => 'taschenlampen', - 'Taschentücher' => 'taschentuecher', - 'Tassimo' => 'tassimo', - 'Tassimo Kaffeemaschinen' => 'tassimo-kaffeemaschinen', - 'Tastaturen' => 'tastatur', - 'TCL Fernseher' => 'tcl-fernseher', - 'Team Sonic Racing' => 'team-sonic-racing', - 'Teamsport' => 'teamsport', - 'Tee' => 'tee', - 'Tefal' => 'tefal', - 'Tefal OptiGrills' => 'tefal-optigrill', - 'Tefal Pfannen' => 'tefal-pfannen', - 'Tekken' => 'tekken', - 'Tekken 7' => 'tekken-7', - 'Telefon- & Internet-Verträge' => 'telefon-internet', - 'Telefone & Zubehör' => 'handy-smartphone', - 'Telekom' => 'telekom-net', - 'Telekom Magenta' => 'telekom-magenta', - 'Telekom SmartHome' => 'telekom-smarthome', - 'Teppiche' => 'teppiche', - 'Tesla' => 'tesla', - 'Tetris' => 'tetris', - 'Teufel' => 'teufel', - 'The Elder Scrolls' => 'the-elder-scrolls', - 'The Elder Scrolls V: Skyrim' => 'skyrim', - 'The Evil Within' => 'the-evil-within', - 'The Evil Within 2' => 'the-evil-within-2', - 'The Last of Us' => 'the-last-of-us', - 'The Last of Us Part II' => 'the-last-of-us-part-ii', - 'The Legend of Zelda' => 'the-legend-of-zelda', - 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild', - 'The Legend of Zelda: Link's Awakening' => 'zelda-links-awakening', - 'The Legend of Zelda: Skyward Sword HD' => 'zelda-skyward-sword-hd', - 'The North Face' => 'the-north-face', - 'The Outer Worlds' => 'the-outer-worlds', - 'Thermosflaschen' => 'thermosflaschen', - 'Thermoskannen' => 'thermoskanne', - 'The Witcher' => 'the-witcher', - 'The Witcher 3' => 'the-witcher-3', - 'Thule' => 'thule', - 'Thule Chariot Fahrradanhänger' => 'thule-chariot-fahrradanhaenger', - 'Thule Dachboxen' => 'thule-dachboxen', - 'Thule Fahrradträger' => 'thule-fahrradtraeger', - 'Tickets & Shows' => 'erlebnisse', - 'Tiefkühlkost' => 'tiefkuehkost', - 'Timberland' => 'timberland', - 'Tintenstrahldrucker' => 'tintenstrahldrucker', - 'Tischlampen' => 'tischlampen', - 'Tischtennis' => 'tischtennis', - 'Tischtennisplatten' => 'tischtennisplatten', - 'Tischtennisschläger' => 'tischtennisschlaeger', - 'Toaster' => 'toaster', - 'Toilettenpapier' => 'toilettenpapier', - 'tolino' => 'tolino', - 'Tomb Raider' => 'tomb-raider', - 'Tom Clancy's' => 'tom-clancys', - 'Tom Clancy's: Ghost Recon Wildlands' => 'tom-clancys-ghost-recon-wildlands', - 'Tom Clancy's Ghost Recon Breakpoint' => 'tom-clancys-ghost-recon-breakpoint', - 'Tom Clancy's The Division 2' => 'tom-clancy-the-division-2', - 'Tommy Hilfiger' => 'tommy-hilfiger', - 'TOM TAILOR' => 'tom-tailor', - 'Toner' => 'toner', - 'Tonic Water' => 'tonic-water', - 'Toniebox' => 'toniebox', - 'Tonies Figuren' => 'tonie-figuren', - 'Töpfe' => 'toepfe', - 'Töpfe & Pfannen' => 'kochen', - 'Toplader' => 'toplader', - 'Toshiba' => 'toshiba', - 'Total War' => 'total-war', - 'Toyota' => 'toyota', - 'TP-Link' => 'tp-link', - 'TP-Link Router' => 'tp-link-router', - 'Trampoline' => 'trampolin', - 'TREKSTOR' => 'trekstor', - 'Trockner' => 'trockner', - 'Tropical Islands' => 'tropical-island', - 'Tropico' => 'tropico', - 'Tropico 5' => 'tropico-5', - 'Tropico 6' => 'tropico-6', - 'TV & Video' => 'tv-video', - 'TV Boxen' => 'tv-box', - 'TV Spielfilm' => 'tv-spielfilm', - 'TV Wandhalterungen' => 'tv-wandhalterung', - 'TV Zubehör' => 'tv-zubehoer', - 'Übergangsjacken' => 'uebergangsjacken', - 'Überwachungskamera' => 'ueberwachungskamera', - 'UE BLAST' => 'ue-blast', - 'UE BOOM' => 'ue-boom', - 'UE BOOM 2' => 'ue-boom-2', - 'UE BOOM 3' => 'ue-boom-3', - 'UE MEGABLAST' => 'ue-megablast', - 'UE MEGABOOM' => 'ue-megaboom', - 'UE MEGABOOM 3' => 'ue-megaboom-3', - 'UE WONDERBOOM' => 'ue-wonderboom', - 'UE WONDERBOOM 2' => 'ue-wonderboom-2', - 'UGG' => 'ugg', - 'Uhren' => 'uhren', - 'Umstandsmode' => 'umstandsmode', - 'Uncharted' => 'uncharted', - 'Uncharted 4' => 'uncharted-4', - 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy', - 'Under Armour' => 'under-armour', - 'Universalfernbedienungen' => 'universalfernbedienungen', - 'Unterwäsche' => 'unterwaesche', - 'Uplay' => 'uplay', - 'Urban Sport' => 'urban-sport', - 'Urlaub' => 'urlaub', - 'USB Sticks' => 'usb-stick', - 'Vakuumierer' => 'vakuumierer', - 'Vans' => 'vans', - 'Vans Old Skool' => 'vans-old-skool', - 'Vans Schuhe' => 'vans-schuhe', - 'Vaude' => 'vaude', - 'Ventilatoren' => 'ventilator', - 'Verbandskästen' => 'verbandskaesten', - 'Versicherung' => 'versicherung', - 'Versicherung & Finanzen' => 'vertraege-finanzen', - 'Videobearbeitungsprogramme' => 'videobearbeitungsprogramme', - 'Video Player' => 'video-player', - 'Videospiele' => 'videospiele', - 'Video Streaming' => 'video-streaming', - 'Vileda' => 'vileda', - 'Villeroy & Boch' => 'villeroy-boch', - 'Virenschutz' => 'virenschutz', - 'VISA' => 'visa', - 'Vliestapeten' => 'vliestapete', - 'Vodafone' => 'vodafone-netz', - 'Vodka' => 'vodka', - 'Volvo' => 'volvo', - 'Vorratsdosen' => 'vorratsdosen', - 'Vorstellungsrunde' => 'vorstellungsrunde', - 'VPN' => 'vpn', - 'VPS' => 'vps', - 'VR Brillen' => 'vr-brille', - 'VR Spiele' => 'vr-spiele', - 'VTech' => 'vtech', - 'VW' => 'vw', - 'Waffeleisen' => 'waffeleisen', - 'Wandbilder' => 'wandtattoos', - 'Wanderrucksäcke' => 'wanderrucksack', - 'Wanderschuhe' => 'wanderschuhe', - 'Wandersport' => 'hiking', - 'Wandfarben' => 'wandfarben', - 'Wandlampen' => 'wandlampen', - 'Wäscheständer' => 'waeschestaender', - 'Waschmaschinen' => 'waschmaschinen', - 'Waschmittel' => 'waschmittel', - 'Waschtrockner' => 'waschtrockner', - 'Wasserfilter' => 'wasserfilter', - 'Wasserkocher' => 'wasserkocher', - 'Wasserkühlung' => 'wasserkuehlung', - 'Wasserspielzeuge' => 'wasserspielzeug', - 'Wassersport' => 'wassersport', - 'Watch Dogs' => 'watch-dogs', - 'Watch Dogs 2' => 'watch-dogs-2', - 'Watch Dogs: Legion' => 'watch-dogs-legion', - 'WC Sitze' => 'wc-sitze', - 'WD-40' => 'wd-40', - 'Wearables' => 'wearable', - 'Webcams' => 'webcam', - 'Weber Gasgrills' => 'weber-gasgrill', - 'Weber Grills' => 'weber-grill', - 'Weihnachtsbäume' => 'weihnachtsbaum', - 'Weihnachtsbeleuchtung' => 'weihnachtsbeleuchtung', - 'Weihnachtsdeko' => 'weihnachtsdeko', - 'Weihnachtspullover' => 'weihnachtspullover', - 'Wein' => 'wein', - 'Wellensteyn' => 'wellensteyn', - 'Wellness & Gesundheit' => 'wellness-massagen', - 'Wera' => 'wera', - 'Werkstatt & Service' => 'werkstatt-service', - 'Werkstatteinrichtungen' => 'werkstatteinrichtungen', - 'Werkzeuge' => 'werkzeug', - 'Werkzeugkoffer' => 'werkzeugkoffer', - 'Wesco Mülleimer' => 'wesco-muelleimer', - 'Western Digital' => 'western-digital', - 'Wetterstationen' => 'wetterstationen', - 'Whirlpools' => 'whirlpools', - 'Whisky' => 'whisky', - 'Wiko' => 'wiko', - 'Wilkinson Sword Rasierer' => 'wilkinson-sword', - 'Windeln' => 'windeln', - 'Winkelschleifer' => 'winkelschleifer', - 'Winterdeko' => 'winterdeko', - 'Winterjacken' => 'winterjacken', - 'Winterreifen' => 'winterreifen', - 'Winterstiefel' => 'winterstiefel', - 'Wireless Charger' => 'wireless-charger', - 'Wirtschaftswoche' => 'wirtschaftswoche', - 'WMF' => 'wmf', - 'WMF Besteck' => 'wmf-besteck', - 'WMF Topfset' => 'wmf-topfset', - 'Wohnzimmermöbel' => 'wohnzimmer', - 'Wolfenstein' => 'wolfenstein', - 'Wolfenstein II: The New Colossus' => 'wolfenstein-2-the-new-colossus', - 'Womanizer' => 'womanizer', - 'World of Warcraft' => 'world-of-warcraft', - 'Wrangler' => 'wrangler', - 'X570 Mainboard' => 'x570-mainboard', - 'Xbox' => 'xbox', - 'Xbox Controller' => 'xbox-controller', - 'Xbox Elite Wireless Controller' => 'xbox-one-elite-controller', - 'Xbox Elite Wireless Controller 2' => 'xbox-one-elite-controller-2', - 'Xbox Game Pass' => 'xbox-game-pass', - 'Xbox Game Pass Ultimate' => 'xbox-game-pass-ultimate', - 'Xbox Guthaben' => 'xbox-guthaben', - 'Xbox Live Gold' => 'xbox-live', - 'Xbox One Controller' => 'xbox-one-controller', - 'Xbox One S Konsolen' => 'xbox-one-s', - 'Xbox One Spiele' => 'xbox-one-spiele', - 'Xbox One X Konsolen' => 'xbox-one-x', - 'Xbox Series S Konsolen' => 'xbox-series-s', - 'Xbox Series X Controller' => 'xbox-series-x-controller', - 'Xbox Series X Konsolen' => 'xbox-series-x', - 'Xbox Series X Spiele' => 'xbox-series-x-spiele', - 'Xbox Wireless Headset' => 'xbox-wireless-headset', - 'Xbox Zubehör' => 'xbox-zubehoer', - 'Xiaomi' => 'xiaomi', - 'Xiaomi Air Laptop' => 'xiaomi-air', - 'Xiaomi E-Scooter' => 'xiaomi-e-scooter', - 'Xiaomi Fernseher' => 'xiaomi-fernseher', - 'Xiaomi Kopfhörer' => 'xiaomi-kopfhoerer', - 'Xiaomi Mi 5S' => 'xiaomi-mi-5', - 'Xiaomi Mi 6' => 'xiaomi-mi-6', - 'Xiaomi Mi 8' => 'xiaomi-mi-8', - 'Xiaomi Mi 8 Lite' => 'xiaomi-mi-8-lite', - 'Xiaomi Mi 8 Pro' => 'xiaomi-mi-8-pro', - 'Xiaomi Mi 9' => 'xiaomi-mi-9', - 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite', - 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se', - 'Xiaomi Mi 9T' => 'xiaomi-mi-9t', - 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro', - 'Xiaomi Mi 10' => 'xiaomi-mi-10', - 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite', - 'Xiaomi Mi 10 Pro' => 'xiaomi-mi-10-pro', - 'Xiaomi Mi 11' => 'xiaomi-mi-11', - 'Xiaomi Mi A1' => 'xiaomi-mi-a1', - 'Xiaomi Mi A2' => 'xiaomi-mi-a2', - 'Xiaomi Mi AirDots' => 'xiaomi-mi-airdots', - 'Xiaomi Mi AirDots Pro' => 'xiaomi-airdots-pro', - 'Xiaomi Mi Band' => 'xiaomi-mi-band', - 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4', - 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5', - 'Xiaomi Mi Electric Scooter 1S' => 'xiaomi-mi-scooter-1s', - 'Xiaomi Mi Electric Scooter M365' => 'xiaomi-mi-electric-scooter-m365', - 'Xiaomi Mi Electric Scooter Pro 2' => 'xiaomi-mi-electric-scooter-pro-2', - 'Xiaomi Mi Mix' => 'xiaomi-mi-mix', - 'Xiaomi Mi Mix 3' => 'xiaomi-mi-mix-3', - 'Xiaomi Mi Note' => 'xiaomi-mi-note', - 'Xiaomi Mi Note 10' => 'xiaomi-mi-note-10', - 'Xiaomi Mi Note 10 Lite' => 'xiaomi-mi-note-10-lite', - 'Xiaomi Mi Note 10 Pro' => 'xiaomi-mi-note-10-pro', - 'Xiaomi Mi TV 4S' => 'xiaomi-mi-smart-tv-4s', - 'Xiaomi Mi TV Stick' => 'xiaomi-mi-tv-stick', - 'Xiaomi Pocophone F1' => 'xiaomi-pocophone-f1', - 'Xiaomi Redmi 9' => 'xiaomi-redmi-9', - 'Xiaomi Redmi 9A' => 'xiaomi-redmi-9a', - 'Xiaomi Redmi AirDots' => 'xiaomi-redmi-airdots', - 'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4', - 'Xiaomi Redmi Note 5' => 'xiaomi-redmi-note-5', - 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8', - 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro', - 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9', - 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro', - 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s', - 'Xiaomi Redmi Note 10' => 'xiaomi-redmi-note-10', - 'Xiaomi Redmi Note 10 Pro' => 'xiaomi-redmi-note-10-pro', - 'Xiaomi Smart Home' => 'xiaomi-smart-home', - 'Xiaomi Smartphones' => 'xiaomi-smartphones', - 'Xiaomi YouPin' => 'xiaomi-youpin', - 'XMG' => 'xmg', - 'Yamaha' => 'yamaha', - 'Yeelight' => 'xiaomi-yeelight', - 'Yoga' => 'yoga', - 'Yogamatten' => 'yogamatten', - 'Yoshi's Crafted World' => 'yoshis-crafted-world', - 'Zahnbürsten' => 'zahnbuersten', - 'Zahnpasta' => 'zahnpasta', - 'Zahnzusatzversicherung' => 'zahnzusatzversicherung', - 'Zeitschriften' => 'zeitschriften-magazine', - 'Zelte' => 'zelte', - 'Zirkel' => 'zirkel', - 'Zoo-Tickets' => 'zoo', - 'Zotac' => 'zotac', - 'ZTE Smartphones' => 'zte-smartphones', - 'ZWILLING' => 'zwilling', - 'ZWILLING Besteck' => 'zwilling-besteck', - ) - ), - 'order' => array( - 'name' => 'sortieren nach', - 'type' => 'list', - 'title' => 'Sortierung der deals', - 'values' => array( - 'Vom heißesten zum kältesten Deal' => '-hot', - 'Vom jüngsten Deal zum ältesten' => '-new', - ) - ), - ), - 'Überwachung Diskussion' => array( - 'url' => array( - 'name' => 'URL der Diskussion', - 'type' => 'text', - 'required' => true, - 'title' => 'URL-Diskussion zu überwachen: https://www.mydealz.de/diskussion/title-123', - 'exampleValue' => 'https://www.mydealz.de/diskussion/anleitung-wie-schreibe-ich-einen-deal-1658317', - ), - 'only_with_url' => array( - 'name' => 'Kommentare ohne URL ausschließen', - 'type' => 'checkbox', - 'title' => 'Kommentare, die keine URL enthalten, im Feed ausschließen', - 'defaultValue' => false, - ) - ) - ); - - public $lang = array( - 'bridge-uri' => SELF::URI, - 'bridge-name' => SELF::NAME, - 'context-keyword' => 'Suche nach Stichworten', - 'context-group' => 'Deals pro Gruppen', - 'context-talk' => 'Überwachung Diskussion', - 'uri-group' => 'gruppe/', - 'request-error' => 'Could not request mydeals', - 'thread-error' => 'Die ID der Diskussion kann nicht ermittelt werden. Überprüfen Sie die eingegebene URL', - 'no-results' => 'Ups, wir konnten keine Deals zu', - 'relative-date-indicator' => array( - 'vor', - 'seit' - ), - 'price' => 'Preis', - 'shipping' => 'Versand', - 'origin' => 'Ursprung', - 'discount' => 'Rabatte', - 'title-keyword' => 'Suche', - 'title-group' => 'Gruppe', - 'title-talk' => 'Überwachung Diskussion', - 'local-months' => array( - 'Jan', - 'Feb', - 'Mär', - 'Apr', - 'Mai', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Okt', - 'Nov', - 'Dez', - '.' - ), - 'local-time-relative' => array( - 'eingestellt vor ', - 'm', - 'h,', - 'day', - 'days', - 'month', - 'year', - 'and ' - ), - 'date-prefixes' => array( - 'eingestellt am ', - 'lokal ', - 'aktualisiert ', - ), - 'relative-date-alt-prefixes' => array( - 'aktualisiert vor ', - 'kommentiert vor ', - 'heiß seit ' - ), - 'relative-date-ignore-suffix' => array( - '/von.*$/' - ), - 'localdeal' => array( - 'Lokal ', - 'Läuft bis ' - ) - ); + 'Deals pro Gruppen' => [ + 'group' => [ + 'name' => 'Gruppen', + 'type' => 'list', + 'title' => 'Gruppe, deren Deals angezeigt werden müssen', + 'values' => [ + '1Password' => '1password', + '3D Drucker' => '3d-drucker', + '4K Fernseher' => '4k-fernseher', + '4K Monitore' => '4k-monitor', + '4K Ultra HD Blu-ray' => 'ultra-hd-blu-ray', + '8K Fernseher' => '8k-fernseher', + '32 Zoll Fernseher' => '32-zoll-fernseher', + '55 Zoll Fernseher' => '55-zoll-fernseher', + '65 Zoll Fernseher' => '65-zoll-fernseher', + '75 Zoll Fernseher' => '75-zoll-fernseher', + '1151 Mainboard' => '1151-mainboard', + 'Abus' => 'abus', + 'ABUS Fahrradschlösser' => 'abus-fahrradschloss', + 'Accessoires' => 'accessoires', + 'Acer' => 'acer', + 'Acer Aspire' => 'acer-aspire', + 'Acer Laptops' => 'acer-laptop', + 'Acer Monitore' => 'acer-monitor', + 'Acer Predator' => 'acer-predator', + 'Action Cameras' => 'actioncam', + 'Actionfiguren' => 'actionfiguren', + 'adidas' => 'adidas', + 'adidas Essentials' => 'adidas-neo', + 'adidas Iniki' => 'adidas-iniki', + 'adidas NMD' => 'adidas-nmd', + 'adidas Originals' => 'adidas-originals', + 'adidas Schuhe' => 'adidas-schuhe', + 'adidas Superstar' => 'adidas-superstar', + 'adidas Ultraboost' => 'adidas-ultraboost', + 'adidas ZX Flux' => 'adidas-zx-flux', + 'Adventskalender' => 'adventskalender', + 'AEG' => 'aeg', + 'AEG Waschmaschinen' => 'aeg-waschmaschine', + 'Age of Empires' => 'age-of-empires', + 'AiO Wasserkühlung' => 'aio-wasserkuehlung', + 'AKG' => 'akg', + 'Akkus' => 'akkus', + 'Akkuschrauber' => 'akkuschrauber', + 'Alfa Romeo' => 'alfa-romeo', + 'Alienware' => 'alienware', + 'Alkohol' => 'alkohol', + 'All Inclusive Reisen' => 'all-inclusive', + 'All in One PCs' => 'all-in-one-pcs', + 'AM4 Mainboard' => 'am4-mainboard', + 'Amazfit' => 'xiaomi-amazfit', + 'Amazfit Bip' => 'amazfit-bip', + 'Amazfit GTS' => 'amazfit-gts', + 'Amazon Echo' => 'amazon-echo', + 'Amazon Echo Dot' => 'amazon-echo-dot', + 'Amazon Echo Plus' => 'amazon-echo-plus', + 'Amazon Echo Show' => 'amazon-echo-show', + 'Amazon Echo Show 5' => 'amazon-echo-show-5', + 'Amazon Echo Show 8' => 'amazon-echo-show-8', + 'Amazon Echo Spot' => 'amazon-echo-spot', + 'Amazon Fire TV Cube' => 'fire-tv-cube', + 'Amazon Fire TV Stick' => 'fire-tv', + 'Amazon Fire TV Stick 4K' => 'fire-tv-stick-4k', + 'Amazon Tablets' => 'amazon-tablet', + 'Amazon Warehouse Deals' => 'amazon-warehouse-deals', + 'AMD' => 'amd', + 'AMD Radeon' => 'amd-radeon', + 'AMD Radeon VII' => 'vega-7', + 'AMD RX Vega' => 'amd-vega', + 'AMD Ryzen' => 'amd-ryzen', + 'AMD Ryzen 9 5900X' => 'amd-ryzen-9-5900x', + 'American Express' => 'american-express', + 'amiibo' => 'amiibo', + 'Analoguhren' => 'analoguhren', + 'Android Apps' => 'android-apps', + 'Android Smartphones' => 'android-smartphones', + 'Angelzubehör' => 'angelsport', + 'Animal Crossing' => 'animal-crossing', + 'Animal Crossing: New Horizons' => 'animal-crossing-new-horizons', + 'Anime' => 'anime', + 'Ankündigungen' => 'ankundigungen', + 'Anno 1800' => 'anno-1800', + 'Anthem' => 'anthem', + 'Anzug' => 'anzug', + 'AOC' => 'aoc', + 'Apex Legends' => 'apex-legends', + 'Apotheke' => 'apotheke', + 'Apple' => 'apple', + 'Apple AirPods' => 'airpods', + 'Apple AirPods 2' => 'airpods-2', + 'Apple AirPods Max' => 'airpods-max', + 'Apple AirPods Pro' => 'airpods-pro', + 'Apple EarPods' => 'apple-earpods', + 'Apple HomePod' => 'homepod', + 'Apple HomePod mini' => 'apple-homepod-mini', + 'Apple Kopfhörer' => 'apple-kopfhoerer', + 'Apple Magic Mouse 2' => 'apple-magic-mouse-2', + 'Apple Pencil' => 'apple-pencil', + 'Apple Pencil 2' => 'apple-pencil-2', + 'Apple TV' => 'apple-tv', + 'Apple Watch' => 'apple-watch', + 'Apple Watch 3' => 'apple-watch-3', + 'Apple Watch 4' => 'apple-watch-4', + 'Apple Watch 5' => 'apple-watch-5', + 'Apple Watch 6' => 'apple-watch-6', + 'Apple Watch SE' => 'apple-watch-se', + 'Apps' => 'apps', + 'Aquaristik' => 'aquaristik', + 'Arbeitsspeicher' => 'arbeitsspeicher', + 'Arbeitszimmermöbel' => 'arbeitszimmer', + 'ASICS' => 'asics', + 'Assassin's Creed' => 'assassins-creed', + 'Assassin's Creed: Valhalla' => 'assassins-creed-valhalla', + 'Assassin's Creed Odyssey' => 'assassins-creed-odyssey', + 'Assassin's Creed Origins' => 'assassins-creed-origins', + 'ASTRO Gaming A50' => 'astro-gaming-a50', + 'ASUS' => 'asus', + 'ASUS Laptops' => 'asus-laptop', + 'Asus Mainboard' => 'asus-mainboard', + 'Asus Monitore' => 'asus-monitor', + 'ASUS ROG' => 'asus-rog', + 'ASUS Smartphones' => 'asus-smartphones', + 'Asus ZenBook' => 'asus-zenbook', + 'ASUS ZenFone 5' => 'asus-zenfone-5', + 'ASUS ZenFone 5Z' => 'asus-zenfone-5z', + 'Audi' => 'audi', + 'Audio & HiFi' => 'audio-hifi', + 'Audioverstärker' => 'audioverstaerker', + 'Audio Zubehör' => 'audio-zubehoer', + 'Aukey' => 'aukey', + 'Außenleuchten' => 'aussenleuchten', + 'Auto & Motorrad' => 'auto-motorrad', + 'Auto Bild' => 'auto-bild', + 'Auto Leasing' => 'auto-leasing', + 'Auto Leasing Gewerbe' => 'gewerbe-leasing', + 'Auto Leasing Privat' => 'privat-leasing', + 'Automatikuhren' => 'automatikuhr', + 'auto motor und sport' => 'auto-motor-sport', + 'Autoradio' => 'autoradio', + 'Auto Teile' => 'autoteile', + 'Autowäsche' => 'autowaesche', + 'Auto Zubehör' => 'auto', + 'AVM FRITZ!Box' => 'avm-fritz-box', + 'AVM FRITZ!Box 7490' => 'avm-fritz-box-7490', + 'AVM FRITZ!Box 7530' => 'avm-fritz-box-7530', + 'AVM FRITZ!Box 7580' => 'avm-fritz-box-7580', + 'AVM FRITZ!Box 7590' => 'avm-fritz-box-7590', + 'AVM FRITZ! DECT 301' => 'avm-fritz-dect-301', + 'AV Receiver' => 'av-receiver', + 'Baby & Kind' => 'kinder', + 'Baby-Erstausstattung' => 'baby-erstausstattung', + 'Babybetten' => 'babybetten', + 'Baby Born' => 'baby-born', + 'Babykleidung' => 'babybekleidung', + 'Babynahrung' => 'babynahrung', + 'Babyphone' => 'babyphone', + 'Backofen & Herd' => 'backofen-herd', + 'Backwaren' => 'backwaren', + 'Backzubehör' => 'backzubehoer', + 'Bademode' => 'bademode', + 'Badmöbel' => 'badezimmer', + 'Bahn-Tickets' => 'bahntickets', + 'Bahncard' => 'bahncard', + 'Balkonmöbel' => 'balkonmoebel', + 'Ballerinas' => 'ballerinas', + 'Bang & Olufsen' => 'bang-olufsen', + 'Bank' => 'bank', + 'Barbie' => 'barbie', + 'Barclaycard' => 'barclaycard', + 'Bartschneider' => 'bartschneider', + 'Batterien' => 'batterien', + 'Battle.net' => 'battle-net', + 'Battlefield' => 'battlefield', + 'Battlefield 1' => 'battlefield-1', + 'Battlefield 5' => 'battlefield-5', + 'Bauknecht' => 'bauknecht', + 'Bauknecht Waschmaschinen' => 'bauknecht-waschmaschine', + 'Baumarkt' => 'baumarkt', + 'Bayonetta' => 'bayonetta', + 'Bayonetta 2' => 'bayonetta-2', + 'Beamer' => 'beamer', + 'Beamer Leinwand' => 'beamer-leinwand', + 'Beats by Dre' => 'beats-by-dre', + 'Beats Solo3' => 'beats-solo3', + 'Beats Solo Pro' => 'beats-solo-pro', + 'Beats Studio3' => 'beats-studio3', + 'Beauty & Gesundheit' => 'beauty', + 'Beko' => 'beko', + 'Beleuchtung' => 'beleuchtung', + 'Belkin' => 'belkin', + 'Ben & Jerry's' => 'ben-jerrys', + 'Bench' => 'bench', + 'BenQ' => 'benq', + 'BenQ Monitore' => 'benq-monitor', + 'be quiet!' => 'be-quiet', + 'be quiet! Netzteile' => 'be-quiet-netzteil', + 'Besteck' => 'besteck', + 'Bethesda' => 'bethesda', + 'Betten' => 'betten', + 'Bettwäsche' => 'bettwaesche', + 'beyerdynamic' => 'beyerdynamic', + 'Beyerdynamic MMX 300' => 'beyerdynamic-mmx-300', + 'BHs' => 'bhs', + 'Bier' => 'bier', + 'Biking & Urban Sport' => 'biking-urban-sport', + 'Bildbearbeitungsprogramme' => 'bildbearbeitungsprogramme', + 'Birkenstock' => 'birkenstock', + 'Black & Decker' => 'black-and-decker', + 'Blackberry Smartphones' => 'blackberry', + 'Black Desert Online' => 'black-desert-online', + 'Blazer' => 'blazer', + 'Blood & Truth' => 'blood-truth', + 'Blu-ray' => 'blu-ray', + 'Blu-ray Player' => 'blu-ray-player', + 'Bluetooth Kopfhörer' => 'bluetooth-kopfhoerer', + 'Bluetooth Lautsprecher' => 'bluetooth-lautsprecher', + 'Blumen' => 'blumen', + 'Blusen' => 'blusen', + 'BMW' => 'bmw', + 'Bodenbelag' => 'bodenbelag', + 'Boho-Chic wohnen' => 'boho-chich-wohnen', + 'Bohrer' => 'bohrer', + 'Bohrhämmer' => 'bohrhaemmer', + 'Bohrmaschinen' => 'bohrmaschinen', + 'Bollerwagen' => 'bollerwagen', + 'Bombay Gin' => 'bombay', + 'Borderlands' => 'borderlands', + 'Borderlands 3' => 'borderlands-3', + 'Bosch' => 'bosch', + 'Bosch Akkuschrauber' => 'bosch-akkuschrauber', + 'Bosch Geschirrspüler' => 'bosch-geschirrspueler', + 'Bosch Kühlschränke' => 'bosch-kuehlschrank', + 'Bosch Waschmaschinen' => 'bosch-waschmaschine', + 'Bose' => 'bose', + 'Bose Headphones 700' => 'bose-headphones-700', + 'Bose Home Speaker 500' => 'bose-home-speaker-500', + 'Bose Kopfhörer' => 'bose-kopfhoerer', + 'Bose QuietComfort' => 'bose-quietcomfort', + 'Bose QuietComfort 35 II' => 'bose-quiet-comfort-35-ii', + 'Bose Solo 5' => 'bose-solo-5', + 'Bose SoundLink' => 'bose-soundlink', + 'Bose SoundTouch' => 'bose-soundtouch', + 'BOSS' => 'boss', + 'Bourbon' => 'bourbon', + 'Bowers & Wilkins' => 'bowers-wilkins', + 'Boxershorts' => 'boxershorts', + 'Boxspringbetten' => 'boxspringbetten', + 'Braun' => 'braun', + 'Braun Rasierer' => 'braun-rasierer', + 'Braun Series 3' => 'braun-series-3', + 'Braun Series 5' => 'braun-series-5', + 'Braun Series 7' => 'braun-series-7', + 'Braun Series 9' => 'braun-series-9', + 'Bridgekameras' => 'bridgekamera', + 'Brigitte' => 'brigitte', + 'Brillen & Kontaktlinsen' => 'brillen', + 'Brita' => 'brita', + 'Britax Römer' => 'britax-roemer', + 'Brotaufstrich' => 'brotaufstrich', + 'Brother Drucker' => 'brother-drucker', + 'Bücher' => 'buecher', + 'Bücher, Magazine & Zeitschriften' => 'buecher-zeitschriften', + 'bugatti' => 'bugatti', + 'Bügeleisen' => 'buegeleisen', + 'Bügeln' => 'buegeln', + 'Buggy' => 'buggy', + 'Burger' => 'burger', + 'BURNHARD' => 'burnhard', + 'Bürobedarf' => 'buerobedarf', + 'Bürostühle' => 'buerostuhl', + 'Bus & Bahn' => 'bus-bahn', + 'Business Mode' => 'business-mode', + 'c't – Magazin für Computertechnik' => 'ct-magazin-computertechnik', + 'Cafissimo' => 'cafissimo', + 'Call of Duty' => 'call-of-duty', + 'Call of Duty: Black Ops 4' => 'call-of-duty-black-ops-4', + 'Call of Duty: Black Ops Cold War' => 'call-of-duty-black-ops-cold-war', + 'Call of Duty: Infinite Warfare' => 'call-of-duty-infinite-warfare', + 'Call of Duty: Modern Warfare' => 'call-of-duty-modern-warfare', + 'Call of Duty: Warzone' => 'call-of-duty-warzone', + 'Call of Duty: WW2' => 'call-of-duty-ww2', + 'Calvin Klein' => 'calvin-klein', + 'Camcorder' => 'camcorder', + 'Campen' => 'campen', + 'Canon' => 'canon', + 'Canon Drucker' => 'canon-drucker', + 'Canon EOS' => 'canon-eos', + 'Canon Kameras' => 'canon-kameras', + 'Canon PowerShot' => 'canon-powershot', + 'CANTON' => 'canton', + 'Caps' => 'caps', + 'Captain Toad: Treasure Tracker' => 'captain-toad-treasure-tracker', + 'Capture One' => 'capture-one', + 'Carhartt' => 'carhartt', + 'Carsharing' => 'carsharing', + 'Casio' => 'casio', + 'Cheap Monday' => 'cheapmonday', + 'Chevrolet' => 'chevrolet', + 'China Handys' => 'china-handys', + 'Chip (Magazin)' => 'chip-magazin', + 'Chips' => 'chips', + 'Christbaumschmuck' => 'christbaumschmuck', + 'Christbaumständer' => 'christbaumstaender', + 'Chromebook' => 'chromebook', + 'Chronographen' => 'chronograph', + 'Chucks' => 'chucks', + 'Citroen' => 'citroen', + 'Coca-Cola' => 'coca-cola', + 'Comics' => 'comics', + 'Computer' => 'computer', + 'Computer & Tablets' => 'computer-tablet', + 'Computer Bild' => 'computer-bild', + 'Controller' => 'controller', + 'Converse' => 'converse', + 'Convertibles' => 'convertibles', + 'Corsair' => 'corsair', + 'Corsair VOID PRO' => 'corsair-void-pro', + 'Couchtische' => 'couchtische', + 'Coupons' => 'coupons', + 'CPU-Kühler' => 'cpu-kuehler', + 'Craghoppers' => 'craghoppers', + 'Crocs' => 'crocs', + 'Crucial' => 'crucial', + 'Cupra' => 'cupra', + 'Cyberpunk 2077' => 'cyberpunk-2077', + 'cybex' => 'cybex', + 'D-Link' => 'd-link', + 'DAB Radios' => 'dab-radios', + 'Dacia' => 'dacia', + 'Damenbekleidung' => 'fashion-frauen', + 'Damenschuhe' => 'damenschuhe', + 'Dampfbügelstation' => 'dampfbuegelstation', + 'Dampfgarer' => 'dampfgarer', + 'Dampfreiniger' => 'dampfreiniger', + 'Dark Souls' => 'dark-souls', + 'Dashcam' => 'dashcam', + 'Datentarif' => 'datentarif', + 'Daypack' => 'daypack', + 'Days Gone' => 'days-gone', + 'DC Shoes' => 'dc-shoes', + 'DDR3 RAM' => 'ddr3-ram', + 'DDR4 RAM' => 'ddr4-ram', + 'De'Longhi' => 'delonghi', + 'Death Stranding' => 'death-stranding', + 'Deckenlampen' => 'deckenlampen', + 'DECT Telefone' => 'telefone', + 'Dekoration' => 'dekoration', + 'Dell' => 'dell', + 'Dell Laptops' => 'dell-laptop', + 'Dell Monitore' => 'dell-monitor', + 'Dell XPS' => 'dell-xps', + 'Denon' => 'denon', + 'Deo' => 'deo', + 'Depot' => 'depot', + 'DER SPIEGEL' => 'der-spiegel', + 'Designermöbel' => 'designermoebel', + 'Desigual' => 'desigual', + 'Desinfektionsmittel' => 'desinfektionsmittel', + 'Desktop PCs' => 'desktop-pc', + 'Dessous' => 'dessous', + 'Destiny' => 'destiny', + 'Destiny 2' => 'destiny-2', + 'Deus Ex' => 'deus-ex', + 'Deus Ex: Mankind' => 'deus-ex-mankind', + 'Deuter' => 'deuter', + 'DeutschlandCard' => 'deutschlandcard', + 'devolo' => 'devolo', + 'DeWalt' => 'dewalt', + 'Die drei Fragezeichen' => 'die-drei-fragezeichen', + 'Die Eiskönigin' => 'die-eiskoenigin', + 'Dienstleistungen & Verträge' => 'dienstleistungen-vertraege', + 'Dies & Das' => 'dies-das', + 'Diesel' => 'diesel', + 'Die Sims' => 'die-sims', + 'Die Sims 4' => 'die-sims-4', + 'Die Zeit' => 'die-zeit', + 'Digitalreceiver' => 'digitalreceiver', + 'Digitaluhren' => 'digitaluhr', + 'Direktflüge' => 'direktfluege', + 'Dirt Devil' => 'dirt-devil', + 'Dishonored' => 'dishonored', + 'Dishonored 2: Das Vermächtnis der Maske' => 'dishonored-2', + 'Disney' => 'disney', + 'Disney+' => 'disney-plus', + 'DJI' => 'dji', + 'DJI Osmo Pocket' => 'dji-osmo-pocket', + 'Dockers' => 'dockers', + 'Dolce Gusto' => 'dolce-gusto', + 'DOOM Eternal' => 'doom-eternal', + 'Douglas Adventskalender' => 'douglas-adventskalender', + 'Dr. Martens' => 'dr-martens', + 'Dragon Ball' => 'dragon-ball', + 'Dragon Ball FighterZ' => 'dragon-ball-fighterz', + 'Dragon Ball Z: Kakarot' => 'dragon-ball-z-kakarot', + 'Dragon Quest Builders' => 'dragon-quest-builders', + 'Dragon Quest Builders 2' => 'dragon-quest-builders-2', + 'Dreame Staubsauger' => 'xiaomi-staubsauger', + 'Dreame T20' => 'dreame-t20', + 'Dreame V9' => 'xiaomi-dreame-v9', + 'Dreame V10' => 'xiaomi-dreame-v10', + 'Dreame V11' => 'xiaomi-dreame-v11', + 'Drohnen' => 'drohnen', + 'Drucker' => 'drucker', + 'Druckerpatronen' => 'druckerpatronen', + 'Druckerzubehör' => 'druckerzubehoer', + 'DSL & Kabel' => 'dsl', + 'Dunstabzugshauben' => 'dunstabzugshauben', + 'Durex' => 'durex', + 'Duscharmaturen' => 'duscharmaturen', + 'Duschgel' => 'duschgel', + 'Duschköpfe' => 'duschkoepfe', + 'DVD' => 'dvd', + 'Dyson' => 'dyson', + 'Dyson Staubsauger' => 'dyson-staubsauger', + 'Dyson V6' => 'dyson-v6', + 'Dyson V7' => 'dyson-v7', + 'Dyson V8' => 'dyson-v8', + 'Dyson V10' => 'dyson-v10', + 'Dyson V11' => 'dyson-v11', + 'Dyson V11 Absolute' => 'dyson-v11-absolute', + 'Dyson V11 Animal' => 'dyson-v11-animal', + 'E-Bikes' => 'e-bikes', + 'E-Scooter' => 'e-scooter', + 'E-Scooter Sharing' => 'e-scooter-sharing', + 'E-Zigaretten' => 'e-zigaretten', + 'Eastpak' => 'eastpak', + 'eBook Reader' => 'ebook-reader', + 'eBooks' => 'ebooks', + 'Ecovacs' => 'ecovacs', + 'Ecovacs Deebot 900' => 'ecovacs-deebot-900', + 'Ecovacs Deebot OZMO 930' => 'ecovacs-deebot-ozmo-930', + 'Edifier' => 'edifier', + 'Edifier R1280DB' => 'edifier-r1280db', + 'Edifier R1280T' => 'edifier-r1280t', + 'Einhell' => 'einhell', + 'Eis' => 'eis', + 'Elektrische Zahnbürsten' => 'elektrische-zahnbuersten', + 'Elektrogrills' => 'elektrogrill', + 'Elektroheizungen' => 'elektroheizungen', + 'Elektronik' => 'elektronik', + 'Elektronik Zubehör' => 'elektronikzubehoer', + 'Elektrorasierer' => 'elektrorasierer', + 'Elektroroller' => 'elektroroller', + 'Elektrowerkzeuge' => 'elektrowerkzeug', + 'Elephone' => 'elephone', + 'ELLE' => 'elle', + 'Emsa' => 'emsa', + 'Energy Drinks' => 'energy-drinks', + 'Entsafter' => 'entsafter', + 'Epilierer' => 'epilierer', + 'Epson' => 'epson', + 'Epson Drucker' => 'epson-drucker', + 'Erotik' => 'erotik', + 'Error Fare' => 'error-fare', + 'Espressomaschinen' => 'espressomaschinen', + 'Esprit' => 'esprit', + 'Esstische' => 'esstisch', + 'Esszimmer' => 'esszimmer', + 'Eterna' => 'eterna', + 'EUROtronic Comet DECT' => 'eurotronic-comet-dect', + 'Externe Festplatten' => 'externe-festplatten', + 'F1 2017' => 'f1-2017', + 'F1 2019' => 'f1-2019', + 'F1 2020' => 'f1-2020', + 'Fahrräder' => 'fahrraeder', + 'Fahrradhelme' => 'fahrradhelme', + 'Fahrradrucksäcke' => 'fahrradrucksack', + 'Fahrradschlösser' => 'fahrradschloss', + 'Fahrradteile' => 'fahrradteile', + 'Fahrradträger' => 'fahrradtraeger', + 'Fahrradzubehör' => 'fahrradzubehoer', + 'Fahrzeuge' => 'fahrzeuge', + 'Falke' => 'falke', + 'Fallout' => 'fallout', + 'Fallout 4' => 'fallout-4', + 'Fallout 76' => 'fallout-76', + 'Family & Kids' => 'family-kids', + 'Far Cry' => 'far-cry', + 'Far Cry 5' => 'far-cry-5', + 'Far Cry New Dawn' => 'far-cry-new-dawn', + 'Fashion & Accessoires' => 'fashion-accessoires', + 'Fast Food' => 'fast-food', + 'Felgen' => 'felgen', + 'Fenstersauger' => 'fenstersauger', + 'Fernbus-Tickets' => 'fernbus', + 'Fernseher' => 'fernseher', + 'Fertiggerichte' => 'fertiggerichte', + 'Festplatten' => 'festplatten', + 'Festplattengehäuse' => 'festplattengehaeuse', + 'FFP2 Masken' => 'ffp2-masken', + 'Fiat' => 'fiat', + 'FIFA' => 'fifa', + 'FIFA 17' => 'fifa-17', + 'FIFA 18' => 'fifa-18', + 'FIFA 19' => 'fifa-19', + 'FIFA 20' => 'fifa-20', + 'FIFA 21' => 'fifa-21', + 'FILA' => 'fila', + 'Filme & Serien' => 'filme-serien', + 'Filterkaffeemaschinen' => 'filterkaffeemaschinen', + 'Final Fantasy' => 'final-fantasy', + 'Final Fantasy 7' => 'final-fantasy-7', + 'Finanzen- und Steuersoftware' => 'finanzen-und-steuersoftware', + 'Finish' => 'finish', + 'Fisch & Meeresfrüchte' => 'fisch-meeresfruechte', + 'Fischertechnik' => 'fischertechnik', + 'Fisher-Price' => 'fisher-price', + 'Fiskars' => 'fiskars', + 'Fissler' => 'fissler', + 'fitbit' => 'fitbit', + 'Fitness & Running' => 'fitness', + 'Fitness Apps' => 'fitness-apps', + 'Fitnessstudio' => 'fitnessstudio', + 'Fitnesstracker' => 'fitnesstracker', + 'Fjällräven' => 'fjaellraeven', + 'Fleisch & Wurst' => 'fleisch-wurst', + 'Fliesenschneider' => 'fliesenschneider', + 'Flüge' => 'fluege', + 'Flurmöbel' => 'flurmoebel', + 'FOCUS' => 'focus', + 'Ford' => 'ford', + 'For Honor' => 'for-honor', + 'Formel 1 Games' => 'formel-1', + 'Fortnite' => 'fortnite', + 'Forza' => 'forza', + 'Forza Horizon' => 'forza-horizon', + 'Forza Horizon 4' => 'forza-horizon-4', + 'Forza Motorsport' => 'forza-motorsport', + 'Forza Motorsport 7' => 'forza-7', + 'Fossil' => 'fossil', + 'Foto & Kamera' => 'foto-video', + 'Foto Apps' => 'foto-apps', + 'Fotobücher' => 'fotobuecher', + 'Fototapete' => 'fototapete', + 'Fragen & Gesuche' => 'gesuche', + 'Frankfurter Allgemeine Zeitung (F.A.Z.)' => 'frankfurter-allgemeine-zeitung', + 'FreeSync Monitore' => 'freesync-monitor', + 'Freizeitpark-Tickets' => 'freizeitpark', + 'Freizeitsport' => 'freizeitsport', + 'Fritteusen' => 'fritteusen', + 'Frontlader' => 'frontlader', + 'Frühlingsdeko' => 'fruehlingsdeko', + 'Frühstücksflocken' => 'fruehstuecksflocken', + 'Fruit of the Loom' => 'fruit-of-the-loom', + 'Fujifilm' => 'fujifilm', + 'Füller' => 'fueller', + 'Full HD-Beamer' => 'full-hd-beamer', + 'Fun Factory' => 'fun-factory', + 'FurReal Friends' => 'furreal-friends', + 'Fußball' => 'fussball', + 'Fußball-Trikots' => 'fussball-trikots', + 'Fußballschuhe' => 'fussballschuhe', + 'G-Star' => 'g-star', + 'G-Sync Monitore' => 'g-sync-monitor', + 'Game of Thrones' => 'game-of-thrones', + 'Gaming' => 'gaming', + 'Gaming Headsets' => 'gaming-headset', + 'Gaming Laptops' => 'gaming-laptop', + 'Gaming Mäuse' => 'gaming-maus', + 'Gaming Monitore' => 'gaming-monitor', + 'Gaming PCs' => 'gaming-pc', + 'Gaming Stühle' => 'gaming-stuhl', + 'Gaming Tastaturen' => 'gaming-tastatur', + 'Gaming Zubehör' => 'spielekonsolen-zubehoer', + 'Ganzjahresreifen' => 'ganzjahresreifen', + 'GAP' => 'gap', + 'Gardena' => 'gardena', + 'Garderobe' => 'garderobe', + 'Garmin' => 'garmin', + 'Garmin Fenix' => 'garmin-fenix', + 'Garten' => 'garten', + 'Garten & Baumarkt' => 'garten-baumarkt', + 'Gartenarbeit' => 'gartenarbeit', + 'Gartenbank' => 'gartenbank', + 'Gartenliegen' => 'sonnenliegen', + 'Gartenmöbel' => 'gartenmoebel', + 'Gartenstühle' => 'gartenstuehle', + 'Gartentische' => 'gartentische', + 'Gasgrills' => 'gasgrill', + 'Gastarif' => 'gastarif', + 'Gears 5' => 'gears-5', + 'Gears of War' => 'gears-of-war', + 'Gefrierschränke' => 'gefrierschrank', + 'Geld-zurück-Aktionen' => 'geld-zurueck', + 'Geldbörsen' => 'geldboersen', + 'Gemüse' => 'gemuese', + 'Geox' => 'geox', + 'Geschirr' => 'geschirr', + 'Geschirrspüler' => 'geschirrspueler', + 'Gesellschaftsspiele' => 'gesellschaftsspiele', + 'Gesichtspflege' => 'gesichtspflege', + 'Gesundheit' => 'gesundheit', + 'Getränke' => 'getraenke', + 'Gewinnspiele' => 'gewinnspiele', + 'GHD' => 'ghd', + 'Ghost of Tsushima' => 'ghost-of-tsushima', + 'GIGABYTE' => 'gigabyte', + 'Gigaset' => 'gigaset', + 'Gillette' => 'gillette', + 'Gillette Rasierer' => 'gillette-rasierer', + 'Gin' => 'gin', + 'Girokonto' => 'konto', + 'Glamour' => 'glamour', + 'Glamourös wohnen' => 'glamouroes-wohnen', + 'Gläser' => 'glaeser', + 'Glätteisen' => 'glaetteisen', + 'Gleitgel' => 'gleitgel', + 'Glühwein' => 'gluehwein', + 'God of War' => 'god-of-war', + 'Google Chromecast' => 'chromecast', + 'Google Chromecast mit Google TV' => 'chromecast-mit-google-tv', + 'Google Chromecast Ultra' => 'chromecast-ultra', + 'Google Home' => 'google-home', + 'Google Home Max' => 'google-home-max', + 'Google Home Mini' => 'google-home-mini', + 'Google Nest Hub' => 'google-nest-hub', + 'Google Pixel' => 'google-pixel', + 'Google Pixel 2' => 'google-pixel-2', + 'Google Pixel 3' => 'google-pixel-3', + 'Google Pixel 4' => 'google-pixel-4', + 'Google Pixel 4 XL' => 'google-pixel-4xl', + 'Google Pixel 4a' => 'google-pixel-4a', + 'Google Pixel 4a 5G' => 'google-pixel-4a-5g', + 'Google Pixel 5' => 'google-pixel-5', + 'Google Smartphones' => 'google-smartphones', + 'Google Stadia Konsolen' => 'google-stadia', + 'GoPro Action Cameras' => 'gopro', + 'GoPro HERO 7' => 'gopro-hero-7', + 'GoPro HERO 8' => 'gopro-hero-8', + 'GoPro HERO 9' => 'gopro-hero-9', + 'Gorenje' => 'gorenje', + 'Grafikkarten' => 'grafikkarten', + 'Gran Turismo' => 'gran-turismo', + 'Gran Turismo Sport' => 'gran-turismo-sport', + 'Grazia' => 'grazia', + 'Grills' => 'grill', + 'Grillzubehör' => 'grillzubehoer', + 'Grundig' => 'grundig', + 'GTA' => 'gta', + 'GTA V' => 'gta-v', + 'GTX 1060' => 'gtx-1060', + 'GTX 1070' => 'gtx-1070', + 'GTX 1080' => 'gtx-1080', + 'GTX 1080 Ti' => 'gtx-1080-ti', + 'GTX 1660' => 'gtx-1660', + 'GTX 1660 Ti' => 'gtx-1660-ti', + 'Gucci' => 'gucci', + 'Gummistiefel' => 'gummistiefel', + 'Gürtel' => 'guertel', + 'Gutscheinfehler' => 'gutscheinfehler', + 'Haarentfernung' => 'haarentfernung', + 'Haargel' => 'haargel', + 'Haarpflege' => 'haarpflege', + 'Haarschneidemaschinen' => 'haarschneidemaschinen', + 'Haarspray' => 'haarspray', + 'Haartrockner' => 'haartrockner', + 'Haftpflichtversicherung' => 'haftpflichtversicherung', + 'Hama' => 'hama', + 'Handelsblatt' => 'handelsblatt', + 'Handmixer' => 'handmixer', + 'Handtaschen' => 'handtaschen', + 'Handtücher' => 'handtuecher', + 'Handwerkzeuge' => 'handwerkzeug', + 'Handy & Smartphone Zubehör' => 'smartphone-zubehoer', + 'Handyhalterung' => 'handyhalterung', + 'Handyhüllen' => 'handyhuelle', + 'Handys mit Vertrag' => 'handys-mit-vertrag', + 'Handys ohne Vertrag' => 'handys-ohne-vertrag', + 'Handyversicherung' => 'handyversicherung', + 'Handyverträge' => 'handyvertraege', + 'Handyverträge 3 Monate Kündigungsfrist' => 'handyvertraege-3-monate-kuendigungsfrist', + 'Handyverträge monatlich kündbar' => 'handyvertraege-monatlich-kuendbar', + 'Hängematten' => 'haengematten', + 'Hanteln' => 'hanteln', + 'Haribo' => 'haribo', + 'Harman Kardon' => 'harman-kardon', + 'Harry Potter' => 'harry-potter', + 'Hasbro' => 'hasbro', + 'Haushaltsartikel' => 'haushaltsartikel', + 'Haushaltsgeräte' => 'haushaltsgeraete', + 'Haushaltswaren' => 'haushaltswaren', + 'Hausratversicherung' => 'hausratsversicherung', + 'Hausschuhe' => 'hausschuhe', + 'Haustier' => 'haustier', + 'Hautpflege' => 'hautpflege', + 'Head & Shoulders' => 'head-and-shoulders', + 'Heckenscheren' => 'heckenschere', + 'Heimkino' => 'heimkino', + 'Heimtextilien' => 'heimtextilien', + 'Heißluftfritteusen' => 'heissluftfriteuse', + 'Heizkörperthermostat' => 'heizkoerperthermostat', + 'Heizungen' => 'heizungen', + 'Hemden' => 'hemden', + 'Hendrick's Gin' => 'hendricks-gin', + 'Herbstdeko' => 'herbstdeko', + 'Herrenbekleidung' => 'fashion-maenner', + 'Herrenschuhe' => 'herrenschuhe', + 'HiPP' => 'hipp', + 'Hisense' => 'hisense', + 'Hochbetten' => 'hochbetten', + 'Hochdruckreiniger' => 'hochdruckreiniger', + 'Hochstuhl' => 'hochstuhl', + 'Hollywoodschaukel' => 'hollywoodschaukel', + 'Home & Living' => 'home-living', + 'homee' => 'homee', + 'Honda' => 'honda', + 'Honor' => 'honor', + 'Honor 5' => 'honor-5', + 'Honor 6' => 'honor-6', + 'Honor 7X' => 'honor-7', + 'Honor 8' => 'honor-8', + 'Honor 9' => 'honor-9', + 'Honor 20' => 'honor-20', + 'Honor 20 Lite' => 'honor-20-lite', + 'Honor Band 4' => 'honor-band-4', + 'Honor Band 5' => 'honor-band-5', + 'Honor Play' => 'honor-play', + 'Honor Smartphones' => 'honor-smartphones', + 'Honor View 10' => 'honor-view-10', + 'Honor View 20' => 'honor-view-20', + 'Hoodies' => 'hoodies', + 'Hörbücher' => 'hoerbuecher', + 'Horizon Zero Dawn' => 'horizon-zero-dawn', + 'Hörspiele' => 'hoerspiele', + 'Hörzu' => 'hoerzu', + 'Hosen' => 'hosen', + 'Hotels & Unterkünfte' => 'hotel', + 'Hot Wheels' => 'hot-wheels', + 'Hoverboards' => 'hoverboards', + 'HP' => 'hp', + 'HP Drucker' => 'hp-drucker', + 'HP Laptops' => 'hp-laptop', + 'HP OMEN' => 'hp-omen', + 'HP Pavilion' => 'hp-pavilion', + 'HTC 10' => 'htc-10', + 'HTC Desire 12' => 'htc-desire', + 'HTC Smartphones' => 'htc-smartphones', + 'HTC U11' => 'htc-u11', + 'HTC Vive' => 'htc-vive', + 'Huawei' => 'huawei', + 'Huawei Kopfhörer' => 'huawei-kopfhoerer', + 'Huawei Mate 9' => 'huawei-mate-9', + 'Huawei Mate 10' => 'huawei-mate-10', + 'Huawei Mate 20' => 'huawei-mate-20', + 'Huawei Mate 20 Lite' => 'huawei-mate-20-lite', + 'Huawei Mate 20 Pro' => 'huawei-mate-20-pro', + 'Huawei Mate 30 Pro' => 'huawei-mate-30-pro', + 'Huawei MateBook' => 'huawei-matebook', + 'Huawei P10' => 'huawei-p10', + 'Huawei P20' => 'huawei-p20', + 'Huawei P30' => 'huawei-p30', + 'Huawei P30 Lite' => 'huawei-p30-lite', + 'Huawei P30 Pro' => 'huawei-p30-pro', + 'Huawei P40' => 'huawei-p40', + 'Huawei P40 Lite' => 'huawei-p40-lite', + 'Huawei P40 Pro' => 'huawei-p40-pro', + 'Huawei P Smart' => 'huawei-p-smart', + 'Huawei Smartphones' => 'huawei-smartphones', + 'Huawei Tablets' => 'huawei-mediapad', + 'Huawei Watch GT2' => 'huawei-watch-gt2', + 'Huawei Y7' => 'huawei-y7', + 'Hunde' => 'hunde', + 'Hundefutter' => 'hundefutter', + 'Hüte & Mützen' => 'huete-muetzen', + 'Hyrule Warriors' => 'hyrule-warriors', + 'Hyrule Warriors: Zeit der Verheerung' => 'hyrule-warriors-zeit-der-verheerung', + 'Hyundai' => 'hyundai', + 'iMac' => 'imac', + 'Immortals Fenyx Rising' => 'immortals-fenyx-rising', + 'In-Ear Kopfhörer' => 'in-ear-kopfhoerer', + 'Industrial Style' => 'industrial-style', + 'Inline Skates' => 'inline-skates', + 'Instax Mini' => 'instax-mini', + 'Intel Core i9-9900K' => 'intel-core-i9-9900k', + 'Intel i3' => 'intel-i3', + 'Intel i5' => 'intel-i5', + 'Intel i7' => 'intel-i7', + 'Intel i9' => 'intel-i9', + 'Intenso' => 'intenso', + 'Internet Security' => 'internet-security', + 'Intimpflege' => 'intimpflege', + 'iOS Apps' => 'ios-apps', + 'iPad' => 'ipad', + 'iPad 2019' => 'ipad-2019', + 'iPad 2020' => 'ipad-2020', + 'iPad Air' => 'ipad-air-2', + 'iPad Air 2019' => 'ipad-air-2019', + 'iPad Air 2020' => 'ipad-air-2020', + 'iPad mini' => 'ipad-mini', + 'iPad Pro' => 'ipad-pro', + 'iPad Pro 11' => 'ipad-pro-11', + 'iPad Pro 12.9' => 'ipad-pro-12-9', + 'iPad Pro 2020' => 'ipad-pro-2020', + 'iPhone' => 'iphone', + 'iPhone 6' => 'iphone-6', + 'iPhone 6 Plus' => 'iphone-6-plus', + 'iPhone 6s' => 'iphone-6s', + 'iPhone 6s Plus' => 'iphone-6s-plus', + 'iPhone 7' => 'iphone-7', + 'iPhone 7 Plus' => 'iphone-7-plus', + 'iPhone 8' => 'iphone-8', + 'iPhone 8 Plus' => 'iphone-8-plus', + 'iPhone 11' => 'iphone-11', + 'iPhone 11 Pro' => 'iphone-11-pro', + 'iPhone 11 Pro Max' => 'iphone-11-pro-max', + 'iPhone 12' => 'iphone-12', + 'iPhone 12 mini' => 'iphone-12-mini', + 'iPhone 12 Pro' => 'iphone-12-pro', + 'iPhone 12 Pro Max' => 'iphone-12-pro-max', + 'iPhone SE' => 'iphone-se', + 'iPhone X' => 'iphone-x', + 'iPhone Xr' => 'iphone-xr', + 'iPhone Xs' => 'iphone-xs', + 'iPhone Xs Max' => 'iphone-xs-max', + 'iPhone Zubehör' => 'iphone-zubehoer', + 'Irish Whiskey' => 'irish-whiskey', + 'iRobot' => 'irobot', + 'iRobot Roomba' => 'irobot-roomba', + 'iRobot Roomba 980' => 'irobot-roomba-980', + 'iRobot Roomba i7' => 'irobot-roomba-i7', + 'Isomatten' => 'isomatten', + 'iTunes Guthaben' => 'itunes-guthaben', + 'Jabra Elite 75t' => 'jabra-elite-75t', + 'Jabra Elite 85h' => 'jabra-elite-85h', + 'Jabra Elite 85t' => 'jabra-elite-85t', + 'Jabra Elite Active 75t' => 'jabra-elite-active-75t', + 'Jabra Kopfhörer' => 'jabra-kopfhoerer', + 'JACK & JONES' => 'jack-jones', + 'Jacken' => 'jacken', + 'JACK WOLFSKIN' => 'jack-wolfskin', + 'Jagdzubehör' => 'jagdzubehoer', + 'JBL' => 'jbl', + 'JBL Charge 4' => 'jbl-charge-4', + 'JBL Flip' => 'jbl-flip', + 'JBL GO' => 'jbl-go', + 'Jeans' => 'jeans', + 'Jim Beam' => 'jim-beam', + 'Jogginghosen' => 'jogginghosen', + 'Joghurt' => 'joghurt', + 'Johnnie Walker' => 'johnnie-walker', + 'Jura Kaffeemaschinen' => 'jura', + 'Just Cause' => 'just-cause', + 'Just Cause 4' => 'just-cause-4', + 'Kaffee' => 'kaffee', + 'Kaffeekapseln' => 'kaffeekapseln', + 'Kaffeemaschinen' => 'kaffeemaschinen', + 'Kaffeemühlen' => 'kaffeemuehlen', + 'Kaffeepadmaschinen' => 'kaffeepadmaschinen', + 'Kaffeepads' => 'kaffeepads', + 'Kaffeevollautomaten' => 'kaffeevollautomaten', + 'Kameras' => 'kamera', + 'Kamera Zubehör' => 'kamerazubehoer', + 'Kamine' => 'kamine', + 'Kapselmaschinen' => 'kapselmaschinen', + 'Kärcher' => 'kaercher', + 'Kärcher Fenstersauger' => 'kaercher-fenstersauger', + 'Kärcher Hochdruckreiniger' => 'kaercher-hochdruckreiniger', + 'Kartenspiele' => 'kartenspiel', + 'Käse' => 'kaese', + 'Katzen' => 'katzen', + 'Katzenfutter' => 'katzenfutter', + 'Kaufen im Ausland' => 'kaufen-ausland', + 'Ketchup' => 'ketchup', + 'KFZ Versicherung' => 'kfz-versicherung', + 'KIA' => 'kia', + 'kiddy' => 'kiddy', + 'Kinder Adventskalender' => 'kinder-adventskalender', + 'Kinderbekleidung' => 'kinderkleidung', + 'Kinderbetten' => 'kinderbett', + 'Kinderfahrräder' => 'kinderfahrrad', + 'Kinderschuhe' => 'kinderschuhe', + 'Kindersitz' => 'kindersitz', + 'Kinderwagen' => 'kinderwagen', + 'Kinderwagen & Autositze' => 'baby-transport', + 'Kinderzimmermöbel' => 'kinderzimmer', + 'Kindle' => 'kindle', + 'Kindle Oasis' => 'kindle-oasis', + 'Kindle Paperwhite' => 'kindle-paperwhite', + 'Kingdom Come: Deliverance' => 'kingdom-come-deliverance', + 'Kingdom Hearts' => 'kingdom-hearts', + 'Kingdom Hearts 3' => 'kingdom-hearts-3', + 'Kingston HyperX Cloud Flight' => 'kingston-hyperx-cloud-flight', + 'Kingston HyperX Cloud II' => 'hyperx-cloud-ii', + 'Kino' => 'kino', + 'KitchenAid' => 'kitchenaid', + 'Kleider' => 'kleider', + 'Kleiderschränke' => 'kleiderschraenke', + 'Kleidung' => 'kleidung', + 'Klemmbausteine' => 'klemmbausteine', + 'Klimaanlagen' => 'klimaanlagen', + 'Klimatechnik' => 'klimatechnik', + 'Klipsch' => 'klipsch', + 'Kochgeräte' => 'kochgeraete', + 'Kodak' => 'kodak', + 'Koffer' => 'koffer', + 'Kohlenmonoxidmelder' => 'kohlenmonoxidmelder', + 'Kolonialstil' => 'kolonialstil', + 'Kommoden & Sideboards' => 'kommoden-sideboards', + 'Kondome' => 'kondome', + 'König der Löwen Musical' => 'koenig-der-loewen-musical', + 'Kontaktgrills' => 'kontaktgrill', + 'Konto & Kreditkarten' => 'konto-kreditkarten', + 'Konzert-Tickets' => 'konzerte', + 'Kopfhörer' => 'kopfhoerer', + 'Körperpflege & Hygiene' => 'koerperpflege', + 'Kosmetik' => 'kosmetik', + 'Kostüme' => 'kostuem', + 'Kraftstoffe & Betriebsstoffe' => 'kraftstoffe-betriebsstoffe', + 'Krafttraining' => 'krafttraining', + 'Kredit' => 'kredit', + 'Kreditkarten' => 'kreditkarten', + 'Kreissägen' => 'kreissaegen', + 'Kreuzfahrten' => 'kreuzfahrten', + 'Krups' => 'krups', + 'Küche' => 'kueche', + 'Küchengeräte' => 'kuechengeraete', + 'Küchenhelfer' => 'kuechenhelfer', + 'Küchenmaschinen' => 'kuechenmaschinen', + 'Küchenmesser' => 'messer', + 'Küchenutensilien' => 'kuechenutensilien', + 'Kugelschreiber' => 'kugelschreiber', + 'Kühl-Gefrierkombinationen' => 'kuehl-gefrierkombination', + 'Kühlboxen' => 'kuehlboxen', + 'Kühlschränke' => 'kuehlschrank', + 'Kultur & Freizeit' => 'kultur-freizeit', + 'Kunst & Hobby' => 'hobby', + 'Kurse & Trainings' => 'kurse-trainings', + 'Lacoste' => 'lacoste', + 'Ladegeräte' => 'ladegeraete', + 'Lampen' => 'lampen', + 'Landhausstil' => 'landhausstil', + 'Landwirtschafts-Simulator' => 'landwirtschafts-simulator', + 'Laptops' => 'laptop', + 'Laserdrucker' => 'laserdrucker', + 'Last Minute Reisen' => 'last-minute', + 'Lattenroste' => 'lattenroste', + 'Laubsauger' => 'laubsauger', + 'Laufräder' => 'laufraeder', + 'Laufschuhe' => 'laufschuhe', + 'Laufsport' => 'laufsport', + 'Lautsprecher' => 'lautsprecher', + 'Lavazza' => 'lavazza', + 'Lay-Z-Spa Whirlpools' => 'lay-z-spa-whirlpools', + 'Lebensmittel' => 'lebensmittel', + 'Lebensmittel & Haushalt' => 'food', + 'LED Lampen' => 'led-lampen', + 'LEGO' => 'lego', + 'LEGO Adventskalender' => 'lego-adventskalender', + 'LEGO Architecture' => 'lego-architecture', + 'LEGO Batman' => 'lego-batman', + 'LEGO City' => 'lego-city', + 'LEGO Creator' => 'lego-creator', + 'LEGO Dimensions' => 'lego-dimensions', + 'LEGO DUPLO' => 'lego-duplo', + 'LEGO Friends' => 'lego-friends', + 'LEGO Harry Potter' => 'lego-harry-potter', + 'LEGO Marvel Super Heroes' => 'lego-marvel-super-heroes', + 'LEGO Nexo Knights' => 'lego-nexo-knights', + 'LEGO NINJAGO' => 'lego-ninjago', + 'LEGO Star Wars' => 'lego-star-wars', + 'LEGO Star Wars Millennium Falcon' => 'lego-star-wars-millennium-falcon', + 'LEGO Super Mario' => 'lego-super-mario', + 'LEGO Technic' => 'lego-technic', + 'LEGO The Simpsons' => 'lego-simpsons', + 'Leifheit' => 'leifheit', + 'Lenovo' => 'lenovo', + 'Lenovo Laptops' => 'lenovo-laptop', + 'Lenovo Tablets' => 'lenovo-tablet', + 'Lenovo ThinkPad' => 'lenovo-thinkpad', + 'Lenovo Yoga' => 'lenovo-yoga', + 'Leonardo' => 'leonardo', + 'Leuchtmittel' => 'leuchten', + 'Levi's' => 'levis', + 'Lexar' => 'lexar', + 'Lexmark' => 'lexmark', + 'LG' => 'lg', + 'LG Fernseher' => 'lg-fernsher', + 'LG G5' => 'lg-g5', + 'LG G6' => 'lg-g6', + 'LG G7 ThinQ' => 'lg-g7-thinq', + 'LG OLED Fernseher' => 'lg-oled-tv', + 'LG Smartphones' => 'lg-smartphones', + 'LG V30' => 'lg-v30', + 'Lichterketten' => 'lichterketten', + 'Liebeskind' => 'liebeskind', + 'Lieferservice' => 'lieferservice', + 'Lindt' => 'lindt', + 'Lindt Adventskalender' => 'lindt-adventskalender', + 'Logitech' => 'logitech', + 'Logitech G413' => 'logitech-g413', + 'Logitech G430' => 'logitech-g430', + 'Logitech G502 Proteus Spectrum' => 'logitech-g502', + 'Logitech G513' => 'logitech-g513', + 'Logitech G533' => 'logitech-g533', + 'Logitech G633 Artemis Spectrum' => 'logitech-g633', + 'Logitech G703' => 'logitech-g703', + 'Logitech G903' => 'logitech-g903', + 'Logitech G910 Orion Spectrum' => 'logitech-g910', + 'Logitech G915' => 'logitech-g915', + 'Logitech G933 Artemis Spectrum' => 'logitech-g933', + 'Logitech Harmony' => 'logitech-harmony', + 'Logitech Mäuse' => 'logitech-maeuse', + 'Logitech MX Master' => 'logitech-mx-master', + 'Logitech MX Master 2S' => 'logitech-mx-master-2s', + 'Logitech Tastaturen' => 'logitech-tastaturen', + 'Logitech Z333' => 'logitech-z333', + 'Logitech Z337' => 'logitech-z337', + 'Logitech Z906' => 'logitech-z906', + 'Luftbefeuchter' => 'luftbefeuchter', + 'Luftentfeuchter' => 'luftentfeuchter', + 'Luftmatratzen' => 'luftmatratzen', + 'Luftreiniger' => 'luftreiniger', + 'Luigi's Mansion' => 'luigis-mansion', + 'Luigi's Mansion 3' => 'luigis-mansion-3', + 'Lustiges Taschenbuch' => 'lustiges-taschenbuch', + 'M.2 SSD' => 'm2-ssd', + 'MacBook' => 'macbook', + 'MacBook Air' => 'macbook-air', + 'MacBook Pro' => 'macbook-pro', + 'MacBook Pro 13' => 'macbook-pro-13', + 'MacBook Pro 15' => 'macbook-pro-15', + 'MacBook Pro 16' => 'macbook-pro-16', + 'Mac mini' => 'mac-mini', + 'Mac Software' => 'mac-software', + 'Madden NFL' => 'madden-nfl', + 'Magazine' => 'magazine', + 'Magnat' => 'magnat', + 'Magnum Eis' => 'magnum-eis', + 'Mähroboter' => 'maehroboter', + 'Mainboards' => 'mainboards', + 'Make Up Adventskalender' => 'make-up-adventskalender', + 'Makita' => 'makita', + 'Makita Akkuschrauber' => 'makita-akkuschrauber', + 'Malerwerkzeuge' => 'malerpinsel', + 'Mangas' => 'mangas', + 'Marantz' => 'marantz', + 'Mario Kart' => 'mario-kart', + 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe', + 'Marken' => 'marken', + 'Marvel' => 'marvel', + 'Marvel's Spider-Man: Miles Morales' => 'marvels-spider-man-miles-morales', + 'Mass Effect' => 'mass-effect', + 'Mass Effect: Andromeda' => 'mass-effect-andromeda', + 'Massivholzmöbel' => 'massivholzmoebel', + 'Mastercard' => 'mastercard', + 'Matratzen' => 'matratzen', + 'Maxi Cosi' => 'maxi-cosi', + 'Mazda' => 'mazda', + 'Medion' => 'medion', + 'Mercedes-Benz' => 'mercedes-benz', + 'Mesh WLAN Router' => 'mesh-wlan-router', + 'Metabo' => 'metabo', + 'Metro (Spiel)' => 'metro', + 'Metro Exodus' => 'metro-exodus', + 'Michael Kors' => 'michael-kors', + 'microSD' => 'microsd', + 'microSDHC' => 'microsdhc', + 'microSDXC' => 'microsdxc', + 'Microsoft Flight Simulator' => 'microsoft-flight-simulator', + 'Microsoft Software' => 'microsoft-software', + 'Microsoft Surface Notebooks' => 'microsoft-surface-notebooks', + 'Microsoft Surface Pro 4' => 'surface-pro-4', + 'Microsoft Surface Pro 6' => 'surface-pro-6', + 'Microsoft Surface Pro 7' => 'microsoft-surface-pro-7', + 'Microsoft Surface Tablets' => 'microsoft-surface', + 'Miele' => 'miele', + 'Miele Geschirrspüler' => 'miele-geschirrspueler', + 'Miele Staubsauger' => 'miele-staubsauger', + 'Miele Waschmaschinen' => 'miele-waschmaschine', + 'Mietwagen' => 'mietwagen', + 'Mikrofone' => 'mikrofone', + 'Mikrowellen' => 'mikrowelle', + 'Milchaufschäumer' => 'milchaufschaeumer', + 'Milka' => 'milka', + 'Minecraft' => 'minecraft', + 'Mineralwasser' => 'mineralwasser', + 'Minions' => 'minions', + 'Mini PCs' => 'mini-pc', + 'Mitsubishi' => 'mitsubishi', + 'Mittelerde' => 'middle-earth', + 'Mittelerde: Mordors Schatten' => 'mittelerde-mordors-schatten', + 'Mittelerde: Schatten des Krieges' => 'mittelerde-schatten-des-krieges', + 'Mixer & Rührer' => 'mixer', + 'Möbel' => 'moebel-deko', + 'Modellbau' => 'modellbau', + 'Modern wohnen' => 'modern-wohnen', + 'Monitore' => 'monitor', + 'Monkey 47' => 'monkey-47', + 'Monopoly' => 'monopoly', + 'Monster Hunter' => 'monster-hunter', + 'Monster Hunter: World' => 'monster-hunter-world', + 'Mortal Kombat' => 'mortal-kombat', + 'Mortal Kombat 11' => 'mortal-kombat-11', + 'Motorola' => 'motorola', + 'Motorola Smartphones' => 'motorola-smartphones', + 'Motorradbekleidung' => 'motorradbekleidung', + 'Motorradhelm' => 'motorradhelm', + 'Motorrad Zubehör' => 'motorrad', + 'Moto Z' => 'moto-z', + 'Mountainbikes' => 'mountainbikes', + 'MSI' => 'msi', + 'Mülleimer' => 'muelleimer', + 'Multifunktionsdrucker' => 'multifunktionsdrucker', + 'Multiroom Speaker' => 'multiroom', + 'Mund- & Zahnpflege' => 'mund-zahnpflege', + 'Mundschutzmasken' => 'mundschutzmasken', + 'Museums-Tickets' => 'museum', + 'Musical Tickets' => 'musical', + 'Musik' => 'musik', + 'Musik Apps' => 'musik-apps', + 'Musikinstrumente' => 'musikinstrumente', + 'Musik Streaming' => 'musik-streaming', + 'Müsli' => 'muesli', + 'Mustang' => 'mustang', + 'Mützen' => 'muetzen', + 'Nachtwäsche' => 'nachtwaesche', + 'Nähbedarf' => 'naehen', + 'Nähmaschinen' => 'naehmaschine', + 'Nahrungsergänzungsmittel' => 'nahrungsergaenzungsmittel', + 'Nahverkehr' => 'nahverkehr', + 'Naketano' => 'naketano', + 'NAS' => 'nas', + 'Nassrasierer' => 'rasierer', + 'Navigationsgeräte' => 'navigationsgeraete', + 'Neato' => 'neato', + 'Neato Robotics Botvac D7 Connected' => 'neato-botvac-d7', + 'Need for Speed' => 'need-for-speed', + 'Need for Speed Heat' => 'need-for-speed-heat', + 'Need for Speed Payback' => 'need-for-speed-payback', + 'Nerf' => 'nerf', + 'Nescafé' => 'nescafe', + 'Nespresso' => 'nespresso', + 'Nespresso Kaffeemaschinen' => 'nespresso-kaffeemaschinen', + 'Netflix' => 'netflix', + 'NETGEAR' => 'netgear', + 'NETGEAR Nighthawk' => 'netgear-nighthawk', + 'NETGEAR Orbi' => 'netgear-orbi', + 'NETGEAR Router' => 'netgear-router', + 'Netzteile' => 'netzteile', + 'Netzwerk' => 'netzwerk', + 'New Balance' => 'new-balance', + 'Nike' => 'nike', + 'Nike Air Force 1' => 'nike-air-force', + 'Nike Air Max' => 'nike-air-max', + 'Nike Air Max 270' => 'nike-air-max-270', + 'Nike Air Max 720' => 'nike-air-max-720', + 'Nike Air Max Thea' => 'nike-air-max-thea', + 'Nike Air Presto' => 'nike-presto', + 'Nike Free' => 'nike-free', + 'Nike Huarache' => 'nike-huarache', + 'Nike Roshe Run' => 'nike-roshe-run', + 'Nike Schuhe' => 'nike-schuhe', + 'Nikon' => 'nikon', + 'Nikon DSLR' => 'nikon-dslr', + 'Ni No Kuni' => 'ni-no-kuni', + 'Ni No Kuni: Der Fluch der Weißen Königin' => 'ni-no-kuni-der-fluch-der-weissen-koenigin', + 'Ni No Kuni II: Revenant Kingdom' => 'ni-no-kuni-ii', + 'Nintendo' => 'nintendo', + 'Nintendo 2DS Konsolen' => 'nintendo-2ds', + 'Nintendo 3DS Konsolen' => 'nintendo-3ds', + 'Nintendo 3DS Spiele' => 'nintendo-3ds-spiele', + 'Nintendo 3DS Zubehör' => 'nintendo-3ds-zubehoer', + 'Nintendo Classic Mini NES Konsolen' => 'nintendo-classic-mini-nes', + 'Nintendo Classic Mini SNES Konsolen' => 'nintendo-classic-mini-snes', + 'Nintendo eShop Guthaben' => 'nintendo-eshop-guthaben', + 'Nintendo Switch Controller' => 'nintendo-switch-controller', + 'Nintendo Switch Konsolen' => 'nintendo-switch', + 'Nintendo Switch Lite Konsolen' => 'nintendo-switch-lite', + 'Nintendo Switch Pro Controller' => 'nintendo-switch-pro-controller', + 'Nintendo Switch Spiele' => 'nintendo-switch-spiele', + 'Nintendo Switch Zubehör' => 'nintendo-switch-zubehoer', + 'Nintendo Zubehör' => 'nintendo-zubehoer', + 'Nissan' => 'nissan', + 'Nivea' => 'nivea', + 'Nokia' => 'nokia', + 'Nokia Handys' => 'nokia-handys', + 'Nudeln' => 'nudeln', + 'Nuki Smart Locks' => 'nuki-smart-lock', + 'Nüsse' => 'nuesse', + 'Nutella' => 'nutella', + 'Nvidia' => 'nvidia', + 'Nvidia GeForce' => 'nvidia-geforce', + 'Nvidia SHIELD TV' => 'nvidia-shield', + 'o2' => 'o2-netz', + 'Objektive' => 'objektiv', + 'Obst' => 'obst', + 'Obst & Gemüse' => 'obst-gemuese', + 'Oculus Quest' => 'oculus-quest', + 'Oculus Rift' => 'oculus-rift', + 'Office Programme' => 'office-programme', + 'OLED Fernseher' => 'oled-fernseher', + 'Olympus' => 'olympus', + 'On-Ear Kopfhörer' => 'on-ear-kopfhoerer', + 'OnePlus 3' => 'oneplus-3', + 'OnePlus 5' => 'oneplus-5', + 'OnePlus 6' => 'oneplus-6', + 'OnePlus 7' => 'oneplus-7', + 'OnePlus 7 Pro' => 'oneplus-7-pro', + 'OnePlus 7T' => 'oneplus-7t', + 'OnePlus 7T Pro' => 'oneplus-7t-pro', + 'OnePlus 8' => 'oneplus-8', + 'OnePlus 8 Pro' => 'one-plus-8-pro', + 'OnePlus 8T' => 'oneplus-8t', + 'OnePlus Nord' => 'oneplus-nord', + 'OnePlus Smartphones' => 'oneplus-smartphones', + 'Onkyo' => 'onkyo', + 'Opel' => 'opel', + 'OPPO Find X2 Lite' => 'oppo-find-x2-lite', + 'OPPO Find X2 Neo' => 'oppo-find-x2-neo', + 'OPPO Find X2 Pro' => 'oppo-find-x2-pro', + 'OPPO Reno2' => 'oppo-reno2', + 'OPPO Reno2 Z' => 'oppo-reno2-z', + 'OPPO Reno4 5G' => 'oppo-reno4-5g', + 'OPPO Reno4 Pro 5G' => 'oppo-reno4-pro-5g', + 'OPPO Reno4 Z 5G' => 'oppo-reno4-z-5g', + 'OPPO Smartphones' => 'oppo-smartphones', + 'Oral-B' => 'oral-b', + 'Oral-B Elektrische Zahnbürsten' => 'oral-b-elektrische-zahnbuersten', + 'Origin' => 'origin', + 'Osram' => 'osram', + 'Osram Smart+' => 'osram-smart-plus', + 'Osterdeko' => 'osterdeko', + 'Outdoor & Camping' => 'outdoor', + 'Outdoorbekleidung' => 'outdoorbekleidung', + 'Outdoorjacken' => 'outdoorjacken', + 'Outdoor Spielzeuge' => 'outdoor-spielzeug', + 'Over-Ear Kopfhörer' => 'over-ear-kopfhoerer', + 'Pampers' => 'pampers', + 'Panama Jack' => 'panama-jack', + 'Panasonic' => 'panasonic', + 'Panasonic Fernseher' => 'panasonic-fernseher', + 'Panasonic Kameras' => 'panasonic-kameras', + 'Panasonic Lumix' => 'panasonic-lumix', + 'Paper Mario: The Origami King' => 'paper-mario-the-origami-king', + 'Papiertapete' => 'papiertapete', + 'Parfum' => 'parfum', + 'Parfum Damen' => 'parfum-damen', + 'Parfum Herren' => 'parfum-herren', + 'Pauschalreisen' => 'pauschalreise', + 'Pavillons' => 'pavillons', + 'Paw Patrol' => 'paw-patrol', + 'PAYBACK' => 'payback', + 'Payday' => 'payday', + 'Payday 2' => 'payday-2', + 'paydirekt' => 'paydirekt', + 'PC Gaming Systeme' => 'pc-gaming-systeme', + 'PC Gaming Zubehör' => 'pc-gaming-zubehoer', + 'PC Gehäuse' => 'pc-gehaeuse', + 'PC Komponenten' => 'hardware', + 'PC Lautsprecher' => 'pc-lautsprecher', + 'PC Mäuse' => 'pc-maus', + 'PC Spiele' => 'pc-spiele', + 'PC Zubehör' => 'pc-zubehoer', + 'Pendelleuchten' => 'pendelleuchten', + 'Pentax' => 'pentax', + 'Pepe Jeans' => 'pepe-jeans', + 'Peppa Wutz' => 'peppa-wutz', + 'PepperBonus' => 'pepperbonus', + 'Pestos' => 'pestos', + 'Peugeot' => 'peugeot', + 'Pfannen' => 'pfannen', + 'Pflanzen' => 'pflanzen', + 'Philips' => 'philips', + 'Philips Fernseher' => 'philips-fernseher', + 'Philips Hue' => 'philips-hue', + 'Philips Hue E14' => 'philips-hue-e14', + 'Philips Hue E27' => 'philips-hue-e27', + 'Philips Hue Go' => 'philips-hue-go', + 'Philips Hue GU10' => 'philips-hue-gu10', + 'Philips Hue LightStrip' => 'philips-hue-lightstrip', + 'Philips Hue Play Gradient LightStrip' => 'philips-hue-play-gradient-lightstrip', + 'Philips Hue Play HDMI Sync Box' => 'philips-hue-play-hdmi-sync-box', + 'Philips Hue Play Lightbar' => 'philips-hue-play', + 'Philips OneBlade' => 'philips-oneblade', + 'Philips Rasierer' => 'philips-rasierer', + 'Philips Sonicare' => 'philips-sonicare', + 'Philips Staubsauger' => 'philips-staubsauger', + 'Philips Wecker' => 'philips-wecker', + 'Photoshop' => 'photoshop', + 'Pioneer' => 'pioneer', + 'Pizza' => 'pizza', + 'Plattenspieler' => 'plattenspieler', + 'Playboy' => 'playboy', + 'Playerunknown's Battlegrounds' => 'playerunknowns-battlegrounds', + 'PLAYMOBIL' => 'playmobil', + 'PLAYMOBIL Adventskalender' => 'playmobil-adventskalender', + 'PlayStation' => 'playstation', + 'PlayStation 4 Controller' => 'playstation-4-controller', + 'PlayStation 4 Konsolen' => 'playstation-4', + 'PlayStation 4 Pro Konsolen' => 'playstation-4-pro', + 'PlayStation 4 Spiele' => 'playstation-4-spiele', + 'PlayStation 5 Konsolen' => 'playstation-5', + 'PlayStation 5 Spiele' => 'playstation-5-spiele', + 'PlayStation Classic Konsolen' => 'playstation-classic', + 'PlayStation Now' => 'playstation-now', + 'PlayStation Plus' => 'playstation-plus', + 'PlayStation Zubehör' => 'playstation-zubehoer', + 'Plüschtiere' => 'plueschtiere', + 'Plus Size Mode' => 'plus-size-mode', + 'POCO F2 Pro' => 'poco-f2-pro', + 'POCO X3' => 'poco-x3', + 'Pokémon' => 'pokemon', + 'Pokémon: Let's Go' => 'pokemon-lets-go', + 'Pokémon Schwert und Schild' => 'pokemon-schwert-schild', + 'Pokémon Tekken' => 'pokemon-tekken', + 'Pokémon Ultrasonne & Ultramond' => 'pokemon-ultrasonne-ultramond', + 'Poloshirts' => 'poloshirts', + 'Polsterbetten' => 'polsterbetten', + 'Polyrattan Möbel' => 'polyrattan', + 'Pools' => 'pools', + 'Powerbanks' => 'powerbanks', + 'Powerbeats Pro' => 'powerbeats', + 'Preisfehler' => 'preisfehler', + 'Prepaid-Tarife' => 'prepaid-tarife', + 'Prime Gaming' => 'twitch-prime', + 'Pro Evolution Soccer' => 'pro-evolution-soccer', + 'Pro Evolution Soccer 2018' => 'pes-2018', + 'Pro Evolution Soccer 2019' => 'pes-2019', + 'Pro Evolution Soccer 2020' => 'pes-2020', + 'Proteine' => 'whey-proteine', + 'Prozessoren' => 'prozessoren', + 'PSN Guthaben' => 'psn-guthaben', + 'Puky' => 'puky', + 'Pullover' => 'pullover', + 'PUMA' => 'puma', + 'Pumps' => 'pumps', + 'Puppen' => 'puppen', + 'Puppenhäuser' => 'puppenhaeuser', + 'Puzzles' => 'puzzle', + 'Qeridoo' => 'qeridoo', + 'Qeridoo Fahrradanhänger' => 'qeridoo-fahrradanhaenger', + 'Qeridoo KidGoo 2' => 'qeridoo-kidgoo-2', + 'Qeridoo Sportrex 2' => 'qeridoo-sportrex-2', + 'Quiksilver' => 'quiksilver', + 'Raclettes' => 'raclettes', + 'Radios' => 'radios', + 'Radsport' => 'radsport', + 'Rasenmäher' => 'rasenmaeher', + 'Rasentrimmer' => 'rasentrimmer', + 'Rasierklingen' => 'rasierklingen', + 'Raspberry Pi' => 'raspberry-pi', + 'Rasur, Enthaarung & Trimmen' => 'rasur-enthaarung', + 'Rauchmelder' => 'rauchmelder', + 'Ravensburger' => 'ravensburger', + 'Ray-Ban' => 'ray-ban', + 'Razer DeathAdder' => 'razer-deathadder', + 'RC Autos' => 'rc-autos', + 'Red Bull' => 'red-bull', + 'Red Dead Redemption' => 'red-dead-redemption', + 'Red Dead Redemption 2' => 'red-dead-redemption-2', + 'Reebok' => 'reebok', + 'Regale' => 'regale', + 'Reifen' => 'reifen', + 'Reinigungsmittel' => 'reinigungsmittel', + 'Reise Apps' => 'reise-apps', + 'Reisen' => 'reisen', + 'Reiskocher' => 'reiskocher', + 'Remington' => 'remington', + 'Renault' => 'renault', + 'Rennräder' => 'rennraeder', + 'Repeater' => 'repeater', + 'Resident Evil' => 'resident-evil', + 'Resident Evil 2' => 'resident-evil-2', + 'Resident Evil 7' => 'resident-evil-7', + 'Restaurant' => 'restaurant', + 'Retro Stil' => 'retro-stil', + 'Rimowa' => 'rimowa', + 'Ring Fit Adventure' => 'ring-fit-adventure', + 'Rituals' => 'rituals', + 'Rituals Adventskalender' => 'rituals-adventskalender', + 'Roborock' => 'xiaomi-roborock', + 'Roborock S5 Max' => 'roborock-s5-max', + 'Roborock S6' => 'roborock-s6', + 'Roborock S6 MaxV' => 'roborock-s6-maxv', + 'ROCCAT' => 'roccat', + 'ROCCAT Tyon' => 'roccat-tyon', + 'Röcke' => 'roecke', + 'Rocket League' => 'rocket-league', + 'Roidmi Staubsauger' => 'roidmi-staubsauger', + 'Rollei' => 'rollei', + 'Rösle' => 'roesle', + 'Router' => 'router', + 'Roxy' => 'roxy', + 'RTX 2060' => 'rtx-2060', + 'RTX 2070' => 'rtx-2070', + 'RTX 2080' => 'rtx-2080', + 'RTX 2080 Ti' => 'rtx-2080-ti', + 'RTX 3070' => 'rtx-3070', + 'RTX 3080' => 'rtx-3080', + 'RTX 3090' => 'rtx-3090', + 'Rucksäcke' => 'rucksaecke', + 'Russell Hobbs' => 'russell-hobbs', + 'RX 480' => 'rx-480', + 'RX 570' => 'rx-570', + 'RX 580' => 'rx-580', + 'RX 590' => 'rx-590', + 'RX 5700 XT' => 'rx-5700-xt', + 'RX 6800' => 'rx-6800', + 'RX 6800 XT' => 'rx-6800-xt', + 'RX 6900 XT' => 'rx-6900-xt', + 'RX Vega 56' => 'rx-vega-56', + 'RX Vega 64' => 'rx-vega-64', + 'Sägen' => 'saegen', + 'Salomon' => 'salomon', + 'Samsonite' => 'samsonite', + 'Samsung' => 'samsung', + 'Samsung Fernseher' => 'samsung-fernseher', + 'Samsung Galaxy A7' => 'samsung-galaxy-a7', + 'Samsung Galaxy A8' => 'samsung-galaxy-a8', + 'Samsung Galaxy A51' => 'samsung-galaxy-a51', + 'Samsung Galaxy A71' => 'samsung-galaxy-a71', + 'Samsung Galaxy Buds' => 'samsung-galaxy-buds', + 'Samsung Galaxy Buds+' => 'samsung-galaxy-buds-plus', + 'Samsung Galaxy Buds Live' => 'samsung-galaxy-buds-live', + 'Samsung Galaxy Buds Pro' => 'samsung-galaxy-buds-pro', + 'Samsung Galaxy Note9' => 'samsung-galaxy-note-9', + 'Samsung Galaxy Note20' => 'samsung-galaxy-note20', + 'Samsung Galaxy Note20 Ultra' => 'samsung-galaxy-note20-ultra', + 'Samsung Galaxy S7' => 'samsung-galaxy-s7', + 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge', + 'Samsung Galaxy S8' => 'samsung-galaxy-s8', + 'Samsung Galaxy S8+' => 'samsung-galaxy-s8-plus', + 'Samsung Galaxy S9' => 'samsung-galaxy-s9', + 'Samsung Galaxy S9+' => 'samsung-galaxy-s9-plus', + 'Samsung Galaxy S10' => 'samsung-galaxy-s10', + 'Samsung Galaxy S10+' => 'samsung-galaxy-s10-plus', + 'Samsung Galaxy S10e' => 'samsung-galaxy-s10e', + 'Samsung Galaxy S20' => 'samsung-galaxy-s20', + 'Samsung Galaxy S20 FE' => 'samsung-galaxy-s20-fe', + 'Samsung Galaxy S20 Ultra' => 'samsung-galaxy-s20-ultra', + 'Samsung Galaxy S20+' => 'samsung-galaxy-s20-plus', + 'Samsung Galaxy S21 5G' => 'samsung-galaxy-s21-5g', + 'Samsung Galaxy S21 Ultra 5G' => 'samsung-galaxy-s21-ultra-5g', + 'Samsung Galaxy S21+ 5G' => 'samsung-galaxy-s21-plus-5g', + 'Samsung Galaxy Tab S4' => 'samsung-galaxy-tab-s4', + 'Samsung Galaxy Tab S6' => 'samsung-galaxy-tab-s6', + 'Samsung Galaxy Watch' => 'samsung-galaxy-watch', + 'Samsung Galaxy Watch Active2' => 'samsung-galaxy-watch-active-2', + 'Samsung Gear' => 'samsung-gear', + 'Samsung Gear S3' => 'samsung-gear-s3', + 'Samsung Gear VR' => 'samsung-gear-vr', + 'Samsung Kopfhörer' => 'samsung-kopfhoerer', + 'Samsung Kühlschränke' => 'samsung-kuehlschrank', + 'Samsung Monitore' => 'samsung-monitor', + 'Samsung QLED Fernseher' => 'samsung-qled-fernseher', + 'Samsung Smartphones' => 'samsung-smartphone', + 'Samsung SSD' => 'samsung-ssd', + 'Samsung Tablets' => 'samsung-tablet', + 'Samsung The Frame Fernseher' => 'samsung-the-frame-fernseher', + 'Samsung Waschmaschinen' => 'samsung-waschmaschine', + 'Sandalen' => 'sandalen', + 'SanDisk' => 'sandisk', + 'SanDisk SSD' => 'sandisk-ssd', + 'Sanitär & Armaturen' => 'sanitaer-armaturen', + 'Saucen' => 'saucen', + 'Saugroboter' => 'saugroboter', + 'Scanner' => 'scanner', + 'Schallplatten' => 'schallplatten', + 'Scheppach' => 'scheppach', + 'Schlafsäcke' => 'schlafsack', + 'Schlafsofas' => 'schlafsofas', + 'Schlafzimmer' => 'schlafzimmer', + 'Schlagschrauber' => 'schlagschrauber', + 'Schlauchboote' => 'schlauchboote', + 'Schleich' => 'schleich', + 'Schlitten' => 'schlitten', + 'Schmuck' => 'schmuck', + 'Schneefräsen' => 'schneefraesen', + 'Schnellkochtöpfe' => 'schnellkochtoepfe', + 'Schnürhalbschuhe' => 'schnuerhalbschuhe', + 'Schokolade' => 'schokolade', + 'Schraubendreher' => 'schraubendreher', + 'Schreibgeräte' => 'schreibgeraete', + 'Schreibtische' => 'schreibtisch', + 'Schuhe' => 'schuhe', + 'Schuhschränke' => 'schuhschraenke', + 'Schulbedarf' => 'schulbedarf', + 'Schulranzen' => 'schulranzen', + 'Schutzfolien' => 'schutzfolien', + 'Schwangerschaft' => 'schwangerschaft', + 'Schwerlastregale' => 'schwerlastregale', + 'Scooter' => 'scooter', + 'Scotch Whisky' => 'scotch-whisky', + 'SDHC Speicherkarten' => 'sdhc-speicherkarten', + 'SD Karten' => 'sd-karten', + 'Seagate' => 'seagate', + 'Sea of Thieves' => 'sea-of-thieves', + 'Seat' => 'seat', + 'Sega Mega Drive Mini Konsolen' => 'sega-mega-drive-mini', + 'Seidensticker' => 'seidensticker', + 'Sekiro: Shadows Die Twice' => 'sekiro', + 'Senf' => 'senf', + 'Sennheiser' => 'sennheiser', + 'Senseo' => 'senseo', + 'Service-Verträge' => 'service-vertraege', + 'Sessel' => 'sessel', + 'Sextoys' => 'sextoys', + 'Shadow of the Tomb Raider' => 'shadow-of-the-tomb-raider', + 'Shampoo' => 'shampoo', + 'Sharkoon' => 'sharkoon', + 'Sharp' => 'sharp', + 'Shenmue' => 'shenmue', + 'Shenmue I & II' => 'shenmue-i-ii', + 'Shenmue III' => 'shenmue-iii', + 'Shishas' => 'shishas', + 'Shishas & Zubehör' => 'shishas-zubehoer', + 'Shoop' => 'shoop', + 'Shops: Erfahrungen' => 'shops', + 'Shorts' => 'shorts', + 'Sicherheitstechnik' => 'sicherheitstechnik', + 'Side-by-Side-Kühlschränke' => 'side-by-side-kuehlschrank', + 'Sid Meier's Civilization VI' => 'sid-meiers-civilization-vi', + 'Sid Meier’s Civilization' => 'sid-meiers-civilization', + 'Siemens' => 'siemens', + 'Siemens Geschirrspüler' => 'siemens-geschirrspueler', + 'Siemens Kühlschränke' => 'siemens-kuehlschrank', + 'Siemens Waschmaschinen' => 'siemens-waschmaschine', + 'Silit' => 'silit', + 'Skandi Stil' => 'skandi-stil', + 'Skateboards' => 'skateboard', + 'Skaten' => 'skaten', + 'Ski & Snowboard' => 'snowboard', + 'Skoda' => 'skoda', + 'Sky' => 'sky', + 'Sky Ticket' => 'sky-ticket', + 'Smarte Beleuchtung' => 'smarte-beleuchtung', + 'Smarte Wecker' => 'smarte-wecker', + 'Smart Home' => 'smart-home', + 'Smart Home Steckdosen' => 'smart-home-steckdosen', + 'Smart Locks' => 'smart-lock', + 'Smartphones' => 'smartphone', + 'Smartphones unter 200€' => 'smartphones-unter-200-euro', + 'Smart Speaker' => 'smart-speaker', + 'Smart Tech & Gadgets' => 'smart-tech', + 'Smartwatches' => 'smartwatch', + 'Smoothie Maker' => 'smoothie-maker', + 'Snacks & Knabberzeug' => 'snacks-knabberzeug', + 'Sneakers' => 'sneaker', + 'Socken' => 'socken', + 'SodaStream' => 'sodastream', + 'Sofas' => 'sofa', + 'Sofortbildkameras' => 'sofortbildkameras', + 'Softdrinks' => 'softdrinks', + 'Software' => 'software', + 'Software & Apps' => 'apps-software', + 'Solarleuchten' => 'solarleuchten', + 'Somat' => 'somat', + 'Sommerreifen' => 'sommerreifen', + 'Sonnenbrillen' => 'sonnenbrillen', + 'Sonnencreme' => 'sonnencreme', + 'Sonnenpflege' => 'sonnenpflege', + 'Sonnenschirme' => 'sonnenschirme', + 'Sonoff' => 'sonoff', + 'Sonos' => 'sonos', + 'Sonos Beam' => 'sonos-beam', + 'Sonos Move' => 'sonos-move', + 'Sonos One' => 'sonos-one', + 'Sonos PLAY:1' => 'sonos-play-1', + 'Sonos PLAY:3' => 'sonos-play-3', + 'Sonos Play:5 (Five)' => 'sonos-play-5', + 'Sonos Playbar' => 'sonos-playbar', + 'Sonos Playbase' => 'sonos-playbase', + 'Sonstiges' => 'diverses', + 'Sony' => 'sony', + 'Sony Alpha 7' => 'sony-alpha-7', + 'Sony Alpha 7 II' => 'sony-alpha-7-ii', + 'Sony Alpha 7 III' => 'sony-alpha-7-iii', + 'Sony Alpha 6000' => 'sony-alpha-6000', + 'Sony Alpha 6300' => 'sony-alpha-6300', + 'Sony Alpha 6400' => 'sony-alpha-6400', + 'Sony Alpha 6500' => 'sony-alpha-6500', + 'Sony DualSense Wireless-Controller' => 'playstation-5-controller', + 'Sony Fernseher' => 'sony-fernseher', + 'Sony Kameras' => 'sony-kameras', + 'Sony Kopfhörer' => 'sony-kopfhoerer', + 'Sony PlayStation VR' => 'sony-playstation-vr', + 'Sony PULSE 3D Wireless Headset' => 'sony-pulse-3d-wireless-headset', + 'Sony WF-1000XM3' => 'sony-wf-1000xm3', + 'Sony WH-1000XM3' => 'sony-wh-1000xm3', + 'Sony WH-1000XM4' => 'sony-wh-1000xm4', + 'Sony Xperia' => 'sony-xperia', + 'Sony Xperia X' => 'sony-xperia-x', + 'Sony Xperia XA' => 'sony-xperia-xa', + 'Sony Xperia XZ' => 'sony-xperia-xz', + 'Soundbar' => 'soundbar', + 'Soundbase' => 'soundbase', + 'Soundkarten' => 'soundkarten', + 'South Park: Die rektakuläre Zerreißprobe' => 'south-park-die-rektakulaere-zerreissprobe', + 'Spartipps' => 'spartipps', + 'Speicherkarten' => 'speicherkarten', + 'Speichermedien' => 'speichermedien', + 'Speiseöle' => 'speiseoele', + 'Spiegelreflexkameras' => 'spiegelreflexkamera', + 'Spiele & Brettspiele' => 'spiele-brettspiele', + 'Spiele Apps' => 'spiele-apps', + 'Spielekonsolen' => 'spielekonsolen', + 'Spielfiguren & Spielsets' => 'spielfiguren-spielsets', + 'Spielzeuge' => 'spielzeug', + 'Spirituosen' => 'spirituosen', + 'Sport & Outdoor' => 'sport', + 'Sportbekleidung' => 'sportbekleidung', + 'Sport Bild' => 'sport-bild', + 'Sportnahrung' => 'sportlernahrung', + 'Sporttasche' => 'sporttasche', + 'Spotify' => 'spotify', + 'Spülmaschinentabs' => 'spuelmaschinentabs', + 'Spyro Reignited Trilogy' => 'spyro-reignited-trilogy', + 'SSD' => 'ssd', + 'Stabmixer' => 'stabmixer', + 'Städtereisen' => 'staedtereise', + 'Standmixer' => 'standmixer', + 'Star Trek' => 'star-trek', + 'Star Wars' => 'star-wars', + 'Star Wars: Battlefront 2' => 'star-wars-battlefront-2', + 'Star Wars: Squadrons' => 'star-wars-squadrons', + 'Star Wars Battlefront' => 'star-wars-battlefront', + 'Star Wars Jedi: Fallen Order' => 'star-wars-jedi-fallen-order', + 'Staubsauger' => 'staubsauger', + 'Staubsaugerbeutel' => 'staubsaugerbeutel', + 'Staubsauger ohne Beutel' => 'staubsauger-ohne-beutel', + 'Steam' => 'steam', + 'Steckschlüssel' => 'steckschluessel', + 'SteelSeries' => 'steelseries', + 'Stehlampen' => 'stehlampen', + 'Steiff' => 'steiff', + 'Stern (Magazin)' => 'stern-magazin', + 'Stichsägen' => 'stichsaegen', + 'Stiefel' => 'stiefel', + 'Stiefeletten' => 'stiefeletten', + 'Stiftung Warentest' => 'stiftung-warentest-magazin', + 'Streaming-Dienste' => 'streaming-dienste', + 'Streaming Lautsprecher' => 'streaming-lautsprecher', + 'Strom & Gas' => 'strom-gas', + 'Stromtarif' => 'stromtarif', + 'Studentenrabatte' => 'studentenrabatte', + 'Stühle' => 'stuehle', + 'Subwoofer' => 'subwoofer', + 'SUP Boards' => 'sup-boards', + 'Superdry' => 'superdry', + 'Super Mario' => 'super-mario', + 'Super Mario 3D All-Stars' => 'super-mario-3d-all-stars', + 'Super Mario Maker 2' => 'super-mario-maker-2', + 'Super Mario Odyssey' => 'super-mario-odyssey', + 'Super Mario Party' => 'super-mario-party', + 'Supermarkt' => 'supermarkt', + 'Super Smash Bros. Ultimate' => 'super-smash-bros-ultimate', + 'Süßigkeiten' => 'suessigkeiten', + 'Synology' => 'synology', + 'Syoss' => 'syoss', + 'Systemkameras' => 'systemkamera', + 'T-Shirts' => 't-shirts', + 'Tablets' => 'tablet', + 'Tablet Zubehör' => 'tablet-zubehoer', + 'tado° Smartes Heizkörper-Thermostat' => 'tado-smartes-thermostat', + 'Tamaris' => 'tamaris', + 'Tangle Teezer' => 'tangle-teezer', + 'Tanqueray' => 'tanqueray', + 'Tapeten' => 'tapeten', + 'Taschen' => 'taschen', + 'Taschenlampen' => 'taschenlampen', + 'Taschentücher' => 'taschentuecher', + 'Tassimo' => 'tassimo', + 'Tassimo Kaffeemaschinen' => 'tassimo-kaffeemaschinen', + 'Tastaturen' => 'tastatur', + 'TCL Fernseher' => 'tcl-fernseher', + 'Team Sonic Racing' => 'team-sonic-racing', + 'Teamsport' => 'teamsport', + 'Tee' => 'tee', + 'Tefal' => 'tefal', + 'Tefal OptiGrills' => 'tefal-optigrill', + 'Tefal Pfannen' => 'tefal-pfannen', + 'Tekken' => 'tekken', + 'Tekken 7' => 'tekken-7', + 'Telefon- & Internet-Verträge' => 'telefon-internet', + 'Telefone & Zubehör' => 'handy-smartphone', + 'Telekom' => 'telekom-net', + 'Telekom Magenta' => 'telekom-magenta', + 'Telekom SmartHome' => 'telekom-smarthome', + 'Teppiche' => 'teppiche', + 'Tesla' => 'tesla', + 'Tetris' => 'tetris', + 'Teufel' => 'teufel', + 'The Elder Scrolls' => 'the-elder-scrolls', + 'The Elder Scrolls V: Skyrim' => 'skyrim', + 'The Evil Within' => 'the-evil-within', + 'The Evil Within 2' => 'the-evil-within-2', + 'The Last of Us' => 'the-last-of-us', + 'The Last of Us Part II' => 'the-last-of-us-part-ii', + 'The Legend of Zelda' => 'the-legend-of-zelda', + 'The Legend of Zelda: Breath of the Wild' => 'zelda-breath-of-the-wild', + 'The Legend of Zelda: Link's Awakening' => 'zelda-links-awakening', + 'The Legend of Zelda: Skyward Sword HD' => 'zelda-skyward-sword-hd', + 'The North Face' => 'the-north-face', + 'The Outer Worlds' => 'the-outer-worlds', + 'Thermosflaschen' => 'thermosflaschen', + 'Thermoskannen' => 'thermoskanne', + 'The Witcher' => 'the-witcher', + 'The Witcher 3' => 'the-witcher-3', + 'Thule' => 'thule', + 'Thule Chariot Fahrradanhänger' => 'thule-chariot-fahrradanhaenger', + 'Thule Dachboxen' => 'thule-dachboxen', + 'Thule Fahrradträger' => 'thule-fahrradtraeger', + 'Tickets & Shows' => 'erlebnisse', + 'Tiefkühlkost' => 'tiefkuehkost', + 'Timberland' => 'timberland', + 'Tintenstrahldrucker' => 'tintenstrahldrucker', + 'Tischlampen' => 'tischlampen', + 'Tischtennis' => 'tischtennis', + 'Tischtennisplatten' => 'tischtennisplatten', + 'Tischtennisschläger' => 'tischtennisschlaeger', + 'Toaster' => 'toaster', + 'Toilettenpapier' => 'toilettenpapier', + 'tolino' => 'tolino', + 'Tomb Raider' => 'tomb-raider', + 'Tom Clancy's' => 'tom-clancys', + 'Tom Clancy's: Ghost Recon Wildlands' => 'tom-clancys-ghost-recon-wildlands', + 'Tom Clancy's Ghost Recon Breakpoint' => 'tom-clancys-ghost-recon-breakpoint', + 'Tom Clancy's The Division 2' => 'tom-clancy-the-division-2', + 'Tommy Hilfiger' => 'tommy-hilfiger', + 'TOM TAILOR' => 'tom-tailor', + 'Toner' => 'toner', + 'Tonic Water' => 'tonic-water', + 'Toniebox' => 'toniebox', + 'Tonies Figuren' => 'tonie-figuren', + 'Töpfe' => 'toepfe', + 'Töpfe & Pfannen' => 'kochen', + 'Toplader' => 'toplader', + 'Toshiba' => 'toshiba', + 'Total War' => 'total-war', + 'Toyota' => 'toyota', + 'TP-Link' => 'tp-link', + 'TP-Link Router' => 'tp-link-router', + 'Trampoline' => 'trampolin', + 'TREKSTOR' => 'trekstor', + 'Trockner' => 'trockner', + 'Tropical Islands' => 'tropical-island', + 'Tropico' => 'tropico', + 'Tropico 5' => 'tropico-5', + 'Tropico 6' => 'tropico-6', + 'TV & Video' => 'tv-video', + 'TV Boxen' => 'tv-box', + 'TV Spielfilm' => 'tv-spielfilm', + 'TV Wandhalterungen' => 'tv-wandhalterung', + 'TV Zubehör' => 'tv-zubehoer', + 'Übergangsjacken' => 'uebergangsjacken', + 'Überwachungskamera' => 'ueberwachungskamera', + 'UE BLAST' => 'ue-blast', + 'UE BOOM' => 'ue-boom', + 'UE BOOM 2' => 'ue-boom-2', + 'UE BOOM 3' => 'ue-boom-3', + 'UE MEGABLAST' => 'ue-megablast', + 'UE MEGABOOM' => 'ue-megaboom', + 'UE MEGABOOM 3' => 'ue-megaboom-3', + 'UE WONDERBOOM' => 'ue-wonderboom', + 'UE WONDERBOOM 2' => 'ue-wonderboom-2', + 'UGG' => 'ugg', + 'Uhren' => 'uhren', + 'Umstandsmode' => 'umstandsmode', + 'Uncharted' => 'uncharted', + 'Uncharted 4' => 'uncharted-4', + 'Uncharted: The Lost Legacy' => 'uncharted-the-lost-legacy', + 'Under Armour' => 'under-armour', + 'Universalfernbedienungen' => 'universalfernbedienungen', + 'Unterwäsche' => 'unterwaesche', + 'Uplay' => 'uplay', + 'Urban Sport' => 'urban-sport', + 'Urlaub' => 'urlaub', + 'USB Sticks' => 'usb-stick', + 'Vakuumierer' => 'vakuumierer', + 'Vans' => 'vans', + 'Vans Old Skool' => 'vans-old-skool', + 'Vans Schuhe' => 'vans-schuhe', + 'Vaude' => 'vaude', + 'Ventilatoren' => 'ventilator', + 'Verbandskästen' => 'verbandskaesten', + 'Versicherung' => 'versicherung', + 'Versicherung & Finanzen' => 'vertraege-finanzen', + 'Videobearbeitungsprogramme' => 'videobearbeitungsprogramme', + 'Video Player' => 'video-player', + 'Videospiele' => 'videospiele', + 'Video Streaming' => 'video-streaming', + 'Vileda' => 'vileda', + 'Villeroy & Boch' => 'villeroy-boch', + 'Virenschutz' => 'virenschutz', + 'VISA' => 'visa', + 'Vliestapeten' => 'vliestapete', + 'Vodafone' => 'vodafone-netz', + 'Vodka' => 'vodka', + 'Volvo' => 'volvo', + 'Vorratsdosen' => 'vorratsdosen', + 'Vorstellungsrunde' => 'vorstellungsrunde', + 'VPN' => 'vpn', + 'VPS' => 'vps', + 'VR Brillen' => 'vr-brille', + 'VR Spiele' => 'vr-spiele', + 'VTech' => 'vtech', + 'VW' => 'vw', + 'Waffeleisen' => 'waffeleisen', + 'Wandbilder' => 'wandtattoos', + 'Wanderrucksäcke' => 'wanderrucksack', + 'Wanderschuhe' => 'wanderschuhe', + 'Wandersport' => 'hiking', + 'Wandfarben' => 'wandfarben', + 'Wandlampen' => 'wandlampen', + 'Wäscheständer' => 'waeschestaender', + 'Waschmaschinen' => 'waschmaschinen', + 'Waschmittel' => 'waschmittel', + 'Waschtrockner' => 'waschtrockner', + 'Wasserfilter' => 'wasserfilter', + 'Wasserkocher' => 'wasserkocher', + 'Wasserkühlung' => 'wasserkuehlung', + 'Wasserspielzeuge' => 'wasserspielzeug', + 'Wassersport' => 'wassersport', + 'Watch Dogs' => 'watch-dogs', + 'Watch Dogs 2' => 'watch-dogs-2', + 'Watch Dogs: Legion' => 'watch-dogs-legion', + 'WC Sitze' => 'wc-sitze', + 'WD-40' => 'wd-40', + 'Wearables' => 'wearable', + 'Webcams' => 'webcam', + 'Weber Gasgrills' => 'weber-gasgrill', + 'Weber Grills' => 'weber-grill', + 'Weihnachtsbäume' => 'weihnachtsbaum', + 'Weihnachtsbeleuchtung' => 'weihnachtsbeleuchtung', + 'Weihnachtsdeko' => 'weihnachtsdeko', + 'Weihnachtspullover' => 'weihnachtspullover', + 'Wein' => 'wein', + 'Wellensteyn' => 'wellensteyn', + 'Wellness & Gesundheit' => 'wellness-massagen', + 'Wera' => 'wera', + 'Werkstatt & Service' => 'werkstatt-service', + 'Werkstatteinrichtungen' => 'werkstatteinrichtungen', + 'Werkzeuge' => 'werkzeug', + 'Werkzeugkoffer' => 'werkzeugkoffer', + 'Wesco Mülleimer' => 'wesco-muelleimer', + 'Western Digital' => 'western-digital', + 'Wetterstationen' => 'wetterstationen', + 'Whirlpools' => 'whirlpools', + 'Whisky' => 'whisky', + 'Wiko' => 'wiko', + 'Wilkinson Sword Rasierer' => 'wilkinson-sword', + 'Windeln' => 'windeln', + 'Winkelschleifer' => 'winkelschleifer', + 'Winterdeko' => 'winterdeko', + 'Winterjacken' => 'winterjacken', + 'Winterreifen' => 'winterreifen', + 'Winterstiefel' => 'winterstiefel', + 'Wireless Charger' => 'wireless-charger', + 'Wirtschaftswoche' => 'wirtschaftswoche', + 'WMF' => 'wmf', + 'WMF Besteck' => 'wmf-besteck', + 'WMF Topfset' => 'wmf-topfset', + 'Wohnzimmermöbel' => 'wohnzimmer', + 'Wolfenstein' => 'wolfenstein', + 'Wolfenstein II: The New Colossus' => 'wolfenstein-2-the-new-colossus', + 'Womanizer' => 'womanizer', + 'World of Warcraft' => 'world-of-warcraft', + 'Wrangler' => 'wrangler', + 'X570 Mainboard' => 'x570-mainboard', + 'Xbox' => 'xbox', + 'Xbox Controller' => 'xbox-controller', + 'Xbox Elite Wireless Controller' => 'xbox-one-elite-controller', + 'Xbox Elite Wireless Controller 2' => 'xbox-one-elite-controller-2', + 'Xbox Game Pass' => 'xbox-game-pass', + 'Xbox Game Pass Ultimate' => 'xbox-game-pass-ultimate', + 'Xbox Guthaben' => 'xbox-guthaben', + 'Xbox Live Gold' => 'xbox-live', + 'Xbox One Controller' => 'xbox-one-controller', + 'Xbox One S Konsolen' => 'xbox-one-s', + 'Xbox One Spiele' => 'xbox-one-spiele', + 'Xbox One X Konsolen' => 'xbox-one-x', + 'Xbox Series S Konsolen' => 'xbox-series-s', + 'Xbox Series X Controller' => 'xbox-series-x-controller', + 'Xbox Series X Konsolen' => 'xbox-series-x', + 'Xbox Series X Spiele' => 'xbox-series-x-spiele', + 'Xbox Wireless Headset' => 'xbox-wireless-headset', + 'Xbox Zubehör' => 'xbox-zubehoer', + 'Xiaomi' => 'xiaomi', + 'Xiaomi Air Laptop' => 'xiaomi-air', + 'Xiaomi E-Scooter' => 'xiaomi-e-scooter', + 'Xiaomi Fernseher' => 'xiaomi-fernseher', + 'Xiaomi Kopfhörer' => 'xiaomi-kopfhoerer', + 'Xiaomi Mi 5S' => 'xiaomi-mi-5', + 'Xiaomi Mi 6' => 'xiaomi-mi-6', + 'Xiaomi Mi 8' => 'xiaomi-mi-8', + 'Xiaomi Mi 8 Lite' => 'xiaomi-mi-8-lite', + 'Xiaomi Mi 8 Pro' => 'xiaomi-mi-8-pro', + 'Xiaomi Mi 9' => 'xiaomi-mi-9', + 'Xiaomi Mi 9 Lite' => 'xiaomi-mi-9-lite', + 'Xiaomi Mi 9 SE' => 'xiaomi-mi-9-se', + 'Xiaomi Mi 9T' => 'xiaomi-mi-9t', + 'Xiaomi Mi 9T Pro' => 'xiaomi-mi-9t-pro', + 'Xiaomi Mi 10' => 'xiaomi-mi-10', + 'Xiaomi Mi 10 Lite' => 'xiaomi-mi-10-lite', + 'Xiaomi Mi 10 Pro' => 'xiaomi-mi-10-pro', + 'Xiaomi Mi 11' => 'xiaomi-mi-11', + 'Xiaomi Mi A1' => 'xiaomi-mi-a1', + 'Xiaomi Mi A2' => 'xiaomi-mi-a2', + 'Xiaomi Mi AirDots' => 'xiaomi-mi-airdots', + 'Xiaomi Mi AirDots Pro' => 'xiaomi-airdots-pro', + 'Xiaomi Mi Band' => 'xiaomi-mi-band', + 'Xiaomi Mi Band 4' => 'xiaomi-mi-band-4', + 'Xiaomi Mi Band 5' => 'xiaomi-mi-band-5', + 'Xiaomi Mi Electric Scooter 1S' => 'xiaomi-mi-scooter-1s', + 'Xiaomi Mi Electric Scooter M365' => 'xiaomi-mi-electric-scooter-m365', + 'Xiaomi Mi Electric Scooter Pro 2' => 'xiaomi-mi-electric-scooter-pro-2', + 'Xiaomi Mi Mix' => 'xiaomi-mi-mix', + 'Xiaomi Mi Mix 3' => 'xiaomi-mi-mix-3', + 'Xiaomi Mi Note' => 'xiaomi-mi-note', + 'Xiaomi Mi Note 10' => 'xiaomi-mi-note-10', + 'Xiaomi Mi Note 10 Lite' => 'xiaomi-mi-note-10-lite', + 'Xiaomi Mi Note 10 Pro' => 'xiaomi-mi-note-10-pro', + 'Xiaomi Mi TV 4S' => 'xiaomi-mi-smart-tv-4s', + 'Xiaomi Mi TV Stick' => 'xiaomi-mi-tv-stick', + 'Xiaomi Pocophone F1' => 'xiaomi-pocophone-f1', + 'Xiaomi Redmi 9' => 'xiaomi-redmi-9', + 'Xiaomi Redmi 9A' => 'xiaomi-redmi-9a', + 'Xiaomi Redmi AirDots' => 'xiaomi-redmi-airdots', + 'Xiaomi Redmi Note 4' => 'xiaomi-redmi-note-4', + 'Xiaomi Redmi Note 5' => 'xiaomi-redmi-note-5', + 'Xiaomi Redmi Note 8' => 'xiaomi-redmi-note-8', + 'Xiaomi Redmi Note 8 Pro' => 'xiaomi-redmi-note-8-pro', + 'Xiaomi Redmi Note 9' => 'xiaomi-redmi-note-9', + 'Xiaomi Redmi Note 9 Pro' => 'xiaomi-redmi-note-9-pro', + 'Xiaomi Redmi Note 9S' => 'xiaomi-redmi-note-9s', + 'Xiaomi Redmi Note 10' => 'xiaomi-redmi-note-10', + 'Xiaomi Redmi Note 10 Pro' => 'xiaomi-redmi-note-10-pro', + 'Xiaomi Smart Home' => 'xiaomi-smart-home', + 'Xiaomi Smartphones' => 'xiaomi-smartphones', + 'Xiaomi YouPin' => 'xiaomi-youpin', + 'XMG' => 'xmg', + 'Yamaha' => 'yamaha', + 'Yeelight' => 'xiaomi-yeelight', + 'Yoga' => 'yoga', + 'Yogamatten' => 'yogamatten', + 'Yoshi's Crafted World' => 'yoshis-crafted-world', + 'Zahnbürsten' => 'zahnbuersten', + 'Zahnpasta' => 'zahnpasta', + 'Zahnzusatzversicherung' => 'zahnzusatzversicherung', + 'Zeitschriften' => 'zeitschriften-magazine', + 'Zelte' => 'zelte', + 'Zirkel' => 'zirkel', + 'Zoo-Tickets' => 'zoo', + 'Zotac' => 'zotac', + 'ZTE Smartphones' => 'zte-smartphones', + 'ZWILLING' => 'zwilling', + 'ZWILLING Besteck' => 'zwilling-besteck', + ] + ], + 'order' => [ + 'name' => 'sortieren nach', + 'type' => 'list', + 'title' => 'Sortierung der deals', + 'values' => [ + 'Vom heißesten zum kältesten Deal' => '-hot', + 'Vom jüngsten Deal zum ältesten' => '-new', + ] + ], + ], + 'Überwachung Diskussion' => [ + 'url' => [ + 'name' => 'URL der Diskussion', + 'type' => 'text', + 'required' => true, + 'title' => 'URL-Diskussion zu überwachen: https://www.mydealz.de/diskussion/title-123', + 'exampleValue' => 'https://www.mydealz.de/diskussion/anleitung-wie-schreibe-ich-einen-deal-1658317', + ], + 'only_with_url' => [ + 'name' => 'Kommentare ohne URL ausschließen', + 'type' => 'checkbox', + 'title' => 'Kommentare, die keine URL enthalten, im Feed ausschließen', + 'defaultValue' => false, + ] + ] + ]; + public $lang = [ + 'bridge-uri' => self::URI, + 'bridge-name' => self::NAME, + 'context-keyword' => 'Suche nach Stichworten', + 'context-group' => 'Deals pro Gruppen', + 'context-talk' => 'Überwachung Diskussion', + 'uri-group' => 'gruppe/', + 'request-error' => 'Could not request mydeals', + 'thread-error' => 'Die ID der Diskussion kann nicht ermittelt werden. Überprüfen Sie die eingegebene URL', + 'no-results' => 'Ups, wir konnten keine Deals zu', + 'relative-date-indicator' => [ + 'vor', + 'seit' + ], + 'price' => 'Preis', + 'shipping' => 'Versand', + 'origin' => 'Ursprung', + 'discount' => 'Rabatte', + 'title-keyword' => 'Suche', + 'title-group' => 'Gruppe', + 'title-talk' => 'Überwachung Diskussion', + 'local-months' => [ + 'Jan', + 'Feb', + 'Mär', + 'Apr', + 'Mai', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Okt', + 'Nov', + 'Dez', + '.' + ], + 'local-time-relative' => [ + 'eingestellt vor ', + 'm', + 'h,', + 'day', + 'days', + 'month', + 'year', + 'and ' + ], + 'date-prefixes' => [ + 'eingestellt am ', + 'lokal ', + 'aktualisiert ', + ], + 'relative-date-alt-prefixes' => [ + 'aktualisiert vor ', + 'kommentiert vor ', + 'heiß seit ' + ], + 'relative-date-ignore-suffix' => [ + '/von.*$/' + ], + 'localdeal' => [ + 'Lokal ', + 'Läuft bis ' + ] + ]; } diff --git a/bridges/N26Bridge.php b/bridges/N26Bridge.php index 5ca78224..8865600d 100644 --- a/bridges/N26Bridge.php +++ b/bridges/N26Bridge.php @@ -2,42 +2,42 @@ class N26Bridge extends BridgeAbstract { - const MAINTAINER = 'quentinus95'; - const NAME = 'N26 Blog'; - const URI = 'https://n26.com'; - const CACHE_TIMEOUT = 1800; - const DESCRIPTION = 'Returns recent blog posts from N26.'; + const MAINTAINER = 'quentinus95'; + const NAME = 'N26 Blog'; + const URI = 'https://n26.com'; + const CACHE_TIMEOUT = 1800; + const DESCRIPTION = 'Returns recent blog posts from N26.'; - public function collectData() - { - $limit = 5; - $url = 'https://n26.com/en-eu/blog/all'; - $html = getSimpleHTMLDOM($url); + public function collectData() + { + $limit = 5; + $url = 'https://n26.com/en-eu/blog/all'; + $html = getSimpleHTMLDOM($url); - $articles = $html->find('div[class="bl bm"]'); + $articles = $html->find('div[class="bl bm"]'); - foreach($articles as $article) { - $item = array(); + foreach ($articles as $article) { + $item = []; - $itemUrl = self::URI . $article->find('a', 1)->href; - $item['uri'] = $itemUrl; + $itemUrl = self::URI . $article->find('a', 1)->href; + $item['uri'] = $itemUrl; - $item['title'] = $article->find('a', 1)->plaintext; + $item['title'] = $article->find('a', 1)->plaintext; - $fullArticle = getSimpleHTMLDOM($item['uri']); + $fullArticle = getSimpleHTMLDOM($item['uri']); - $createdAt = $fullArticle->find('time', 0); - $item['timestamp'] = strtotime($createdAt->plaintext); + $createdAt = $fullArticle->find('time', 0); + $item['timestamp'] = strtotime($createdAt->plaintext); - $this->items[] = $item; - if (count($this->items) >= $limit) { - break; - } - } - } + $this->items[] = $item; + if (count($this->items) >= $limit) { + break; + } + } + } - public function getIcon() - { - return 'https://n26.com/favicon.ico'; - } + public function getIcon() + { + return 'https://n26.com/favicon.ico'; + } } diff --git a/bridges/NFLRUSBridge.php b/bridges/NFLRUSBridge.php index c9a92379..18e067a8 100644 --- a/bridges/NFLRUSBridge.php +++ b/bridges/NFLRUSBridge.php @@ -1,27 +1,28 @@ <?php -class NFLRUSBridge extends BridgeAbstract { +class NFLRUSBridge extends BridgeAbstract +{ + const NAME = 'NFLRUS'; + const URI = 'http://nflrus.ru/'; + const DESCRIPTION = 'Returns the recent articles published on nflrus.ru'; + const MAINTAINER = 'Maxim Shpak'; - const NAME = 'NFLRUS'; - const URI = 'http://nflrus.ru/'; - const DESCRIPTION = 'Returns the recent articles published on nflrus.ru'; - const MAINTAINER = 'Maxim Shpak'; + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); + $html = defaultLinkTo($html, self::URI); - public function collectData() { - $html = getSimpleHTMLDOM(self::URI); - $html = defaultLinkTo($html, self::URI); + $articles = $html->find('.big-post_content-col'); - $articles = $html->find('.big-post_content-col'); + foreach ($articles as $article) { + $item = []; - foreach($articles as $article) { - $item = array(); + $url = $article->find('.big-post_title.card-title a', 0); - $url = $article->find('.big-post_title.card-title a', 0); - - $item['uri'] = $url->href; - $item['title'] = $url->plaintext; - $item['content'] = $article->find('div', 0)->innertext; - $this->items[] = $item; - } - } + $item['uri'] = $url->href; + $item['title'] = $url->plaintext; + $item['content'] = $article->find('div', 0)->innertext; + $this->items[] = $item; + } + } } diff --git a/bridges/NYTBridge.php b/bridges/NYTBridge.php index 15fded3a..87404c4d 100644 --- a/bridges/NYTBridge.php +++ b/bridges/NYTBridge.php @@ -1,37 +1,41 @@ <?php -class NYTBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'New York Times Bridge'; - const URI = 'https://www.nytimes.com/'; - const CACHE_TIMEOUT = 900; // 15 minutes - const DESCRIPTION = 'RSS feed for the New York Times'; - - public function collectData(){ - $this->collectExpandableDatas('https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', 40); - } - - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $article = ''; - - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); - - // handle subtitle - $subtitle = $articlePage->find('p.css-w6ymp8', 0); - if ($subtitle != null) { - $article .= '<strong>' . $subtitle->plaintext . '</strong>'; - } - - // figure contain's the main article image - $article .= $articlePage->find('figure', 0) . '<br />'; - - // section.meteredContent has the actual article - foreach($articlePage->find('section.meteredContent p') as $element) - $article .= '' . $element . ''; - - $item['content'] = $article; - return $item; - } +class NYTBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'New York Times Bridge'; + const URI = 'https://www.nytimes.com/'; + const CACHE_TIMEOUT = 900; // 15 minutes + const DESCRIPTION = 'RSS feed for the New York Times'; + + public function collectData() + { + $this->collectExpandableDatas('https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', 40); + } + + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $article = ''; + + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + + // handle subtitle + $subtitle = $articlePage->find('p.css-w6ymp8', 0); + if ($subtitle != null) { + $article .= '<strong>' . $subtitle->plaintext . '</strong>'; + } + + // figure contain's the main article image + $article .= $articlePage->find('figure', 0) . '<br />'; + + // section.meteredContent has the actual article + foreach ($articlePage->find('section.meteredContent p') as $element) { + $article .= '' . $element . ''; + } + + $item['content'] = $article; + return $item; + } } diff --git a/bridges/NasaApodBridge.php b/bridges/NasaApodBridge.php index f23f6da9..5aff63aa 100644 --- a/bridges/NasaApodBridge.php +++ b/bridges/NasaApodBridge.php @@ -1,46 +1,47 @@ <?php -class NasaApodBridge extends BridgeAbstract { - - const MAINTAINER = 'corenting'; - const NAME = 'NASA APOD Bridge'; - const URI = 'https://apod.nasa.gov/apod/'; - const CACHE_TIMEOUT = 43200; // 12h - const DESCRIPTION = 'Returns the 3 latest NASA APOD pictures and explanations'; - - public function collectData(){ - - $html = getSimpleHTMLDOM(self::URI . 'archivepix.html'); - - // Start at 1 to skip the "APOD Full Archive" on top of the page - for($i = 1; $i < 4; $i++) { - $item = array(); - - $uri_page = $html->find('a', $i + 3)->href; - $uri = self::URI . $uri_page; - $item['uri'] = $uri; - - $picture_html = getSimpleHTMLDOM($uri); - $picture_html_string = $picture_html->innertext; - - //Extract image and explanation - $image_wrapper = $picture_html->find('a', 1); - $image_path = $image_wrapper->href; - $img_placeholder = $image_wrapper->find('img', 0); - $img_alt = $img_placeholder->alt; - $img_style = $img_placeholder->style; - $image_uri = self::URI . $image_path; - $new_img_placeholder = "<img src=\"$image_uri\" alt=\"$img_alt\" style=\"$img_style\">"; - $media = "<a href=\"$image_uri\">$new_img_placeholder</a>"; - $explanation = $picture_html->find('p', 2)->innertext; - - //Extract date from the picture page - $date = explode(' ', $picture_html->find('p', 1)->innertext); - $item['timestamp'] = strtotime($date[4] . $date[3] . $date[2]); - - //Other informations - $item['content'] = $media . '<br />' . $explanation; - $item['title'] = $picture_html->find('b', 0)->innertext; - $this->items[] = $item; - } - } + +class NasaApodBridge extends BridgeAbstract +{ + const MAINTAINER = 'corenting'; + const NAME = 'NASA APOD Bridge'; + const URI = 'https://apod.nasa.gov/apod/'; + const CACHE_TIMEOUT = 43200; // 12h + const DESCRIPTION = 'Returns the 3 latest NASA APOD pictures and explanations'; + + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI . 'archivepix.html'); + + // Start at 1 to skip the "APOD Full Archive" on top of the page + for ($i = 1; $i < 4; $i++) { + $item = []; + + $uri_page = $html->find('a', $i + 3)->href; + $uri = self::URI . $uri_page; + $item['uri'] = $uri; + + $picture_html = getSimpleHTMLDOM($uri); + $picture_html_string = $picture_html->innertext; + + //Extract image and explanation + $image_wrapper = $picture_html->find('a', 1); + $image_path = $image_wrapper->href; + $img_placeholder = $image_wrapper->find('img', 0); + $img_alt = $img_placeholder->alt; + $img_style = $img_placeholder->style; + $image_uri = self::URI . $image_path; + $new_img_placeholder = "<img src=\"$image_uri\" alt=\"$img_alt\" style=\"$img_style\">"; + $media = "<a href=\"$image_uri\">$new_img_placeholder</a>"; + $explanation = $picture_html->find('p', 2)->innertext; + + //Extract date from the picture page + $date = explode(' ', $picture_html->find('p', 1)->innertext); + $item['timestamp'] = strtotime($date[4] . $date[3] . $date[2]); + + //Other informations + $item['content'] = $media . '<br />' . $explanation; + $item['title'] = $picture_html->find('b', 0)->innertext; + $this->items[] = $item; + } + } } diff --git a/bridges/NationalGeographicBridge.php b/bridges/NationalGeographicBridge.php index e5273a8e..a7bb947a 100644 --- a/bridges/NationalGeographicBridge.php +++ b/bridges/NationalGeographicBridge.php @@ -1,334 +1,350 @@ <?php -class NationalGeographicBridge extends BridgeAbstract { - - const CONTEXT_BY_TOPIC = 'By Topic'; - const PARAMETER_TOPIC = 'topic'; - const PARAMETER_FULL_ARTICLE = 'full'; - const TOPIC_MAGAZINE = 'Magazine'; - const TOPIC_LATEST_STORIES = 'Latest Stories'; - const CACHE_TIMEOUT = 900; //15 min - - const NAME = 'National Geographic'; - const URI = 'https://www.nationalgeographic.com/'; - const DESCRIPTION = 'Fetches the latest articles from the National Geographic Magazine'; - const MAINTAINER = 'csisoap'; - const PARAMETERS = array( - self::CONTEXT_BY_TOPIC => array( - self::PARAMETER_TOPIC => array( - 'name' => 'Topic', - 'type' => 'list', - 'values' => array( - self::TOPIC_MAGAZINE => 'magazine', - self::TOPIC_LATEST_STORIES => 'latest-stories' - ), - 'title' => 'Select your topic', - 'defaultValue' => 'Magazine' - ) - ), - 'global' => array( - self::PARAMETER_FULL_ARTICLE => array( - 'name' => 'Full Article', - 'type' => 'checkbox', - 'title' => 'Enable to load full articles and other infos (takes longer)' - ) - ) - ); - - private $topicName = ''; - const CONTEXT = 'eyJjb250ZW50VHlwZSI6IlVuaXNvbkh1YiIsInZhcmlhYmxlcyI6eyJsb2NhdG9yIjoiL3BhZ2VzL3 + +class NationalGeographicBridge extends BridgeAbstract +{ + const CONTEXT_BY_TOPIC = 'By Topic'; + const PARAMETER_TOPIC = 'topic'; + const PARAMETER_FULL_ARTICLE = 'full'; + const TOPIC_MAGAZINE = 'Magazine'; + const TOPIC_LATEST_STORIES = 'Latest Stories'; + const CACHE_TIMEOUT = 900; //15 min + + const NAME = 'National Geographic'; + const URI = 'https://www.nationalgeographic.com/'; + const DESCRIPTION = 'Fetches the latest articles from the National Geographic Magazine'; + const MAINTAINER = 'csisoap'; + const PARAMETERS = [ + self::CONTEXT_BY_TOPIC => [ + self::PARAMETER_TOPIC => [ + 'name' => 'Topic', + 'type' => 'list', + 'values' => [ + self::TOPIC_MAGAZINE => 'magazine', + self::TOPIC_LATEST_STORIES => 'latest-stories' + ], + 'title' => 'Select your topic', + 'defaultValue' => 'Magazine' + ] + ], + 'global' => [ + self::PARAMETER_FULL_ARTICLE => [ + 'name' => 'Full Article', + 'type' => 'checkbox', + 'title' => 'Enable to load full articles and other infos (takes longer)' + ] + ] + ]; + + private $topicName = ''; + const CONTEXT = 'eyJjb250ZW50VHlwZSI6IlVuaXNvbkh1YiIsInZhcmlhYmxlcyI6eyJsb2NhdG9yIjoiL3BhZ2VzL3 RvcGljL2xhdGVzdC1zdG9yaWVzIiwicG9ydGZvbGlvIjoibmF0Z2VvIiwicXVlcn lUeXBlIjoiTE9DQVRPUiJ9LCJtb2R1bGVJZCI6bnVsbH0'; - const LATEST_STORIES_ID = array( - '1df278bb-0e3d-4a67-a0ce-8fae48392822-f2-m1' - ); - const MAGAZINE_ID = array( - '94d87d74-f41a-4a32-9acd-b591ba2df288-f2-m1', - '94d87d74-f41a-4a32-9acd-b591ba2df288-f5-m2', - ); - - public function getURI() { - switch ($this->queriedContext) { - case self::CONTEXT_BY_TOPIC: - return self::URI . $this->getInput(self::PARAMETER_TOPIC); - default: - return parent::getURI(); - } - } - - private function getAPIURL($id) { - $context = preg_replace('/\s*/m', '', self::CONTEXT); - $url = 'https://www.nationalgeographic.com/proxy/hub?context=' - . $context . '&id=' . $id - . '&moduleType=InfiniteFeedModule&_xhr=pageContent'; - return $url; - } - - public function collectData() { - $this->topicName = $this->getTopicName($this->getInput(self::PARAMETER_TOPIC)); - switch($this->topicName) { - case self::TOPIC_MAGAZINE: - return $this->collectMagazine(); - case self::TOPIC_LATEST_STORIES: - return $this->collectLatestStories(); - default: - returnServerError('Unknown topic: "' . $this->topicName . '"'); - } - } - - public function getName() { - switch ($this->queriedContext) { - case self::CONTEXT_BY_TOPIC: - return static::NAME . ': ' . $this->topicName; - default: - return parent::getName(); - } - } - - private function getTopicName($topic) { - return array_search($topic, static::PARAMETERS[self::CONTEXT_BY_TOPIC][self::PARAMETER_TOPIC]['values']); - } - - private function collectMagazine() { - $stories = array(); - - foreach(self::MAGAZINE_ID as $id) { - $uri = $this->getAPIURL($id); - - $json_raw = getContents($uri); - - $json = json_decode($json_raw, true)['tiles']; - $stories = array_merge($json, $stories); - } - - foreach($stories as $story) { - $this->addStory($story); - } - } - - private function collectLatestStories() { - $stories = array(); - - foreach(self::LATEST_STORIES_ID as $id) { - $uri = $this->getAPIURL($id); - - $json_raw = getContents($uri); - - $json = json_decode($json_raw, true)['tiles']; - $stories = array_merge($stories, $json); - } - - foreach($stories as $story) { - $this->addStory($story); - } - } - - private function addStory($story) { - $title = 'Unknown title'; - $content = ''; - $story_type = ''; - $uri = ''; - - foreach($story['ctas'] as $component) { - $uri = $component['url']; - $story_type = $component['icon']; - } - - $item = array(); - if(isset($story['description'])) { - $content = '<p>' . $story['description'] . '</p>'; - } - $title = $story['title']; - $item['uri'] = $uri; - $item['title'] = $story['title']; - - // if full article is requested! - if ($this->getInput(self::PARAMETER_FULL_ARTICLE)) { - if($story_type != 'interactive') { - /* Nat Geo doesn't provided much info about interactive page - * and it requires JS to load the interactive. - */ - $article_data = $this->getFullArticle($item['uri']); - $item['timestamp'] = $article_data['published_date']; - $item['author'] = $article_data['authors']; - $item['content'] = $content . $article_data['content']; - } else { - $item['content'] = $content; - } - } else - $item['content'] = $content; - - $image = $story['img']; - $item['enclosures'][] = $image['src']; - - $tags = $story['tags']; - foreach($tags as $tag) { - $tag_name = $tag['name']; - $item['categories'][] = $tag_name; - } - - $this->items[] = $item; - } - - private function filterArticleData($data) { - $article_module = array_filter( - $data, function ($item) { - if(isset($item['id']) && $item['id'] == 'natgeo-template1-frame-1') { - return true; - } - } - ); - - $article_data = array_reduce( - $article_module, - function (array $carry, array $item) { - $module = $item['mods']; - return array_merge( - $carry, - array_filter( - $module, function ($data) { - return $data['id'] == 'natgeo-template1-frame-1-module-1'; - } - ) - ); - }, - array() - ); - - return $article_data[0]; - } - - private function handleImages($image_module, $image_type) { - $image_alt = ''; - $image_credit = ''; - $image_src = ''; - $image_caption = ''; - $caption = ''; - switch($image_type) { - case 'image': - case 'imagegroup': - $image = $image_module['image']; - $image_src = $image['src']; - if(isset($image_module['alt'])) { - $image_alt = $image_module['alt']; - } elseif(isset($image['altText'])) { - $image_alt = $image['altText']; - } - if(isset($image['crdt'])) { - $image_credit = $image['crdt']; - } - $caption = (isset($image_module['caption']) ? $image_module['caption'] : ''); - break; - case 'photogallery': - $image_credit = (isset($image_module['caption']['credit']) ? $image_module['caption']['credit'] : ''); - $caption = $image_module['caption']['text']; - $image_src = $image_module['img']['src']; - $image_alt = $image_module['img']['altText']; - break; - case 'video': - $image_credit = (isset($image_module['credit']) ? $image_module['credit'] : ''); - $description = (isset($image_module['description']) ? $image_module['description'] : ''); - $caption = $description . ' Video can be watched on the article\'s page'; - $image = $image_module['image']; - $image_alt = $image['altText']; - $image_src = $image['src']; - } - - $image_caption = $caption . ' ' . $image_credit - . '. Notes: Some image may have copyrighted on it.'; - $wrapper = <<<EOD + const LATEST_STORIES_ID = [ + '1df278bb-0e3d-4a67-a0ce-8fae48392822-f2-m1' + ]; + const MAGAZINE_ID = [ + '94d87d74-f41a-4a32-9acd-b591ba2df288-f2-m1', + '94d87d74-f41a-4a32-9acd-b591ba2df288-f5-m2', + ]; + + public function getURI() + { + switch ($this->queriedContext) { + case self::CONTEXT_BY_TOPIC: + return self::URI . $this->getInput(self::PARAMETER_TOPIC); + default: + return parent::getURI(); + } + } + + private function getAPIURL($id) + { + $context = preg_replace('/\s*/m', '', self::CONTEXT); + $url = 'https://www.nationalgeographic.com/proxy/hub?context=' + . $context . '&id=' . $id + . '&moduleType=InfiniteFeedModule&_xhr=pageContent'; + return $url; + } + + public function collectData() + { + $this->topicName = $this->getTopicName($this->getInput(self::PARAMETER_TOPIC)); + switch ($this->topicName) { + case self::TOPIC_MAGAZINE: + return $this->collectMagazine(); + case self::TOPIC_LATEST_STORIES: + return $this->collectLatestStories(); + default: + returnServerError('Unknown topic: "' . $this->topicName . '"'); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case self::CONTEXT_BY_TOPIC: + return static::NAME . ': ' . $this->topicName; + default: + return parent::getName(); + } + } + + private function getTopicName($topic) + { + return array_search($topic, static::PARAMETERS[self::CONTEXT_BY_TOPIC][self::PARAMETER_TOPIC]['values']); + } + + private function collectMagazine() + { + $stories = []; + + foreach (self::MAGAZINE_ID as $id) { + $uri = $this->getAPIURL($id); + + $json_raw = getContents($uri); + + $json = json_decode($json_raw, true)['tiles']; + $stories = array_merge($json, $stories); + } + + foreach ($stories as $story) { + $this->addStory($story); + } + } + + private function collectLatestStories() + { + $stories = []; + + foreach (self::LATEST_STORIES_ID as $id) { + $uri = $this->getAPIURL($id); + + $json_raw = getContents($uri); + + $json = json_decode($json_raw, true)['tiles']; + $stories = array_merge($stories, $json); + } + + foreach ($stories as $story) { + $this->addStory($story); + } + } + + private function addStory($story) + { + $title = 'Unknown title'; + $content = ''; + $story_type = ''; + $uri = ''; + + foreach ($story['ctas'] as $component) { + $uri = $component['url']; + $story_type = $component['icon']; + } + + $item = []; + if (isset($story['description'])) { + $content = '<p>' . $story['description'] . '</p>'; + } + $title = $story['title']; + $item['uri'] = $uri; + $item['title'] = $story['title']; + + // if full article is requested! + if ($this->getInput(self::PARAMETER_FULL_ARTICLE)) { + if ($story_type != 'interactive') { + /* Nat Geo doesn't provided much info about interactive page + * and it requires JS to load the interactive. + */ + $article_data = $this->getFullArticle($item['uri']); + $item['timestamp'] = $article_data['published_date']; + $item['author'] = $article_data['authors']; + $item['content'] = $content . $article_data['content']; + } else { + $item['content'] = $content; + } + } else { + $item['content'] = $content; + } + + $image = $story['img']; + $item['enclosures'][] = $image['src']; + + $tags = $story['tags']; + foreach ($tags as $tag) { + $tag_name = $tag['name']; + $item['categories'][] = $tag_name; + } + + $this->items[] = $item; + } + + private function filterArticleData($data) + { + $article_module = array_filter( + $data, + function ($item) { + if (isset($item['id']) && $item['id'] == 'natgeo-template1-frame-1') { + return true; + } + } + ); + + $article_data = array_reduce( + $article_module, + function (array $carry, array $item) { + $module = $item['mods']; + return array_merge( + $carry, + array_filter( + $module, + function ($data) { + return $data['id'] == 'natgeo-template1-frame-1-module-1'; + } + ) + ); + }, + [] + ); + + return $article_data[0]; + } + + private function handleImages($image_module, $image_type) + { + $image_alt = ''; + $image_credit = ''; + $image_src = ''; + $image_caption = ''; + $caption = ''; + switch ($image_type) { + case 'image': + case 'imagegroup': + $image = $image_module['image']; + $image_src = $image['src']; + if (isset($image_module['alt'])) { + $image_alt = $image_module['alt']; + } elseif (isset($image['altText'])) { + $image_alt = $image['altText']; + } + if (isset($image['crdt'])) { + $image_credit = $image['crdt']; + } + $caption = (isset($image_module['caption']) ? $image_module['caption'] : ''); + break; + case 'photogallery': + $image_credit = (isset($image_module['caption']['credit']) ? $image_module['caption']['credit'] : ''); + $caption = $image_module['caption']['text']; + $image_src = $image_module['img']['src']; + $image_alt = $image_module['img']['altText']; + break; + case 'video': + $image_credit = (isset($image_module['credit']) ? $image_module['credit'] : ''); + $description = (isset($image_module['description']) ? $image_module['description'] : ''); + $caption = $description . ' Video can be watched on the article\'s page'; + $image = $image_module['image']; + $image_alt = $image['altText']; + $image_src = $image['src']; + } + + $image_caption = $caption . ' ' . $image_credit + . '. Notes: Some image may have copyrighted on it.'; + $wrapper = <<<EOD <figure> <img src="{$image_src}" alt="{$image_alt}"> <figcaption>$image_caption</figcaption> </figure> EOD; - return $wrapper; - } - - private function getFullArticle($uri) { - $html = getContents($uri); - - $scriptRegex = '/window\[\'__natgeo__\'\]=(.*);<\/script>/'; - - preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0); - - $json = json_decode($matches[1][0], true); - - $unfiltered_data = $json['page']['content']['article']['frms']; - $filtered_data = $this->filterArticleData($unfiltered_data); - - $article = $filtered_data['edgs'][0]; - - $contributors = $article['cntrbGrp']; - $authors = array(); - if(count($contributors) > 0) { - $authors = $contributors[0]['contributors']; - } - - $authors_name = ''; - $counter = 0; - foreach($authors as $author) { - $counter++; - if($counter == count($authors)) { - $authors_name .= $author['displayName']; - } else { - $authors_name .= $author['displayName'] . ', '; - } - } - - $published_date = $article['pbDt']; - $article_body = $article['bdy']; - $content = ''; - - foreach($article_body as $body) { - switch($body['type']) { - case 'p': - $content .= '<p>' . $body['cntnt']['mrkup'] . '</p>'; - break; - case 'h2': - $content .= '<h2>' . $body['cntnt']['mrkup'] . '</h2>'; - break; - case 'inline': - $module = $body['cntnt']; - if(empty($module)) - continue 2; - switch($module['cmsType']) { - case 'image': - $content .= $this->handleImages($module, $module['cmsType']); - break; - case 'imagegroup': - $images = $module['images']; - foreach($images as $image) { - $content .= $this->handleImages($image, $module['cmsType']); - } - break; - case 'editorsNote': - $content .= $module['note']; - break; - case 'listicle': - $content .= '<h2>' . $module['title'] . '</h2>'; - if(isset($module['image'])) { - $content .= $this->handleImages($module['image'], $module['image']['cmsType']); - } - $content .= '<p>' . (isset($module['text']) ? $module['text'] : '') . '</p>'; - break; - case 'photogallery': - $gallery = $body['cntnt']['media']; - foreach($gallery as $image) { - $content .= $this->handleImages($image, $module['cmsType']); - } - break; - case 'video': - $content .= $this->handleImages($module, $module['cmsType']); - break; - case 'pullquote': - $quote = $module['quote']; - $author_name = ''; - $authors = (isset($module['byLineProps']['authors']) ? $module['byLineProps']['authors'] : array()); - foreach($authors as $author) { - $author_desc = (isset($author['authorDesc']) ? $author['authorDesc'] : ''); - $author_name .= $author['displayName'] . ', ' . $author_desc; - } - $content .= <<<EOD + return $wrapper; + } + + private function getFullArticle($uri) + { + $html = getContents($uri); + + $scriptRegex = '/window\[\'__natgeo__\'\]=(.*);<\/script>/'; + + preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0); + + $json = json_decode($matches[1][0], true); + + $unfiltered_data = $json['page']['content']['article']['frms']; + $filtered_data = $this->filterArticleData($unfiltered_data); + + $article = $filtered_data['edgs'][0]; + + $contributors = $article['cntrbGrp']; + $authors = []; + if (count($contributors) > 0) { + $authors = $contributors[0]['contributors']; + } + + $authors_name = ''; + $counter = 0; + foreach ($authors as $author) { + $counter++; + if ($counter == count($authors)) { + $authors_name .= $author['displayName']; + } else { + $authors_name .= $author['displayName'] . ', '; + } + } + + $published_date = $article['pbDt']; + $article_body = $article['bdy']; + $content = ''; + + foreach ($article_body as $body) { + switch ($body['type']) { + case 'p': + $content .= '<p>' . $body['cntnt']['mrkup'] . '</p>'; + break; + case 'h2': + $content .= '<h2>' . $body['cntnt']['mrkup'] . '</h2>'; + break; + case 'inline': + $module = $body['cntnt']; + if (empty($module)) { + continue 2; + } + switch ($module['cmsType']) { + case 'image': + $content .= $this->handleImages($module, $module['cmsType']); + break; + case 'imagegroup': + $images = $module['images']; + foreach ($images as $image) { + $content .= $this->handleImages($image, $module['cmsType']); + } + break; + case 'editorsNote': + $content .= $module['note']; + break; + case 'listicle': + $content .= '<h2>' . $module['title'] . '</h2>'; + if (isset($module['image'])) { + $content .= $this->handleImages($module['image'], $module['image']['cmsType']); + } + $content .= '<p>' . (isset($module['text']) ? $module['text'] : '') . '</p>'; + break; + case 'photogallery': + $gallery = $body['cntnt']['media']; + foreach ($gallery as $image) { + $content .= $this->handleImages($image, $module['cmsType']); + } + break; + case 'video': + $content .= $this->handleImages($module, $module['cmsType']); + break; + case 'pullquote': + $quote = $module['quote']; + $author_name = ''; + $authors = (isset($module['byLineProps']['authors']) ? $module['byLineProps']['authors'] : []); + foreach ($authors as $author) { + $author_desc = (isset($author['authorDesc']) ? $author['authorDesc'] : ''); + $author_name .= $author['displayName'] . ', ' . $author_desc; + } + $content .= <<<EOD <figure> <blockquote> <p>$quote</p> @@ -336,19 +352,19 @@ EOD; <figcaption>$author_name</figcaption> </figure> EOD; - break; - } - break; - case 'ul': - $content .= $body['cntnt']['mrkup'] . '<hr>'; - break; - } - } - - return array( - 'content' => $content, - 'published_date' => $published_date, - 'authors' => $authors_name - ); - } + break; + } + break; + case 'ul': + $content .= $body['cntnt']['mrkup'] . '<hr>'; + break; + } + } + + return [ + 'content' => $content, + 'published_date' => $published_date, + 'authors' => $authors_name + ]; + } } diff --git a/bridges/NewOnNetflixBridge.php b/bridges/NewOnNetflixBridge.php index 094038a8..43278fd9 100644 --- a/bridges/NewOnNetflixBridge.php +++ b/bridges/NewOnNetflixBridge.php @@ -1,58 +1,60 @@ <?php -class NewOnNetflixBridge extends BridgeAbstract { - const NAME = 'NewOnNetflix removals bridge'; - const URI = 'https://www.newonnetflix.info'; - const DESCRIPTION = 'Upcoming removals from Netflix (NewOnNetflix already provides additions as RSS)'; - const MAINTAINER = 'jdesgats'; - const PARAMETERS = array(array( - 'country' => array( - 'name' => 'Country', - 'type' => 'list', - 'values' => array( - 'Australia/New Zealand' => 'anz', - 'Canada' => 'can', - 'United Kingdom' => 'uk', - 'United States' => 'usa', - ), - 'defaultValue' => 'uk', - ) - )); - const CACHE_TIMEOUT = 3600 * 24; +class NewOnNetflixBridge extends BridgeAbstract +{ + const NAME = 'NewOnNetflix removals bridge'; + const URI = 'https://www.newonnetflix.info'; + const DESCRIPTION = 'Upcoming removals from Netflix (NewOnNetflix already provides additions as RSS)'; + const MAINTAINER = 'jdesgats'; + const PARAMETERS = [[ + 'country' => [ + 'name' => 'Country', + 'type' => 'list', + 'values' => [ + 'Australia/New Zealand' => 'anz', + 'Canada' => 'can', + 'United Kingdom' => 'uk', + 'United States' => 'usa', + ], + 'defaultValue' => 'uk', + ] + ]]; + const CACHE_TIMEOUT = 3600 * 24; - public function collectData() { - $baseURI = 'https://' . $this->getInput('country') . '.newonnetflix.info'; - $html = getSimpleHTMLDOMCached($baseURI . '/lastchance', self::CACHE_TIMEOUT); + public function collectData() + { + $baseURI = 'https://' . $this->getInput('country') . '.newonnetflix.info'; + $html = getSimpleHTMLDOMCached($baseURI . '/lastchance', self::CACHE_TIMEOUT); - foreach($html->find('article.oldpost') as $element) { - $title = $element->find('a.infopop[title]', 0); - $img = $element->find('img[lazy_src]', 0); - $date = $element->find('span[title]', 0); + foreach ($html->find('article.oldpost') as $element) { + $title = $element->find('a.infopop[title]', 0); + $img = $element->find('img[lazy_src]', 0); + $date = $element->find('span[title]', 0); - // format sholud be 'dd/mm/yy - dd/mm/yy' - // (the added date might be "unknown") - $fromTo = array(); - if (preg_match('/^\s*(.*?)\s*-\s*(.*?)\s*$/', $date->title, $fromTo)) { - $from = $fromTo[1]; - $to = $fromTo[2]; - } else { - $from = 'unknown'; - $to = 'unknown'; - } - $summary = <<<EOD + // format sholud be 'dd/mm/yy - dd/mm/yy' + // (the added date might be "unknown") + $fromTo = []; + if (preg_match('/^\s*(.*?)\s*-\s*(.*?)\s*$/', $date->title, $fromTo)) { + $from = $fromTo[1]; + $to = $fromTo[2]; + } else { + $from = 'unknown'; + $to = 'unknown'; + } + $summary = <<<EOD <img src="{$img->lazy_src}" loading="lazy"> <div>{$title->title}</div> <div><strong>Added on:</strong>$from</div> <div><strong>Removed on:</strong>$to</div> EOD; - $item = array(); - $item['uri'] = $baseURI . $title->href; - $item['title'] = $to . ' - ' . $title->plaintext; - $item['content'] = $summary; - // some movies are added and removed multiple times - $item['uid'] = $title->href . '-' . $to; - $this->items[] = $item; - } - } + $item = []; + $item['uri'] = $baseURI . $title->href; + $item['title'] = $to . ' - ' . $title->plaintext; + $item['content'] = $summary; + // some movies are added and removed multiple times + $item['uid'] = $title->href . '-' . $to; + $this->items[] = $item; + } + } } diff --git a/bridges/NewgroundsBridge.php b/bridges/NewgroundsBridge.php index f84ad8c0..fe956573 100644 --- a/bridges/NewgroundsBridge.php +++ b/bridges/NewgroundsBridge.php @@ -1,53 +1,54 @@ <?php + declare(strict_types=1); class NewgroundsBridge extends BridgeAbstract { - const NAME = 'Newgrounds'; - const URI = 'https://www.newgrounds.com'; - const DESCRIPTION = 'Get the latest art from a given user'; - const MAINTAINER = 'KamaleiZestri'; - const PARAMETERS = [ - 'User' => [ - 'username' => [ - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'TomFulp' - ] - ] - ]; + const NAME = 'Newgrounds'; + const URI = 'https://www.newgrounds.com'; + const DESCRIPTION = 'Get the latest art from a given user'; + const MAINTAINER = 'KamaleiZestri'; + const PARAMETERS = [ + 'User' => [ + 'username' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'TomFulp' + ] + ] + ]; - public function collectData() - { - $username = $this->getInput('username'); - if (!preg_match('/^\w+$/', $username)) { - throw new \Exception('Illegal username'); - } + public function collectData() + { + $username = $this->getInput('username'); + if (!preg_match('/^\w+$/', $username)) { + throw new \Exception('Illegal username'); + } - $html = getSimpleHTMLDOM($this->getURI()); + $html = getSimpleHTMLDOM($this->getURI()); - $posts = $html->find('.item-portalitem-art-medium'); + $posts = $html->find('.item-portalitem-art-medium'); - foreach ($posts as $post) { - $item = []; + foreach ($posts as $post) { + $item = []; - $item['author'] = $username; - $item['uri'] = $post->href; + $item['author'] = $username; + $item['uri'] = $post->href; - $titleOrRestricted = $post->find('h4')[0]->innertext; + $titleOrRestricted = $post->find('h4')[0]->innertext; - // Newgrounds doesn't show public previews for NSFW content. - if ($titleOrRestricted === 'Restricted Content: Sign in to view!') { - $item['title'] = 'NSFW: ' . $item['uri']; - $item['content'] = <<<EOD + // Newgrounds doesn't show public previews for NSFW content. + if ($titleOrRestricted === 'Restricted Content: Sign in to view!') { + $item['title'] = 'NSFW: ' . $item['uri']; + $item['content'] = <<<EOD <a href="{$item['uri']}"> {$item['title']} </a> EOD; - } else { - $item['title'] = $titleOrRestricted; - $item['content'] = <<<EOD + } else { + $item['title'] = $titleOrRestricted; + $item['content'] = <<<EOD <a href="{$item['uri']}"> <img style="align:top; width:270px; border:1px solid black;" @@ -56,25 +57,25 @@ EOD; title="{$item['title']}" /> </a> EOD; - } + } - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getName() - { - if ($this->getInput('username')) { - return sprintf('%s - %s', $this->getInput('username'), self::NAME); - } - return parent::getName(); - } + public function getName() + { + if ($this->getInput('username')) { + return sprintf('%s - %s', $this->getInput('username'), self::NAME); + } + return parent::getName(); + } - public function getURI() - { - if ($this->getInput('username')) { - return sprintf('https://%s.newgrounds.com/art', $this->getInput('username')); - } - return parent::getURI(); - } + public function getURI() + { + if ($this->getInput('username')) { + return sprintf('https://%s.newgrounds.com/art', $this->getInput('username')); + } + return parent::getURI(); + } } diff --git a/bridges/NextInpactBridge.php b/bridges/NextInpactBridge.php index 4cac7769..408fd783 100644 --- a/bridges/NextInpactBridge.php +++ b/bridges/NextInpactBridge.php @@ -1,187 +1,193 @@ <?php -class NextInpactBridge extends FeedExpander { - - const MAINTAINER = 'qwertygc and ORelio'; - const NAME = 'NextInpact Bridge'; - const URI = 'https://www.nextinpact.com/'; - const URI_HARDWARE = 'https://www.inpact-hardware.com/'; - const DESCRIPTION = 'Returns the newest articles.'; - - const PARAMETERS = array( array( - 'feed' => array( - 'name' => 'Feed', - 'type' => 'list', - 'values' => array( - 'Nos actualités' => array( - 'Toutes nos publications' => 'news', - 'Toutes nos publications sauf #LeBrief' => 'nobrief', - 'Toutes nos publications sauf INpact Hardware' => 'noih', - 'Seulement les publications INpact Hardware' => 'hardware:news', - 'Seulement les publications Next INpact' => 'nobrief-noih', - 'Seulement les publications #LeBrief' => 'lebrief', - ), - 'Flux spécifiques' => array( - 'Le blog' => 'blog', - 'Les bons plans' => 'bonsplans', - 'Publications INpact Hardware en accès libre' => 'hardware:acces-libre', - 'Publications Next INpact en accès libre' => 'acces-libre', - ), - 'Flux thématiques' => array( - 'Tech' => 'category:1', - 'Logiciel' => 'category:2', - 'Internet' => 'category:3', - 'Mobilité' => 'category:4', - 'Droit' => 'category:5', - 'Économie' => 'category:6', - 'Culture numérique' => 'category:7', - 'Next INpact' => 'category:8', - ) - ) - ), - 'filter_premium' => array( - 'name' => 'Premium', - 'type' => 'list', - 'values' => array( - 'No filter' => '0', - 'Hide Premium' => '1', - 'Only Premium' => '2' - ) - ), - 'filter_brief' => array( - 'name' => 'Brief', - 'type' => 'list', - 'values' => array( - 'No filter' => '0', - 'Hide Brief' => '1', - 'Only Brief' => '2' - ) - ), - 'limit' => self::LIMIT, - )); - - public function collectData(){ - $feed = $this->getInput('feed'); - $base_uri = self::URI; - $args = ''; - - if (empty($feed)) { - // Default to All articles - $feed = 'news'; - } - - if (strpos($feed, 'hardware:') === 0) { - // Feed hosted on Hardware domain - $base_uri = self::URI_HARDWARE; - $feed = str_replace('hardware:', '', $feed); - } - - if (strpos($feed, 'category:') === 0) { - // Feed with specific category parameter - $args = '?CategoryIds=' . str_replace('category:', '', $feed); - $feed = 'params'; - } - - $url = sprintf('%srss/%s.xml%s', $base_uri, $feed, $args); - $limit = $this->getInput('limit') ?? 10; - $this->collectExpandableDatas($url, $limit); - } - - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $item['content'] = $this->extractContent($item, $item['uri']); - if (is_null($item['content'])) - return null; //Filtered article - return $item; - } - - private function extractContent($item, $url){ - $html = getSimpleHTMLDOMCached($url); - if (!is_object($html)) - return 'Failed to request NextInpact: ' . $url; - - // Filter premium and brief articles? - $brief_selector = 'div.brief-container'; - foreach(array( - 'filter_premium' => 'p.red-msg', - 'filter_brief' => $brief_selector - ) as $param_name => $selector) { - $param_val = intval($this->getInput($param_name)); - if ($param_val != 0) { - $element_present = is_object($html->find($selector, 0)); - $element_wanted = ($param_val == 2); - if ($element_present != $element_wanted) { - return null; //Filter article - } - } - } - - $article_content = $html->find('div.article-content', 0); - if (!is_object($article_content)) { - $article_content = $html->find('div.content', 0); - } - if (is_object($article_content)) { - - // Subtitle - $subtitle = $html->find('small.subtitle', 0); - if(!is_object($subtitle) && !is_object($html->find($brief_selector, 0))) { - $subtitle = $html->find('small', 0); - } - if(!is_object($subtitle)) { - $content_wrapper = $html->find('div.content-wrapper', 0); - if (is_object($content_wrapper)) { - $subtitle = $content_wrapper->find('h2.title', 0); - } - } - if(is_object($subtitle) && (!isset($item['title']) || $subtitle->plaintext != $item['title'])) { - $subtitle = '<p><em>' . trim($subtitle->plaintext) . '</em></p>'; - } else { - $subtitle = ''; - } - - // Image - $postimg = $html->find('div.article-image, div.image-container', 0); - if(is_object($postimg)) { - $postimg = $postimg->find('img', 0); - if (!empty($postimg->src)) { - $postimg = $postimg->src; - } else { - $postimg = $postimg->srcset; //"url 355w, url 1003w, url 748w" - $postimg = explode(', ', $postimg); //split by ', ' to get each url separately - $postimg = end($postimg); //Get last item: "url 748w" which is of largest size - $postimg = explode(' ', $postimg); //split by ' ' to separate url from res - $postimg = array_reverse($postimg); //reverse array content to have url last - $postimg = end($postimg); //Get last item of array: "url" - } - $postimg = '<p><img src="' . $postimg . '" alt="-" /></p>'; - } else { - $postimg = ''; - } - - // Paywall - $paywall = $html->find('div.paywall-restriction', 0); - if (is_object($paywall) && is_object($paywall->find('p.red-msg', 0))) { - $paywall = '<p><em>' . $paywall->find('span.head-mention', 0)->innertext . '</em></p>'; - } else { - $paywall = ''; - } - - // Content - $article_content = $article_content->outertext; - $article_content = str_replace('>Signaler une erreur</span>', '></span>', $article_content); - - // Result - $text = $subtitle - . $postimg - . $article_content - . $paywall; - - } else { - $text = '<p><em>Failed to retrieve full article content</em></p>'; - if (isset($item['content'])) { - $text = $item['content'] . $text; - } - } - - return $text; - } + +class NextInpactBridge extends FeedExpander +{ + const MAINTAINER = 'qwertygc and ORelio'; + const NAME = 'NextInpact Bridge'; + const URI = 'https://www.nextinpact.com/'; + const URI_HARDWARE = 'https://www.inpact-hardware.com/'; + const DESCRIPTION = 'Returns the newest articles.'; + + const PARAMETERS = [ [ + 'feed' => [ + 'name' => 'Feed', + 'type' => 'list', + 'values' => [ + 'Nos actualités' => [ + 'Toutes nos publications' => 'news', + 'Toutes nos publications sauf #LeBrief' => 'nobrief', + 'Toutes nos publications sauf INpact Hardware' => 'noih', + 'Seulement les publications INpact Hardware' => 'hardware:news', + 'Seulement les publications Next INpact' => 'nobrief-noih', + 'Seulement les publications #LeBrief' => 'lebrief', + ], + 'Flux spécifiques' => [ + 'Le blog' => 'blog', + 'Les bons plans' => 'bonsplans', + 'Publications INpact Hardware en accès libre' => 'hardware:acces-libre', + 'Publications Next INpact en accès libre' => 'acces-libre', + ], + 'Flux thématiques' => [ + 'Tech' => 'category:1', + 'Logiciel' => 'category:2', + 'Internet' => 'category:3', + 'Mobilité' => 'category:4', + 'Droit' => 'category:5', + 'Économie' => 'category:6', + 'Culture numérique' => 'category:7', + 'Next INpact' => 'category:8', + ] + ] + ], + 'filter_premium' => [ + 'name' => 'Premium', + 'type' => 'list', + 'values' => [ + 'No filter' => '0', + 'Hide Premium' => '1', + 'Only Premium' => '2' + ] + ], + 'filter_brief' => [ + 'name' => 'Brief', + 'type' => 'list', + 'values' => [ + 'No filter' => '0', + 'Hide Brief' => '1', + 'Only Brief' => '2' + ] + ], + 'limit' => self::LIMIT, + ]]; + + public function collectData() + { + $feed = $this->getInput('feed'); + $base_uri = self::URI; + $args = ''; + + if (empty($feed)) { + // Default to All articles + $feed = 'news'; + } + + if (strpos($feed, 'hardware:') === 0) { + // Feed hosted on Hardware domain + $base_uri = self::URI_HARDWARE; + $feed = str_replace('hardware:', '', $feed); + } + + if (strpos($feed, 'category:') === 0) { + // Feed with specific category parameter + $args = '?CategoryIds=' . str_replace('category:', '', $feed); + $feed = 'params'; + } + + $url = sprintf('%srss/%s.xml%s', $base_uri, $feed, $args); + $limit = $this->getInput('limit') ?? 10; + $this->collectExpandableDatas($url, $limit); + } + + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $item['content'] = $this->extractContent($item, $item['uri']); + if (is_null($item['content'])) { + return null; //Filtered article + } + return $item; + } + + private function extractContent($item, $url) + { + $html = getSimpleHTMLDOMCached($url); + if (!is_object($html)) { + return 'Failed to request NextInpact: ' . $url; + } + + // Filter premium and brief articles? + $brief_selector = 'div.brief-container'; + foreach ( + [ + 'filter_premium' => 'p.red-msg', + 'filter_brief' => $brief_selector + ] as $param_name => $selector + ) { + $param_val = intval($this->getInput($param_name)); + if ($param_val != 0) { + $element_present = is_object($html->find($selector, 0)); + $element_wanted = ($param_val == 2); + if ($element_present != $element_wanted) { + return null; //Filter article + } + } + } + + $article_content = $html->find('div.article-content', 0); + if (!is_object($article_content)) { + $article_content = $html->find('div.content', 0); + } + if (is_object($article_content)) { + // Subtitle + $subtitle = $html->find('small.subtitle', 0); + if (!is_object($subtitle) && !is_object($html->find($brief_selector, 0))) { + $subtitle = $html->find('small', 0); + } + if (!is_object($subtitle)) { + $content_wrapper = $html->find('div.content-wrapper', 0); + if (is_object($content_wrapper)) { + $subtitle = $content_wrapper->find('h2.title', 0); + } + } + if (is_object($subtitle) && (!isset($item['title']) || $subtitle->plaintext != $item['title'])) { + $subtitle = '<p><em>' . trim($subtitle->plaintext) . '</em></p>'; + } else { + $subtitle = ''; + } + + // Image + $postimg = $html->find('div.article-image, div.image-container', 0); + if (is_object($postimg)) { + $postimg = $postimg->find('img', 0); + if (!empty($postimg->src)) { + $postimg = $postimg->src; + } else { + $postimg = $postimg->srcset; //"url 355w, url 1003w, url 748w" + $postimg = explode(', ', $postimg); //split by ', ' to get each url separately + $postimg = end($postimg); //Get last item: "url 748w" which is of largest size + $postimg = explode(' ', $postimg); //split by ' ' to separate url from res + $postimg = array_reverse($postimg); //reverse array content to have url last + $postimg = end($postimg); //Get last item of array: "url" + } + $postimg = '<p><img src="' . $postimg . '" alt="-" /></p>'; + } else { + $postimg = ''; + } + + // Paywall + $paywall = $html->find('div.paywall-restriction', 0); + if (is_object($paywall) && is_object($paywall->find('p.red-msg', 0))) { + $paywall = '<p><em>' . $paywall->find('span.head-mention', 0)->innertext . '</em></p>'; + } else { + $paywall = ''; + } + + // Content + $article_content = $article_content->outertext; + $article_content = str_replace('>Signaler une erreur</span>', '></span>', $article_content); + + // Result + $text = $subtitle + . $postimg + . $article_content + . $paywall; + } else { + $text = '<p><em>Failed to retrieve full article content</em></p>'; + if (isset($item['content'])) { + $text = $item['content'] . $text; + } + } + + return $text; + } } diff --git a/bridges/NextgovBridge.php b/bridges/NextgovBridge.php index bd76ecae..ad17f88e 100644 --- a/bridges/NextgovBridge.php +++ b/bridges/NextgovBridge.php @@ -1,67 +1,72 @@ <?php -class NextgovBridge extends FeedExpander { - const MAINTAINER = 'ORelio'; - const NAME = 'Nextgov Bridge'; - const URI = 'https://www.nextgov.com/'; - const DESCRIPTION = 'USA Federal technology news, best practices, and web 2.0 tools.'; +class NextgovBridge extends FeedExpander +{ + const MAINTAINER = 'ORelio'; + const NAME = 'Nextgov Bridge'; + const URI = 'https://www.nextgov.com/'; + const DESCRIPTION = 'USA Federal technology news, best practices, and web 2.0 tools.'; - const PARAMETERS = array( array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'All' => 'all', - 'Technology News' => 'technology-news', - 'CIO Briefing' => 'cio-briefing', - 'Emerging Tech' => 'emerging-tech', - 'Cybersecurity' => 'cybersecurity', - 'IT Modernization' => 'it-modernization', - 'Policy' => 'policy', - 'Ideas' => 'ideas', - ) - ) - )); + const PARAMETERS = [ [ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'All' => 'all', + 'Technology News' => 'technology-news', + 'CIO Briefing' => 'cio-briefing', + 'Emerging Tech' => 'emerging-tech', + 'Cybersecurity' => 'cybersecurity', + 'IT Modernization' => 'it-modernization', + 'Policy' => 'policy', + 'Ideas' => 'ideas', + ] + ] + ]]; - public function collectData(){ - $this->collectExpandableDatas(self::URI . 'rss/' . $this->getInput('category') . '/', 10); - } + public function collectData() + { + $this->collectExpandableDatas(self::URI . 'rss/' . $this->getInput('category') . '/', 10); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); - $article_thumbnail = 'https://cdn.nextgov.com/nextgov/images/logo.png'; - $item['content'] = '<p><b>' . $item['content'] . '</b></p>'; + $article_thumbnail = 'https://cdn.nextgov.com/nextgov/images/logo.png'; + $item['content'] = '<p><b>' . $item['content'] . '</b></p>'; - $namespaces = $newsItem->getNamespaces(true); - if(isset($namespaces['media'])) { - $media = $newsItem->children($namespaces['media']); - if(isset($media->content)) { - $attributes = $media->content->attributes(); - $item['content'] = '<p><img src="' . $attributes['url'] . '"></p>' . $item['content']; - $article_thumbnail = str_replace( - 'large.jpg', - 'small.jpg', - strval($attributes['url']) - ); - } - } + $namespaces = $newsItem->getNamespaces(true); + if (isset($namespaces['media'])) { + $media = $newsItem->children($namespaces['media']); + if (isset($media->content)) { + $attributes = $media->content->attributes(); + $item['content'] = '<p><img src="' . $attributes['url'] . '"></p>' . $item['content']; + $article_thumbnail = str_replace( + 'large.jpg', + 'small.jpg', + strval($attributes['url']) + ); + } + } - $item['enclosures'] = array($article_thumbnail); - $item['content'] .= $this->extractContent($item['uri']); - return $item; - } + $item['enclosures'] = [$article_thumbnail]; + $item['content'] .= $this->extractContent($item['uri']); + return $item; + } - private function extractContent($url){ - $article = getSimpleHTMLDOMCached($url); + private function extractContent($url) + { + $article = getSimpleHTMLDOMCached($url); - if (!is_object($article)) - return 'Could not request Nextgov: ' . $url; + if (!is_object($article)) { + return 'Could not request Nextgov: ' . $url; + } - $contents = $article->find('div.wysiwyg', 0); - $contents = $contents->innertext; - $contents = stripWithDelimiters($contents, '<div class="ad-container">', '</div>'); - $contents = stripWithDelimiters($contents, '<div', '</div>'); //ad outer div - return trim(stripWithDelimiters($contents, '<script', '</script>')); - } + $contents = $article->find('div.wysiwyg', 0); + $contents = $contents->innertext; + $contents = stripWithDelimiters($contents, '<div class="ad-container">', '</div>'); + $contents = stripWithDelimiters($contents, '<div', '</div>'); //ad outer div + return trim(stripWithDelimiters($contents, '<script', '</script>')); + } } diff --git a/bridges/NiceMatinBridge.php b/bridges/NiceMatinBridge.php index b0af7608..6e622b42 100644 --- a/bridges/NiceMatinBridge.php +++ b/bridges/NiceMatinBridge.php @@ -1,32 +1,38 @@ <?php -class NiceMatinBridge extends FeedExpander { - const MAINTAINER = 'pit-fgfjiudghdf'; - const NAME = 'NiceMatin'; - const URI = 'https://www.nicematin.com/'; - const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)'; +class NiceMatinBridge extends FeedExpander +{ + const MAINTAINER = 'pit-fgfjiudghdf'; + const NAME = 'NiceMatin'; + const URI = 'https://www.nicematin.com/'; + const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)'; - public function collectData(){ - $this->collectExpandableDatas(self::URI . 'derniere-minute/rss', 10); - } + public function collectData() + { + $this->collectExpandableDatas(self::URI . 'derniere-minute/rss', 10); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $item['content'] = $this->extractContent($item['uri']); - return $item; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $item['content'] = $this->extractContent($item['uri']); + return $item; + } - private function extractContent($url){ - $html = getSimpleHTMLDOMCached($url); - if(!$html) - return 'Could not acquire content from url: ' . $url . '!'; + private function extractContent($url) + { + $html = getSimpleHTMLDOMCached($url); + if (!$html) { + return 'Could not acquire content from url: ' . $url . '!'; + } - $content = $html->find('article', 0); - if(!$content) - return 'Could not find \'section\'!'; + $content = $html->find('article', 0); + if (!$content) { + return 'Could not find \'section\'!'; + } - $text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content->innertext); - $text = strip_tags($text, '<p><a><img>'); - return $text; - } + $text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content->innertext); + $text = strip_tags($text, '<p><a><img>'); + return $text; + } } diff --git a/bridges/NikonDownloadCenterBridge.php b/bridges/NikonDownloadCenterBridge.php index 88b33c41..143d40f5 100644 --- a/bridges/NikonDownloadCenterBridge.php +++ b/bridges/NikonDownloadCenterBridge.php @@ -1,34 +1,39 @@ <?php -class NikonDownloadCenterBridge extends BridgeAbstract { - const NAME = 'Nikon Download Center – What\'s New'; - const URI = 'https://downloadcenter.nikonimglib.com/'; - const DESCRIPTION = 'Firmware updates and new software from Nikon.'; - const MAINTAINER = 'sal0max'; - const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours - public function getURI() { - $year = date('Y'); - return self::URI . 'en/update/index/' . $year . '.html'; - } +class NikonDownloadCenterBridge extends BridgeAbstract +{ + const NAME = 'Nikon Download Center – What\'s New'; + const URI = 'https://downloadcenter.nikonimglib.com/'; + const DESCRIPTION = 'Firmware updates and new software from Nikon.'; + const MAINTAINER = 'sal0max'; + const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours - public function getIcon() { - return self::URI . 'favicon.ico'; - } + public function getURI() + { + $year = date('Y'); + return self::URI . 'en/update/index/' . $year . '.html'; + } - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); + public function getIcon() + { + return self::URI . 'favicon.ico'; + } - foreach ($html->find('dd>ul>li') as $element) { - $date = $element->find('.date', 0)->plaintext; - $productType = $element->find('.icon>img', 0)->alt; - $desc = $element->find('p>a', 0)->plaintext; - $link = urljoin(self::URI, $element->find('p>a', 0)->href); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); - $item = array( - 'title' => $desc, - 'uri' => $link, - 'timestamp' => strtotime($date), - 'content' => <<<EOD + foreach ($html->find('dd>ul>li') as $element) { + $date = $element->find('.date', 0)->plaintext; + $productType = $element->find('.icon>img', 0)->alt; + $desc = $element->find('p>a', 0)->plaintext; + $link = urljoin(self::URI, $element->find('p>a', 0)->href); + + $item = [ + 'title' => $desc, + 'uri' => $link, + 'timestamp' => strtotime($date), + 'content' => <<<EOD <p> New/updated {$productType}:<br> <strong><a href="{$link}">{$desc}</a></strong> @@ -37,8 +42,8 @@ class NikonDownloadCenterBridge extends BridgeAbstract { {$date} </p> EOD - ); - $this->items[] = $item; - } - } + ]; + $this->items[] = $item; + } + } } diff --git a/bridges/NineGagBridge.php b/bridges/NineGagBridge.php index 6b8ef822..61e3975d 100644 --- a/bridges/NineGagBridge.php +++ b/bridges/NineGagBridge.php @@ -1,372 +1,388 @@ <?php -class NineGagBridge extends BridgeAbstract { - const NAME = '9gag Bridge'; - const URI = 'https://9gag.com/'; - const DESCRIPTION = 'Returns latest quotes from 9gag.'; - const MAINTAINER = 'ZeNairolf'; - const CACHE_TIMEOUT = 3600; - const PARAMETERS = array( - 'Popular' => array( - 'd' => array( - 'name' => 'Section', - 'type' => 'list', - 'values' => array( - 'Hot' => 'hot', - 'Trending' => 'trending', - 'Fresh' => 'fresh', - ), - ), - 'video' => array( - 'name' => 'Filter Video', - 'type' => 'list', - 'values' => array( - 'NotFiltred' => 'none', - 'VideoFiltred' => 'without', - 'VideoOnly' => 'only', - ), - ), - 'p' => array( - 'name' => 'Pages', - 'type' => 'number', - 'defaultValue' => 3, - ), - ), - 'Sections' => array( - 'g' => array( - 'name' => 'Section', - 'type' => 'list', - 'values' => array( - 'Among Us' => 'among-us', - 'Animals' => 'animals', - 'Anime & Manga' => 'anime-manga', - 'Anime Waifu' => 'animewaifu', - 'Anime Wallpaper' => 'animewallpaper', - 'Apex Legends' => 'apexlegends', - 'Ask 9GAG' => 'ask9gag', - 'Awesome' => 'awesome', - 'Car' => 'car', - 'Comic & Webtoon' => 'comic-webtoon', - 'Coronavirus ' => 'coronavirus', - 'Cosplay' => 'cosplay', - 'Countryballs' => 'countryballs', - 'Cozy & Comfy' => 'home-living', - 'Crappy Design' => 'crappydesign', - 'Cryptocurrency ' => 'cryptocurrency', - 'Cyberpunk 2077' => 'cyberpunk2077', - 'Dark Humor' => 'darkhumor', - 'Drawing, DIY & Crafts' => 'drawing-diy-crafts', - 'Fashion & Beauty' => 'rate-my-outfit', - 'Food & Drinks' => 'food-drinks', - 'Football' => 'football', - 'Fortnite' => 'fortnite', - 'Funny' => 'funny', - 'Game of Thrones' => 'got', - 'Gaming' => 'gaming', - 'GIF' => 'gif', - 'Girl' => 'girl', - 'Girl Celebrity' => 'girlcelebrity', - 'Guy' => 'guy', - 'History' => 'history', - 'Horror' => 'horror', - 'K-Pop' => 'kpop', - 'Latest News' => 'timely', - 'League of Legends' => 'leagueoflegends', - 'LEGO' => 'lego', - 'Marvel & DC' => 'superhero', - 'Meme' => 'meme', - 'Movie & TV' => 'movie-tv', - 'Music' => 'music', - 'NBA' => 'basketball', - 'Overwatch' => 'overwatch', - 'PC Master Race' => 'pcmr', - 'Pokémon' => 'pokemon', - 'Politics ' => 'politics', - 'PUBG' => 'pubg', - 'Random ' => 'random', - 'Relationship' => 'relationship', - 'Satisfying' => 'satisfying', - 'Savage' => 'savage', - 'Science & Tech' => 'science-tech', - 'Sport ' => 'sport', - 'Star Wars' => 'starwars', - 'Teens Can Relate' => 'school', - 'Travel & Photography' => 'travel-photography', - 'Video' => 'video', - 'Wallpaper' => 'wallpaper', - 'Warhammer' => 'warhammer', - 'Wholesome' => 'wholesome', - 'WTF' => 'wtf', - ), - ), - 't' => array( - 'name' => 'Type', - 'type' => 'list', - 'values' => array( - 'Hot' => 'hot', - 'Fresh' => 'fresh', - ), - ), - 'video' => array( - 'name' => 'Filter Video', - 'type' => 'list', - 'values' => array( - 'NotFiltred' => 'none', - 'VideoFiltred' => 'without', - 'VideoOnly' => 'only', - ), - ), - 'p' => array( - 'name' => 'Pages', - 'type' => 'number', - 'defaultValue' => 3, - ), - ), - ); - - const MIN_NBR_PAGE = 1; - const MAX_NBR_PAGE = 6; - - protected $p = null; - - public function collectData() { - $url = sprintf( - '%sv1/group-posts/group/%s/type/%s?', - self::URI, - $this->getGroup(), - $this->getType() - ); - $cursor = 'c=10'; - $posts = array(); - for ($i = 0; $i < $this->getPages(); ++$i) { - $content = getContents($url . $cursor); - $json = json_decode($content, true); - $posts = array_merge($posts, $json['data']['posts']); - $cursor = $json['data']['nextCursor']; - } - - foreach ($posts as $post) { - $AvoidElement = false; - switch ($this->getInput('video')) { - case 'without': - if ($post['type'] === 'Animated') { - $AvoidElement = true; - } - break; - case 'only': - echo $post['type']; - if ($post['type'] !== 'Animated') { - $AvoidElement = true; - } - break; - case 'none': default: - break; - } - - if (!$AvoidElement) { - $item['uri'] = preg_replace('/^http:/i', 'https:', $post['url']); - $item['title'] = $post['title']; - $item['content'] = self::getContent($post); - $item['categories'] = self::getCategories($post); - $item['timestamp'] = self::getTimestamp($post); - - $this->items[] = $item; - } - } - } - - public function getName() { - if ($this->getInput('d')) { - $name = sprintf('%s - %s', '9GAG', $this->getParameterKey('d')); - } elseif ($this->getInput('g')) { - $name = sprintf('%s - %s', '9GAG', $this->getParameterKey('g')); - if ($this->getInput('t')) { - $name = sprintf('%s [%s]', $name, $this->getParameterKey('t')); - } - } - if (!empty($name)) { - return $name; - } - - return self::NAME; - } - - public function getURI() { - $uri = $this->getInput('g'); - if ($uri === 'default') { - $uri = $this->getInput('t'); - } - - return self::URI . $uri; - } - - protected function getGroup() { - if ($this->getInput('d')) { - return 'default'; - } - - return $this->getInput('g'); - } - - protected function getType() { - if ($this->getInput('d')) { - return $this->getInput('d'); - } - - return $this->getInput('t'); - } - - protected function getPages() { - if ($this->p === null) { - $value = (int) $this->getInput('p'); - $value = ($value < self::MIN_NBR_PAGE) ? self::MIN_NBR_PAGE : $value; - $value = ($value > self::MAX_NBR_PAGE) ? self::MAX_NBR_PAGE : $value; - - $this->p = $value; - } - - return $this->p; - } - - protected function getParameterKey($input = '') { - $params = $this->getParameters(); - $tab = 'Sections'; - if ($input === 'd') { - $tab = 'Popular'; - } - if (!isset($params[$tab][$input])) { - return ''; - } - - return array_search( - $this->getInput($input), - $params[$tab][$input]['values'] - ); - } - - protected static function getContent($post) { - if ($post['type'] === 'Animated') { - $content = self::getAnimated($post); - } elseif ($post['type'] === 'Article') { - $content = self::getArticle($post); - } else { - $content = self::getPhoto($post); - } - - return $content; - } - - protected static function getPhoto($post) { - $image = $post['images']['image460']; - $photo = '<picture>'; - $photo .= sprintf( - '<source srcset="%s" type="image/webp">', - $image['webpUrl'] - ); - $photo .= sprintf( - '<img src="%s" alt="%s" %s>', - $image['url'], - $post['title'], - 'width="500"' - ); - $photo .= '</picture>'; - - return $photo; - } - - protected static function getAnimated($post) { - $poster = $post['images']['image460']['url']; - $sources = $post['images']; - $video = sprintf( - '<video poster="%s" %s>', - $poster, - 'preload="auto" loop controls style="min-height: 300px" width="500"' - ); - $video .= sprintf( - '<source src="%s" type="video/webm">', - $sources['image460sv']['vp9Url'] - ); - $video .= sprintf( - '<source src="%s" type="video/mp4">', - $sources['image460sv']['h265Url'] - ); - $video .= sprintf( - '<source src="%s" type="video/mp4">', - $sources['image460svwm']['url'] - ); - $video .= '</video>'; - - return $video; - } - - protected static function getArticle($post) { - $blocks = $post['article']['blocks']; - $medias = $post['article']['medias']; - $contents = array(); - foreach ($blocks as $block) { - if ('Media' === $block['type']) { - $mediaId = $block['mediaId']; - $contents[] = self::getContent($medias[$mediaId]); - } elseif ('RichText' === $block['type']) { - $contents[] = self::getRichText($block['content']); - } - } - - $content = join('</div><div>', $contents); - $content = sprintf( - '<%1$s>%2$s</%1$s>', - 'div', - $content - ); - - return $content; - } - - protected static function getRichText($text = '') { - $text = trim($text); - - if (preg_match('/^>\s(?<text>.*)/', $text, $matches)) { - $text = sprintf( - '<%1$s>%2$s</%1$s>', - 'blockquote', - $matches['text'] - ); - } else { - $text = sprintf( - '<%1$s>%2$s</%1$s>', - 'p', - $text - ); - } - - return $text; - } - - protected static function getCategories($post) { - $params = self::PARAMETERS; - $sections = $params['Sections']['g']['values']; - - if(isset($post['sections'])) { - $postSections = $post['sections']; - } elseif (isset($post['postSection'])) { - $postSections = array($post['postSection']); - } else { - $postSections = array(); - } - - foreach ($postSections as $key => $section) { - $postSections[$key] = array_search($section, $sections); - } - - return $postSections; - } - - protected static function getTimestamp($post) { - $url = $post['images']['image460']['url']; - $headers = get_headers($url, true); - $date = $headers['Date']; - $time = strtotime($date); - - return $time; - } +class NineGagBridge extends BridgeAbstract +{ + const NAME = '9gag Bridge'; + const URI = 'https://9gag.com/'; + const DESCRIPTION = 'Returns latest quotes from 9gag.'; + const MAINTAINER = 'ZeNairolf'; + const CACHE_TIMEOUT = 3600; + const PARAMETERS = [ + 'Popular' => [ + 'd' => [ + 'name' => 'Section', + 'type' => 'list', + 'values' => [ + 'Hot' => 'hot', + 'Trending' => 'trending', + 'Fresh' => 'fresh', + ], + ], + 'video' => [ + 'name' => 'Filter Video', + 'type' => 'list', + 'values' => [ + 'NotFiltred' => 'none', + 'VideoFiltred' => 'without', + 'VideoOnly' => 'only', + ], + ], + 'p' => [ + 'name' => 'Pages', + 'type' => 'number', + 'defaultValue' => 3, + ], + ], + 'Sections' => [ + 'g' => [ + 'name' => 'Section', + 'type' => 'list', + 'values' => [ + 'Among Us' => 'among-us', + 'Animals' => 'animals', + 'Anime & Manga' => 'anime-manga', + 'Anime Waifu' => 'animewaifu', + 'Anime Wallpaper' => 'animewallpaper', + 'Apex Legends' => 'apexlegends', + 'Ask 9GAG' => 'ask9gag', + 'Awesome' => 'awesome', + 'Car' => 'car', + 'Comic & Webtoon' => 'comic-webtoon', + 'Coronavirus ' => 'coronavirus', + 'Cosplay' => 'cosplay', + 'Countryballs' => 'countryballs', + 'Cozy & Comfy' => 'home-living', + 'Crappy Design' => 'crappydesign', + 'Cryptocurrency ' => 'cryptocurrency', + 'Cyberpunk 2077' => 'cyberpunk2077', + 'Dark Humor' => 'darkhumor', + 'Drawing, DIY & Crafts' => 'drawing-diy-crafts', + 'Fashion & Beauty' => 'rate-my-outfit', + 'Food & Drinks' => 'food-drinks', + 'Football' => 'football', + 'Fortnite' => 'fortnite', + 'Funny' => 'funny', + 'Game of Thrones' => 'got', + 'Gaming' => 'gaming', + 'GIF' => 'gif', + 'Girl' => 'girl', + 'Girl Celebrity' => 'girlcelebrity', + 'Guy' => 'guy', + 'History' => 'history', + 'Horror' => 'horror', + 'K-Pop' => 'kpop', + 'Latest News' => 'timely', + 'League of Legends' => 'leagueoflegends', + 'LEGO' => 'lego', + 'Marvel & DC' => 'superhero', + 'Meme' => 'meme', + 'Movie & TV' => 'movie-tv', + 'Music' => 'music', + 'NBA' => 'basketball', + 'Overwatch' => 'overwatch', + 'PC Master Race' => 'pcmr', + 'Pokémon' => 'pokemon', + 'Politics ' => 'politics', + 'PUBG' => 'pubg', + 'Random ' => 'random', + 'Relationship' => 'relationship', + 'Satisfying' => 'satisfying', + 'Savage' => 'savage', + 'Science & Tech' => 'science-tech', + 'Sport ' => 'sport', + 'Star Wars' => 'starwars', + 'Teens Can Relate' => 'school', + 'Travel & Photography' => 'travel-photography', + 'Video' => 'video', + 'Wallpaper' => 'wallpaper', + 'Warhammer' => 'warhammer', + 'Wholesome' => 'wholesome', + 'WTF' => 'wtf', + ], + ], + 't' => [ + 'name' => 'Type', + 'type' => 'list', + 'values' => [ + 'Hot' => 'hot', + 'Fresh' => 'fresh', + ], + ], + 'video' => [ + 'name' => 'Filter Video', + 'type' => 'list', + 'values' => [ + 'NotFiltred' => 'none', + 'VideoFiltred' => 'without', + 'VideoOnly' => 'only', + ], + ], + 'p' => [ + 'name' => 'Pages', + 'type' => 'number', + 'defaultValue' => 3, + ], + ], + ]; + + const MIN_NBR_PAGE = 1; + const MAX_NBR_PAGE = 6; + + protected $p = null; + + public function collectData() + { + $url = sprintf( + '%sv1/group-posts/group/%s/type/%s?', + self::URI, + $this->getGroup(), + $this->getType() + ); + $cursor = 'c=10'; + $posts = []; + for ($i = 0; $i < $this->getPages(); ++$i) { + $content = getContents($url . $cursor); + $json = json_decode($content, true); + $posts = array_merge($posts, $json['data']['posts']); + $cursor = $json['data']['nextCursor']; + } + + foreach ($posts as $post) { + $AvoidElement = false; + switch ($this->getInput('video')) { + case 'without': + if ($post['type'] === 'Animated') { + $AvoidElement = true; + } + break; + case 'only': + echo $post['type']; + if ($post['type'] !== 'Animated') { + $AvoidElement = true; + } + break; + case 'none': + default: + break; + } + + if (!$AvoidElement) { + $item['uri'] = preg_replace('/^http:/i', 'https:', $post['url']); + $item['title'] = $post['title']; + $item['content'] = self::getContent($post); + $item['categories'] = self::getCategories($post); + $item['timestamp'] = self::getTimestamp($post); + + $this->items[] = $item; + } + } + } + + public function getName() + { + if ($this->getInput('d')) { + $name = sprintf('%s - %s', '9GAG', $this->getParameterKey('d')); + } elseif ($this->getInput('g')) { + $name = sprintf('%s - %s', '9GAG', $this->getParameterKey('g')); + if ($this->getInput('t')) { + $name = sprintf('%s [%s]', $name, $this->getParameterKey('t')); + } + } + if (!empty($name)) { + return $name; + } + + return self::NAME; + } + + public function getURI() + { + $uri = $this->getInput('g'); + if ($uri === 'default') { + $uri = $this->getInput('t'); + } + + return self::URI . $uri; + } + + protected function getGroup() + { + if ($this->getInput('d')) { + return 'default'; + } + + return $this->getInput('g'); + } + + protected function getType() + { + if ($this->getInput('d')) { + return $this->getInput('d'); + } + + return $this->getInput('t'); + } + + protected function getPages() + { + if ($this->p === null) { + $value = (int) $this->getInput('p'); + $value = ($value < self::MIN_NBR_PAGE) ? self::MIN_NBR_PAGE : $value; + $value = ($value > self::MAX_NBR_PAGE) ? self::MAX_NBR_PAGE : $value; + + $this->p = $value; + } + + return $this->p; + } + + protected function getParameterKey($input = '') + { + $params = $this->getParameters(); + $tab = 'Sections'; + if ($input === 'd') { + $tab = 'Popular'; + } + if (!isset($params[$tab][$input])) { + return ''; + } + + return array_search( + $this->getInput($input), + $params[$tab][$input]['values'] + ); + } + + protected static function getContent($post) + { + if ($post['type'] === 'Animated') { + $content = self::getAnimated($post); + } elseif ($post['type'] === 'Article') { + $content = self::getArticle($post); + } else { + $content = self::getPhoto($post); + } + + return $content; + } + + protected static function getPhoto($post) + { + $image = $post['images']['image460']; + $photo = '<picture>'; + $photo .= sprintf( + '<source srcset="%s" type="image/webp">', + $image['webpUrl'] + ); + $photo .= sprintf( + '<img src="%s" alt="%s" %s>', + $image['url'], + $post['title'], + 'width="500"' + ); + $photo .= '</picture>'; + + return $photo; + } + + protected static function getAnimated($post) + { + $poster = $post['images']['image460']['url']; + $sources = $post['images']; + $video = sprintf( + '<video poster="%s" %s>', + $poster, + 'preload="auto" loop controls style="min-height: 300px" width="500"' + ); + $video .= sprintf( + '<source src="%s" type="video/webm">', + $sources['image460sv']['vp9Url'] + ); + $video .= sprintf( + '<source src="%s" type="video/mp4">', + $sources['image460sv']['h265Url'] + ); + $video .= sprintf( + '<source src="%s" type="video/mp4">', + $sources['image460svwm']['url'] + ); + $video .= '</video>'; + + return $video; + } + + protected static function getArticle($post) + { + $blocks = $post['article']['blocks']; + $medias = $post['article']['medias']; + $contents = []; + foreach ($blocks as $block) { + if ('Media' === $block['type']) { + $mediaId = $block['mediaId']; + $contents[] = self::getContent($medias[$mediaId]); + } elseif ('RichText' === $block['type']) { + $contents[] = self::getRichText($block['content']); + } + } + + $content = join('</div><div>', $contents); + $content = sprintf( + '<%1$s>%2$s</%1$s>', + 'div', + $content + ); + + return $content; + } + + protected static function getRichText($text = '') + { + $text = trim($text); + + if (preg_match('/^>\s(?<text>.*)/', $text, $matches)) { + $text = sprintf( + '<%1$s>%2$s</%1$s>', + 'blockquote', + $matches['text'] + ); + } else { + $text = sprintf( + '<%1$s>%2$s</%1$s>', + 'p', + $text + ); + } + + return $text; + } + + protected static function getCategories($post) + { + $params = self::PARAMETERS; + $sections = $params['Sections']['g']['values']; + + if (isset($post['sections'])) { + $postSections = $post['sections']; + } elseif (isset($post['postSection'])) { + $postSections = [$post['postSection']]; + } else { + $postSections = []; + } + + foreach ($postSections as $key => $section) { + $postSections[$key] = array_search($section, $sections); + } + + return $postSections; + } + + protected static function getTimestamp($post) + { + $url = $post['images']['image460']['url']; + $headers = get_headers($url, true); + $date = $headers['Date']; + $time = strtotime($date); + + return $time; + } } diff --git a/bridges/NordbayernBridge.php b/bridges/NordbayernBridge.php index ded9c682..7a80f930 100644 --- a/bridges/NordbayernBridge.php +++ b/bridges/NordbayernBridge.php @@ -1,163 +1,177 @@ <?php -class NordbayernBridge extends BridgeAbstract { +class NordbayernBridge extends BridgeAbstract +{ + const MAINTAINER = 'schabi.org'; + const NAME = 'Nordbayern'; + const CACHE_TIMEOUT = 3600; + const URI = 'https://www.nordbayern.de'; + const DESCRIPTION = 'Bridge for Bavarian regional news site nordbayern.de'; + const PARAMETERS = [ [ + 'region' => [ + 'name' => 'region', + 'type' => 'list', + 'exampleValue' => 'Nürnberg', + 'title' => 'Select a region', + 'values' => [ + 'Nürnberg' => 'nuernberg', + 'Fürth' => 'fuerth', + 'Erlangen' => 'erlangen', + 'Altdorf' => 'altdorf', + 'Ansbach' => 'ansbach', + 'Bad Windsheim' => 'bad-windsheim', + 'Bamberg' => 'bamberg', + 'Dinkelsbühl/Feuchtwangen' => 'dinkelsbuehl-feuchtwangen', + 'Feucht' => 'feucht', + 'Forchheim' => 'forchheim', + 'Gunzenhausen' => 'gunzenhausen', + 'Hersbruck' => 'hersbruck', + 'Herzogenaurach' => 'herzogenaurach', + 'Hilpoltstein' => 'hilpoltstein', + 'Höchstadt' => 'hoechstadt', + 'Lauf' => 'lauf', + 'Neumarkt' => 'neumarkt', + 'Neustadt/Aisch' => 'neustadt-aisch', + 'Pegnitz' => 'pegnitz', + 'Roth' => 'roth', + 'Rothenburg o.d.T.' => 'rothenburg-o-d-t', + 'Treuchtlingen' => 'treuchtlingen', + 'Weißenburg' => 'weissenburg' + ] + ], + 'policeReports' => [ + 'name' => 'Police Reports', + 'type' => 'checkbox', + 'exampleValue' => 'checked', + 'title' => 'Include Police Reports', + ] + ]]; - const MAINTAINER = 'schabi.org'; - const NAME = 'Nordbayern'; - const CACHE_TIMEOUT = 3600; - const URI = 'https://www.nordbayern.de'; - const DESCRIPTION = 'Bridge for Bavarian regional news site nordbayern.de'; - const PARAMETERS = array( array( - 'region' => array( - 'name' => 'region', - 'type' => 'list', - 'exampleValue' => 'Nürnberg', - 'title' => 'Select a region', - 'values' => array( - 'Nürnberg' => 'nuernberg', - 'Fürth' => 'fuerth', - 'Erlangen' => 'erlangen', - 'Altdorf' => 'altdorf', - 'Ansbach' => 'ansbach', - 'Bad Windsheim' => 'bad-windsheim', - 'Bamberg' => 'bamberg', - 'Dinkelsbühl/Feuchtwangen' => 'dinkelsbuehl-feuchtwangen', - 'Feucht' => 'feucht', - 'Forchheim' => 'forchheim', - 'Gunzenhausen' => 'gunzenhausen', - 'Hersbruck' => 'hersbruck', - 'Herzogenaurach' => 'herzogenaurach', - 'Hilpoltstein' => 'hilpoltstein', - 'Höchstadt' => 'hoechstadt', - 'Lauf' => 'lauf', - 'Neumarkt' => 'neumarkt', - 'Neustadt/Aisch' => 'neustadt-aisch', - 'Pegnitz' => 'pegnitz', - 'Roth' => 'roth', - 'Rothenburg o.d.T.' => 'rothenburg-o-d-t', - 'Treuchtlingen' => 'treuchtlingen', - 'Weißenburg' => 'weissenburg' - ) - ), - 'policeReports' => array( - 'name' => 'Police Reports', - 'type' => 'checkbox', - 'exampleValue' => 'checked', - 'title' => 'Include Police Reports', - ) - )); + private function getValidImage($picture) + { + $img = $picture->find('img', 0); + if ($img) { + $imgUrl = $img->src; + if (!preg_match('#/logo-.*\.png#', $imgUrl)) { + return '<br><img src="' . $imgUrl . '">'; + } + } + return ''; + } - private function getValidImage($picture) { - $img = $picture->find('img', 0); - if ($img) { - $imgUrl = $img->src; - if(!preg_match('#/logo-.*\.png#', $imgUrl)) { - return '<br><img src="' . $imgUrl . '">'; - } - } - return ''; - } + private function getUseFullContent($rawContent) + { + $content = ''; + foreach ($rawContent->children as $element) { + if ( + ($element->tag === 'p' || $element->tag === 'h3') && + $element->class !== 'article__teaser' + ) { + $content .= $element; + } elseif ($element->tag === 'main') { + $content .= self::getUseFullContent($element->find('article', 0)); + } elseif ($element->tag === 'header') { + $content .= self::getUseFullContent($element); + } elseif ( + $element->tag === 'div' && + !str_contains($element->class, 'article__infobox') && + !str_contains($element->class, 'authorinfo') + ) { + $content .= self::getUseFullContent($element); + } elseif ( + $element->tag === 'section' && + (str_contains($element->class, 'article__richtext') || + str_contains($element->class, 'article__context')) + ) { + $content .= self::getUseFullContent($element); + } elseif ($element->tag === 'picture') { + $content .= self::getValidImage($element); + } + } + return $content; + } - private function getUseFullContent($rawContent) { - $content = ''; - foreach($rawContent->children as $element) { - if(($element->tag === 'p' || $element->tag === 'h3') && - $element->class !== 'article__teaser') { - $content .= $element; - } else if($element->tag === 'main') { - $content .= self::getUseFullContent($element->find('article', 0)); - } else if($element->tag === 'header') { - $content .= self::getUseFullContent($element); - } else if($element->tag === 'div' && - !str_contains($element->class, 'article__infobox') && - !str_contains($element->class, 'authorinfo')) { - $content .= self::getUseFullContent($element); - } else if($element->tag === 'section' && - (str_contains($element->class, 'article__richtext') || - str_contains($element->class, 'article__context'))) { - $content .= self::getUseFullContent($element); - } else if($element->tag === 'picture') { - $content .= self::getValidImage($element); - } - } - return $content; - } + private function getTeaser($content) + { + $teaser = $content->find('p[class=article__teaser]', 0); + if ($teaser === null) { + return ''; + } + $teaser = $teaser->plaintext; + $teaser = preg_replace('/[ ]{2,}/', ' ', $teaser); + $teaser = '<p class="article__teaser">' . $teaser . '</p>'; + return $teaser; + } - private function getTeaser($content) { - $teaser = $content->find('p[class=article__teaser]', 0); - if($teaser === null) { - return ''; - } - $teaser = $teaser->plaintext; - $teaser = preg_replace('/[ ]{2,}/', ' ', $teaser); - $teaser = '<p class="article__teaser">' . $teaser . '</p>'; - return $teaser; - } + private function handleArticle($link) + { + $item = []; + $article = getSimpleHTMLDOM($link); + defaultLinkTo($article, self::URI); + $content = $article->find('article[id=article]', 0); + $item['uri'] = $link; - private function handleArticle($link) { - $item = array(); - $article = getSimpleHTMLDOM($link); - defaultLinkTo($article, self::URI); - $content = $article->find('article[id=article]', 0); - $item['uri'] = $link; + $author = $article->find('.article__author', 1); + if ($author !== null) { + $item['author'] = trim($author->plaintext); + } - $author = $article->find('.article__author', 1); - if ($author !== null) { - $item['author'] = trim($author->plaintext); - } + $createdAt = $article->find('[class=article__release]', 0); + if ($createdAt) { + $item['timestamp'] = strtotime(str_replace('Uhr', '', $createdAt->plaintext)); + } - $createdAt = $article->find('[class=article__release]', 0); - if ($createdAt) { - $item['timestamp'] = strtotime(str_replace('Uhr', '', $createdAt->plaintext)); - } + if ($article->find('h2', 0) === null) { + $item['title'] = $article->find('h3', 0)->innertext; + } else { + $item['title'] = $article->find('h2', 0)->innertext; + } + $item['content'] = ''; - if ($article->find('h2', 0) === null) { - $item['title'] = $article->find('h3', 0)->innertext; - } else { - $item['title'] = $article->find('h2', 0)->innertext; - } - $item['content'] = ''; + if ($article->find('section[class*=article__richtext]', 0) === null) { + $content = $article->find('div[class*=modul__teaser]', 0) + ->find('p', 0); + $item['content'] .= $content; + } else { + $content = $article->find('article', 0); + // change order of article teaser in order to show it on top + // of the title image. If we didn't do this some rss programs + // would show the subtitle of the title image as teaser instead + // of the actuall article teaser. + $item['content'] .= self::getTeaser($content); + $item['content'] .= self::getUseFullContent($content); + } - if ($article->find('section[class*=article__richtext]', 0) === null) { - $content = $article->find('div[class*=modul__teaser]', 0) - ->find('p', 0); - $item['content'] .= $content; - } else { - $content = $article->find('article', 0); - // change order of article teaser in order to show it on top - // of the title image. If we didn't do this some rss programs - // would show the subtitle of the title image as teaser instead - // of the actuall article teaser. - $item['content'] .= self::getTeaser($content); - $item['content'] .= self::getUseFullContent($content); - } + // exclude police reports if desired + if ( + $this->getInput('policeReports') || + !str_contains($item['content'], 'Hier geht es zu allen aktuellen Polizeimeldungen.') + ) { + $this->items[] = $item; + } - // exclude police reports if desired - if($this->getInput('policeReports') || - !str_contains($item['content'], 'Hier geht es zu allen aktuellen Polizeimeldungen.')) { - $this->items[] = $item; - } + $article->clear(); + } - $article->clear(); - } + private function handleNewsblock($listSite) + { + $main = $listSite->find('main', 0); + foreach ($main->find('article') as $article) { + $url = $article->find('a', 0)->href; + $url = urljoin(self::URI, $url); + self::handleArticle($url); + } + } - private function handleNewsblock($listSite) { - $main = $listSite->find('main', 0); - foreach($main->find('article') as $article) { - $url = $article->find('a', 0)->href; - $url = urljoin(self::URI, $url); - self::handleArticle($url); - } - } + public function collectData() + { + $region = $this->getInput('region'); + if ($region === 'rothenburg-o-d-t') { + $region = 'rothenburg-ob-der-tauber'; + } + $url = self::URI . '/region/' . $region; + $listSite = getSimpleHTMLDOM($url); - public function collectData() { - $region = $this->getInput('region'); - if($region === 'rothenburg-o-d-t') { - $region = 'rothenburg-ob-der-tauber'; - } - $url = self::URI . '/region/' . $region; - $listSite = getSimpleHTMLDOM($url); - - self::handleNewsblock($listSite); - } + self::handleNewsblock($listSite); + } } diff --git a/bridges/NotAlwaysBridge.php b/bridges/NotAlwaysBridge.php index f8e56cd6..33b619ad 100644 --- a/bridges/NotAlwaysBridge.php +++ b/bridges/NotAlwaysBridge.php @@ -1,59 +1,64 @@ <?php -class NotAlwaysBridge extends BridgeAbstract { - - const MAINTAINER = 'mozes'; - const NAME = 'Not Always family Bridge'; - const URI = 'https://notalwaysright.com/'; - const DESCRIPTION = 'Returns the latest stories'; - const CACHE_TIMEOUT = 1800; // 30 minutes - - const PARAMETERS = array( array( - 'filter' => array( - 'type' => 'list', - 'name' => 'Filter', - 'values' => array( - 'All' => '', - 'Right' => 'right', - 'Working' => 'working', - 'Romantic' => 'romantic', - 'Related' => 'related', - 'Learning' => 'learning', - 'Friendly' => 'friendly', - 'Hopeless' => 'hopeless', - 'Unfiltered' => 'unfiltered' - ) - ) - )); - - public function getIcon() { - return self::URI . 'favicon_nar.png'; - } - - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); - foreach($html->find('.post') as $post) { - #print_r($post); - $item = array(); - $item['uri'] = $post->find('h1', 0)->find('a', 0)->href; - $item['content'] = $post; - $item['title'] = $post->find('h1', 0)->find('a', 0)->innertext; - $this->items[] = $item; - } - } - - public function getName(){ - if(!is_null($this->getInput('filter'))) { - return $this->getInput('filter') . ' - NotAlways Bridge'; - } - - return parent::getName(); - } - - public function getURI(){ - if(!is_null($this->getInput('filter'))) { - return self::URI . $this->getInput('filter') . '/'; - } - - return parent::getURI(); - } + +class NotAlwaysBridge extends BridgeAbstract +{ + const MAINTAINER = 'mozes'; + const NAME = 'Not Always family Bridge'; + const URI = 'https://notalwaysright.com/'; + const DESCRIPTION = 'Returns the latest stories'; + const CACHE_TIMEOUT = 1800; // 30 minutes + + const PARAMETERS = [ [ + 'filter' => [ + 'type' => 'list', + 'name' => 'Filter', + 'values' => [ + 'All' => '', + 'Right' => 'right', + 'Working' => 'working', + 'Romantic' => 'romantic', + 'Related' => 'related', + 'Learning' => 'learning', + 'Friendly' => 'friendly', + 'Hopeless' => 'hopeless', + 'Unfiltered' => 'unfiltered' + ] + ] + ]]; + + public function getIcon() + { + return self::URI . 'favicon_nar.png'; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + foreach ($html->find('.post') as $post) { + #print_r($post); + $item = []; + $item['uri'] = $post->find('h1', 0)->find('a', 0)->href; + $item['content'] = $post; + $item['title'] = $post->find('h1', 0)->find('a', 0)->innertext; + $this->items[] = $item; + } + } + + public function getName() + { + if (!is_null($this->getInput('filter'))) { + return $this->getInput('filter') . ' - NotAlways Bridge'; + } + + return parent::getName(); + } + + public function getURI() + { + if (!is_null($this->getInput('filter'))) { + return self::URI . $this->getInput('filter') . '/'; + } + + return parent::getURI(); + } } diff --git a/bridges/NovayaGazetaEuropeBridge.php b/bridges/NovayaGazetaEuropeBridge.php index c7511a31..fb8c0e77 100644 --- a/bridges/NovayaGazetaEuropeBridge.php +++ b/bridges/NovayaGazetaEuropeBridge.php @@ -1,141 +1,145 @@ <?php + class NovayaGazetaEuropeBridge extends BridgeAbstract { + const MAINTAINER = 'sqrtminusone'; + const NAME = 'Novaya Gazeta Europe Bridge'; + const URI = 'https://novayagazeta.eu'; - const MAINTAINER = 'sqrtminusone'; - const NAME = 'Novaya Gazeta Europe Bridge'; - const URI = 'https://novayagazeta.eu'; - - const CACHE_TIMEOUT = 3600; // 1 hour - const DESCRIPTION = 'Returns articles from Novaya Gazeta Europe'; + const CACHE_TIMEOUT = 3600; // 1 hour + const DESCRIPTION = 'Returns articles from Novaya Gazeta Europe'; - const PARAMETERS = array( - '' => array( - 'language' => array( - 'name' => 'Language', - 'type' => 'list', - 'defaultValue' => 'ru', - 'values' => array( - 'Russian' => 'ru', - 'English' => 'en', - ) - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Maximum number of items to return', - 'defaultValue' => 20 - ) - ) - ); + const PARAMETERS = [ + '' => [ + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'defaultValue' => 'ru', + 'values' => [ + 'Russian' => 'ru', + 'English' => 'en', + ] + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Maximum number of items to return', + 'defaultValue' => 20 + ] + ] + ]; - public function collectData() - { - $url = 'https://novayagazeta.eu/api/v1/get/main'; - if ($this->getInput('language') != 'ru') { - $url .= '?lang=' . $this->getInput('language'); - } + public function collectData() + { + $url = 'https://novayagazeta.eu/api/v1/get/main'; + if ($this->getInput('language') != 'ru') { + $url .= '?lang=' . $this->getInput('language'); + } - $json = getContents($url); - $data = json_decode($json); + $json = getContents($url); + $data = json_decode($json); - foreach ($data->records as $record) { - foreach ($record->blocks as $block) { - if (!property_exists($block, 'date')) { - continue; - } - $title = strip_tags($block->title); - if (!empty($block->subtitle)) { - $title .= '. ' . strip_tags($block->subtitle); - } - $item = array( - 'uri' => self::URI . '/articles/' . $block->slug, - 'block' => $block, - 'title' => $title, - 'author' => join(', ', array_map(function ($author) { - return $author->name; - }, $block->authors)), - 'timestamp' => $block->date / 1000, - 'categories' => $block->tags - ); - $this->items[] = $item; - } - } - usort($this->items, function ($item1, $item2) { - return $item2['timestamp'] <=> $item1['timestamp']; - }); - if ($this->getInput('limit') !== null) { - $this->items = array_slice($this->items, 0, $this->getInput('limit')); - } - foreach ($this->items as &$item) { - $block = $item['block']; - $body = ''; - if (property_exists($block, 'body') && $block->body !== null) { - $body = self::convertBody($block); - } else { - $record_json = getContents("https://novayagazeta.eu/api/v1/get/record?slug={$block->slug}"); - $record_data = json_decode($record_json); - $body = self::convertBody($record_data->record); - } - $item['content'] = $body; - unset($item['block']); - } - } + foreach ($data->records as $record) { + foreach ($record->blocks as $block) { + if (!property_exists($block, 'date')) { + continue; + } + $title = strip_tags($block->title); + if (!empty($block->subtitle)) { + $title .= '. ' . strip_tags($block->subtitle); + } + $item = [ + 'uri' => self::URI . '/articles/' . $block->slug, + 'block' => $block, + 'title' => $title, + 'author' => join(', ', array_map(function ($author) { + return $author->name; + }, $block->authors)), + 'timestamp' => $block->date / 1000, + 'categories' => $block->tags + ]; + $this->items[] = $item; + } + } + usort($this->items, function ($item1, $item2) { + return $item2['timestamp'] <=> $item1['timestamp']; + }); + if ($this->getInput('limit') !== null) { + $this->items = array_slice($this->items, 0, $this->getInput('limit')); + } + foreach ($this->items as &$item) { + $block = $item['block']; + $body = ''; + if (property_exists($block, 'body') && $block->body !== null) { + $body = self::convertBody($block); + } else { + $record_json = getContents("https://novayagazeta.eu/api/v1/get/record?slug={$block->slug}"); + $record_data = json_decode($record_json); + $body = self::convertBody($record_data->record); + } + $item['content'] = $body; + unset($item['block']); + } + } - private function convertBody($data) { - $body = ''; - if ($data->previewUrl !== null && !$data->isPreviewHidden) { - $body .= '<figure><img src="' . $data->previewUrl . '"/>'; - if ($data->previewCaption !== null) { - $body .= '<figcaption>' . $data->previewCaption . '</figcaption>'; - } - $body .= '</figure>'; - } - if ($data->lead !== null) { - $body .= "<p><b>{$data->lead}</b></p>"; - } - if (!empty($data->body)) { - foreach ($data->body as $datum) { - $body .= self::convertElement($datum); - } - } - return $body; - } + private function convertBody($data) + { + $body = ''; + if ($data->previewUrl !== null && !$data->isPreviewHidden) { + $body .= '<figure><img src="' . $data->previewUrl . '"/>'; + if ($data->previewCaption !== null) { + $body .= '<figcaption>' . $data->previewCaption . '</figcaption>'; + } + $body .= '</figure>'; + } + if ($data->lead !== null) { + $body .= "<p><b>{$data->lead}</b></p>"; + } + if (!empty($data->body)) { + foreach ($data->body as $datum) { + $body .= self::convertElement($datum); + } + } + return $body; + } - private function convertElement($datum) { - switch ($datum->type) { - case 'text': - return $datum->data; - case 'image/single': - $alt = strip_tags($datum->data); - $res = "<figure><img src=\"{$datum->previewUrl}\" alt=\"{$alt}\" />"; - if ($datum->data !== null) { - $res .= "<figcaption>{$datum->data}</figcaption>"; - } - $res .= '</figure>'; - return $res; - case 'text/quote': - return "<figure><blockquote>{$datum->data}</blockquote></figure><br>"; - case 'embed/native': - $desc = $datum->link; - if (property_exists($datum, 'caption')) { - $desc = $datum->caption; - } - return "<p><a link=\"{$datum->link}\">{$desc}</a></p>"; - case 'text/framed': - $res = ''; - if (property_exists($datum, 'typeDisplay')) { - $res .= "<p><b>{$datum->typeDisplay}</b></p>"; - } - $res .= "<p>{$datum->data}</p>"; - if (property_exists($datum, 'attachment') - && property_exists($datum->attachment, 'type')) { - $res .= self::convertElement($datum->attachment); - } - return $res; - default: - return ''; - } - } + private function convertElement($datum) + { + switch ($datum->type) { + case 'text': + return $datum->data; + case 'image/single': + $alt = strip_tags($datum->data); + $res = "<figure><img src=\"{$datum->previewUrl}\" alt=\"{$alt}\" />"; + if ($datum->data !== null) { + $res .= "<figcaption>{$datum->data}</figcaption>"; + } + $res .= '</figure>'; + return $res; + case 'text/quote': + return "<figure><blockquote>{$datum->data}</blockquote></figure><br>"; + case 'embed/native': + $desc = $datum->link; + if (property_exists($datum, 'caption')) { + $desc = $datum->caption; + } + return "<p><a link=\"{$datum->link}\">{$desc}</a></p>"; + case 'text/framed': + $res = ''; + if (property_exists($datum, 'typeDisplay')) { + $res .= "<p><b>{$datum->typeDisplay}</b></p>"; + } + $res .= "<p>{$datum->data}</p>"; + if ( + property_exists($datum, 'attachment') + && property_exists($datum->attachment, 'type') + ) { + $res .= self::convertElement($datum->attachment); + } + return $res; + default: + return ''; + } + } } diff --git a/bridges/NovelUpdatesBridge.php b/bridges/NovelUpdatesBridge.php index 60d3fa5d..62e5f5b8 100644 --- a/bridges/NovelUpdatesBridge.php +++ b/bridges/NovelUpdatesBridge.php @@ -1,68 +1,72 @@ <?php -class NovelUpdatesBridge extends BridgeAbstract { - const MAINTAINER = 'albirew'; - const NAME = 'Novel Updates'; - const URI = 'https://www.novelupdates.com/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Returns releases from Novel Updates'; - const PARAMETERS = array( array( - 'n' => array( - 'name' => 'Novel name as found in the url', - 'exampleValue' => 'spirit-realm', - 'required' => true - ) - )); +class NovelUpdatesBridge extends BridgeAbstract +{ + const MAINTAINER = 'albirew'; + const NAME = 'Novel Updates'; + const URI = 'https://www.novelupdates.com/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Returns releases from Novel Updates'; + const PARAMETERS = [ [ + 'n' => [ + 'name' => 'Novel name as found in the url', + 'exampleValue' => 'spirit-realm', + 'required' => true + ] + ]]; - private $seriesTitle = ''; + private $seriesTitle = ''; - public function getURI(){ - if(!is_null($this->getInput('n'))) { - return static::URI . '/series/' . $this->getInput('n') . '/'; - } + public function getURI() + { + if (!is_null($this->getInput('n'))) { + return static::URI . '/series/' . $this->getInput('n') . '/'; + } - return parent::getURI(); - } + return parent::getURI(); + } - public function collectData(){ - $fullhtml = getSimpleHTMLDOM($this->getURI()); + public function collectData() + { + $fullhtml = getSimpleHTMLDOM($this->getURI()); - $this->seriesTitle = $fullhtml->find('h4.seriestitle', 0)->plaintext; - // dirty fix for nasty simpledom bug: https://github.com/sebsauvage/rss-bridge/issues/259 - // forcefully removes tbody - $html = $fullhtml->find('table#myTable', 0)->innertext; - $html = stristr($html, '<tbody>'); //strip thead - $html = stristr($html, '<tr>'); //remove tbody - $html = str_get_html(stristr($html, '</tbody>', true)); //remove last tbody and get back as an array - foreach($html->find('tr') as $element) { - $item = array(); - $item['uri'] = $element->find('td', 2)->find('a', 0)->href; - $item['title'] = $element->find('td', 2)->find('a', 0)->plaintext; - $item['team'] = $element->find('td', 1)->innertext; - $item['timestamp'] = strtotime($element->find('td', 0)->plaintext); - $item['content'] = '<a href="' - . $item['uri'] - . '">' - . $this->seriesTitle - . ' - ' - . $item['title'] - . '</a> by ' - . $item['team'] - . '<br><a href="' - . $item['uri'] - . '">' - . $fullhtml->find('div.seriesimg', 0)->innertext - . '</a>'; + $this->seriesTitle = $fullhtml->find('h4.seriestitle', 0)->plaintext; + // dirty fix for nasty simpledom bug: https://github.com/sebsauvage/rss-bridge/issues/259 + // forcefully removes tbody + $html = $fullhtml->find('table#myTable', 0)->innertext; + $html = stristr($html, '<tbody>'); //strip thead + $html = stristr($html, '<tr>'); //remove tbody + $html = str_get_html(stristr($html, '</tbody>', true)); //remove last tbody and get back as an array + foreach ($html->find('tr') as $element) { + $item = []; + $item['uri'] = $element->find('td', 2)->find('a', 0)->href; + $item['title'] = $element->find('td', 2)->find('a', 0)->plaintext; + $item['team'] = $element->find('td', 1)->innertext; + $item['timestamp'] = strtotime($element->find('td', 0)->plaintext); + $item['content'] = '<a href="' + . $item['uri'] + . '">' + . $this->seriesTitle + . ' - ' + . $item['title'] + . '</a> by ' + . $item['team'] + . '<br><a href="' + . $item['uri'] + . '">' + . $fullhtml->find('div.seriesimg', 0)->innertext + . '</a>'; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getName(){ - if(!empty($this->seriesTitle)) { - return $this->seriesTitle . ' - ' . static::NAME; - } + public function getName() + { + if (!empty($this->seriesTitle)) { + return $this->seriesTitle . ' - ' . static::NAME; + } - return parent::getName(); - } + return parent::getName(); + } } diff --git a/bridges/NpciBridge.php b/bridges/NpciBridge.php index 64dab909..17567778 100644 --- a/bridges/NpciBridge.php +++ b/bridges/NpciBridge.php @@ -1,95 +1,99 @@ <?php -class NpciBridge extends BridgeAbstract { - const MAINTAINER = 'captn3m0'; - const NAME = 'NCPI Circulars'; - const URI = 'https://npci.org.in'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'Returns circulars from National Payments Corporation of India)'; +class NpciBridge extends BridgeAbstract +{ + const MAINTAINER = 'captn3m0'; + const NAME = 'NCPI Circulars'; + const URI = 'https://npci.org.in'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'Returns circulars from National Payments Corporation of India)'; - const URL_SUFFIX = [ - 'cts' => 'circulars', - 'upi' => 'circular', - 'rupay' => 'circulars', - 'nach' => 'circulars', - 'imps' => 'circular', - 'netc-fastag' => 'circulars', - '99' => 'circular', - 'nfs' => 'circulars', - 'aeps' => 'circulars', - 'bhim-aadhaar' => 'circular', - 'e-rupi' => 'circular', - 'Bharat QR' => 'circulars', - 'bharat-billpay' => 'circulars', - ]; + const URL_SUFFIX = [ + 'cts' => 'circulars', + 'upi' => 'circular', + 'rupay' => 'circulars', + 'nach' => 'circulars', + 'imps' => 'circular', + 'netc-fastag' => 'circulars', + '99' => 'circular', + 'nfs' => 'circulars', + 'aeps' => 'circulars', + 'bhim-aadhaar' => 'circular', + 'e-rupi' => 'circular', + 'Bharat QR' => 'circulars', + 'bharat-billpay' => 'circulars', + ]; - const PARAMETERS = [[ - 'product' => [ - 'name' => 'product', - 'type' => 'list', - 'values' => [ - 'CTS' => 'cts', - 'UPI' => 'upi', - 'RuPay' => 'rupay', - 'NACH' => 'nach', - 'IMPS' => 'imps', - 'NETC FASTag' => 'netc-fastag', - '*99#' => '99', - 'NFS' => 'nfs', - 'AePS' => 'aeps', - 'BHIM Aadhaar' => 'bhim-aadhaar', - 'e-RUPI' => 'e-rupi', - 'Bharat BillPay' => 'bharat-billpay' - ] - ] - ]]; + const PARAMETERS = [[ + 'product' => [ + 'name' => 'product', + 'type' => 'list', + 'values' => [ + 'CTS' => 'cts', + 'UPI' => 'upi', + 'RuPay' => 'rupay', + 'NACH' => 'nach', + 'IMPS' => 'imps', + 'NETC FASTag' => 'netc-fastag', + '*99#' => '99', + 'NFS' => 'nfs', + 'AePS' => 'aeps', + 'BHIM Aadhaar' => 'bhim-aadhaar', + 'e-RUPI' => 'e-rupi', + 'Bharat BillPay' => 'bharat-billpay' + ] + ] + ]]; - public function getName() { - $product = $this->getInput('product'); - if ($product) { - $productNameMap = array_flip(self::PARAMETERS[0]['product']['values']); - $productName = $productNameMap[$product]; - return "NPCI Circulars: $productName"; - } + public function getName() + { + $product = $this->getInput('product'); + if ($product) { + $productNameMap = array_flip(self::PARAMETERS[0]['product']['values']); + $productName = $productNameMap[$product]; + return "NPCI Circulars: $productName"; + } - return 'NPCI Circulars'; - } + return 'NPCI Circulars'; + } - public function getURI(){ - $product = $this->getInput('product'); - return $product ? sprintf('%s/what-we-do/%s/%s', self::URI, $product, self::URL_SUFFIX[$product]) : self::URI; - } + public function getURI() + { + $product = $this->getInput('product'); + return $product ? sprintf('%s/what-we-do/%s/%s', self::URI, $product, self::URL_SUFFIX[$product]) : self::URI; + } - public function collectData(){ - $html = getSimpleHTMLDOMCached($this->getURI()); - $year = date('Y'); - $elements = $html->find("div[id=year$year] .pdf-item"); + public function collectData() + { + $html = getSimpleHTMLDOMCached($this->getURI()); + $year = date('Y'); + $elements = $html->find("div[id=year$year] .pdf-item"); - foreach($elements as $element) { - $title = $element->find('p', 0)->innertext; + foreach ($elements as $element) { + $title = $element->find('p', 0)->innertext; - $link = $element->find('a', 0); + $link = $element->find('a', 0); - $uri = null; + $uri = null; - if ($link) { - $pdfLink = $link->getAttribute('href'); - $uri = self::URI . str_replace(' ', '+', $pdfLink); - } + if ($link) { + $pdfLink = $link->getAttribute('href'); + $uri = self::URI . str_replace(' ', '+', $pdfLink); + } - $item = [ - 'uri' => $uri, - 'title' => $title, - 'content' => $title , - 'uid' => sha1($pdfLink), - 'enclosures' => [ - $uri - ] - ]; + $item = [ + 'uri' => $uri, + 'title' => $title, + 'content' => $title , + 'uid' => sha1($pdfLink), + 'enclosures' => [ + $uri + ] + ]; - $this->items[] = $item; - } + $this->items[] = $item; + } - $this->items = array_slice($this->items, 0, 15); - } + $this->items = array_slice($this->items, 0, 15); + } } diff --git a/bridges/NyaaTorrentsBridge.php b/bridges/NyaaTorrentsBridge.php index 1ce1a027..e281b79d 100644 --- a/bridges/NyaaTorrentsBridge.php +++ b/bridges/NyaaTorrentsBridge.php @@ -1,107 +1,112 @@ <?php -class NyaaTorrentsBridge extends FeedExpander { - const MAINTAINER = 'ORelio'; - const NAME = 'NyaaTorrents'; - const URI = 'https://nyaa.si/'; - const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.'; - const PARAMETERS = array( - array( - 'f' => array( - 'name' => 'Filter', - 'type' => 'list', - 'values' => array( - 'No filter' => '0', - 'No remakes' => '1', - 'Trusted only' => '2' - ) - ), - 'c' => array( - 'name' => 'Category', - 'type' => 'list', - 'values' => array( - 'All categories' => '0_0', - 'Anime' => '1_0', - 'Anime - AMV' => '1_1', - 'Anime - English' => '1_2', - 'Anime - Non-English' => '1_3', - 'Anime - Raw' => '1_4', - 'Audio' => '2_0', - 'Audio - Lossless' => '2_1', - 'Audio - Lossy' => '2_2', - 'Literature' => '3_0', - 'Literature - English' => '3_1', - 'Literature - Non-English' => '3_2', - 'Literature - Raw' => '3_3', - 'Live Action' => '4_0', - 'Live Action - English' => '4_1', - 'Live Action - Idol/PV' => '4_2', - 'Live Action - Non-English' => '4_3', - 'Live Action - Raw' => '4_4', - 'Pictures' => '5_0', - 'Pictures - Graphics' => '5_1', - 'Pictures - Photos' => '5_2', - 'Software' => '6_0', - 'Software - Apps' => '6_1', - 'Software - Games' => '6_2', - ) - ), - 'q' => array( - 'name' => 'Keyword', - 'description' => 'Keyword(s)', - 'type' => 'text' - ), - 'u' => array( - 'name' => 'User', - 'description' => 'User', - 'type' => 'text' - ) - ) - ); +class NyaaTorrentsBridge extends FeedExpander +{ + const MAINTAINER = 'ORelio'; + const NAME = 'NyaaTorrents'; + const URI = 'https://nyaa.si/'; + const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.'; + const PARAMETERS = [ + [ + 'f' => [ + 'name' => 'Filter', + 'type' => 'list', + 'values' => [ + 'No filter' => '0', + 'No remakes' => '1', + 'Trusted only' => '2' + ] + ], + 'c' => [ + 'name' => 'Category', + 'type' => 'list', + 'values' => [ + 'All categories' => '0_0', + 'Anime' => '1_0', + 'Anime - AMV' => '1_1', + 'Anime - English' => '1_2', + 'Anime - Non-English' => '1_3', + 'Anime - Raw' => '1_4', + 'Audio' => '2_0', + 'Audio - Lossless' => '2_1', + 'Audio - Lossy' => '2_2', + 'Literature' => '3_0', + 'Literature - English' => '3_1', + 'Literature - Non-English' => '3_2', + 'Literature - Raw' => '3_3', + 'Live Action' => '4_0', + 'Live Action - English' => '4_1', + 'Live Action - Idol/PV' => '4_2', + 'Live Action - Non-English' => '4_3', + 'Live Action - Raw' => '4_4', + 'Pictures' => '5_0', + 'Pictures - Graphics' => '5_1', + 'Pictures - Photos' => '5_2', + 'Software' => '6_0', + 'Software - Apps' => '6_1', + 'Software - Games' => '6_2', + ] + ], + 'q' => [ + 'name' => 'Keyword', + 'description' => 'Keyword(s)', + 'type' => 'text' + ], + 'u' => [ + 'name' => 'User', + 'description' => 'User', + 'type' => 'text' + ] + ] + ]; - public function getIcon() { - return self::URI . 'static/favicon.png'; - } + public function getIcon() + { + return self::URI . 'static/favicon.png'; + } - public function collectData(){ - $this->collectExpandableDatas( - self::URI . '?page=rss&s=id&o=desc&' - . http_build_query(array( - 'f' => $this->getInput('f'), - 'c' => $this->getInput('c'), - 'q' => $this->getInput('q'), - 'u' => $this->getInput('u') - )), 20); - } + public function collectData() + { + $this->collectExpandableDatas( + self::URI . '?page=rss&s=id&o=desc&' + . http_build_query([ + 'f' => $this->getInput('f'), + 'c' => $this->getInput('c'), + 'q' => $this->getInput('q'), + 'u' => $this->getInput('u') + ]), + 20 + ); + } - protected function parseItem($newItem){ - $item = parent::parseItem($newItem); + protected function parseItem($newItem) + { + $item = parent::parseItem($newItem); - //Convert URI from torrent file to web page - $item['uri'] = str_replace('/download/', '/view/', $item['uri']); - $item['uri'] = str_replace('.torrent', '', $item['uri']); + //Convert URI from torrent file to web page + $item['uri'] = str_replace('/download/', '/view/', $item['uri']); + $item['uri'] = str_replace('.torrent', '', $item['uri']); - if ($item_html = getSimpleHTMLDOMCached($item['uri'])) { + if ($item_html = getSimpleHTMLDOMCached($item['uri'])) { + //Retrieve full description from page contents + $item_desc = str_get_html( + markdownToHtml(html_entity_decode($item_html->find('#torrent-description', 0)->innertext)) + ); - //Retrieve full description from page contents - $item_desc = str_get_html( - markdownToHtml(html_entity_decode($item_html->find('#torrent-description', 0)->innertext)) - ); + //Retrieve image for thumbnail or generic logo fallback + $item_image = $this->getURI() . 'static/img/avatar/default.png'; + foreach ($item_desc->find('img') as $img) { + if (strpos($img->src, 'prez') === false) { + $item_image = $img->src; + break; + } + } - //Retrieve image for thumbnail or generic logo fallback - $item_image = $this->getURI() . 'static/img/avatar/default.png'; - foreach ($item_desc->find('img') as $img) { - if (strpos($img->src, 'prez') === false) { - $item_image = $img->src; - break; - } - } + //Add expanded fields to the current item + $item['enclosures'] = [$item_image]; + $item['content'] = $item_desc; + } - //Add expanded fields to the current item - $item['enclosures'] = array($item_image); - $item['content'] = $item_desc; - } - - return $item; - } + return $item; + } } diff --git a/bridges/OnVaSortirBridge.php b/bridges/OnVaSortirBridge.php index ed1dcb65..af80dd31 100644 --- a/bridges/OnVaSortirBridge.php +++ b/bridges/OnVaSortirBridge.php @@ -1,130 +1,134 @@ <?php -class OnVaSortirBridge extends FeedExpander { - const MAINTAINER = 'AntoineTurmel'; - const NAME = 'OnVaSortir'; - const URI = 'https://www.onvasortir.com'; - const DESCRIPTION = 'Returns the newest events from OnVaSortir (full text)'; - const PARAMETERS = array( - array( - 'city' => array( - 'name' => 'City', - 'type' => 'list', - 'values' => array( - 'Agen' => 'Agen', - 'Ajaccio' => 'Ajaccio', - 'Albi' => 'Albi', - 'Amiens' => 'Amiens', - 'Angers' => 'Angers', - 'Angoulême' => 'Angouleme', - 'Annecy' => 'annecy', - 'Aurillac' => 'aurillac', - 'Auxerre' => 'auxerre', - 'Avignon' => 'avignon', - 'Béziers' => 'Beziers', - 'Bastia' => 'Bastia', - 'Beauvais' => 'Beauvais', - 'Belfort' => 'Belfort', - 'Bergerac' => 'bergerac', - 'Besançon' => 'Besancon', - 'Biarritz' => 'Biarritz', - 'Blois' => 'Blois', - 'Bordeaux' => 'bordeaux', - 'Bourg-en-Bresse' => 'bourg-en-bresse', - 'Bourges' => 'Bourges', - 'Brest' => 'Brest', - 'Brive' => 'brive-la-gaillarde', - 'Bruxelles' => 'bruxelles', - 'Caen' => 'Caen', - 'Calais' => 'Calais', - 'Carcassonne' => 'Carcassonne', - 'Châteauroux' => 'Chateauroux', - 'Chalon-sur-saone' => 'chalon-sur-saone', - 'Chambéry' => 'chambery', - 'Chantilly' => 'chantilly', - 'Charleroi' => 'charleroi', - 'Charleville-Mézières' => 'Charleville-Mezieres', - 'Chartres' => 'Chartres', - 'Cherbourg' => 'Cherbourg', - 'Cholet' => 'cholet', - 'Clermont-Ferrand' => 'Clermont-Ferrand', - 'Compiègne' => 'compiegne', - 'Dieppe' => 'dieppe', - 'Dijon' => 'Dijon', - 'Dunkerque' => 'Dunkerque', - 'Evreux' => 'evreux', - 'Fréjus' => 'frejus', - 'Gap' => 'gap', - 'Genève' => 'geneve', - 'Grenoble' => 'Grenoble', - 'La Roche sur Yon' => 'La-Roche-sur-Yon', - 'La Rochelle' => 'La-Rochelle', - 'Lausanne' => 'lausanne', - 'Laval' => 'Laval', - 'Le Havre' => 'le-havre', - 'Le Mans' => 'le-mans', - 'Liège' => 'liege', - 'Lille' => 'lille', - 'Limoges' => 'Limoges', - 'Lorient' => 'Lorient', - 'Luxembourg' => 'Luxembourg', - 'Lyon' => 'lyon', - 'Marseille' => 'marseille', - 'Metz' => 'Metz', - 'Mons' => 'Mons', - 'Mont de Marsan' => 'mont-de-marsan', - 'Montauban' => 'Montauban', - 'Montluçon' => 'montlucon', - 'Montpellier' => 'montpellier', - 'Mulhouse' => 'Mulhouse', - 'Nîmes' => 'nimes', - 'Namur' => 'Namur', - 'Nancy' => 'Nancy', - 'Nantes' => 'nantes', - 'Nevers' => 'nevers', - 'Nice' => 'nice', - 'Niort' => 'niort', - 'Orléans' => 'orleans', - 'Périgueux' => 'perigueux', - 'Paris' => 'paris', - 'Pau' => 'Pau', - 'Perpignan' => 'Perpignan', - 'Poitiers' => 'Poitiers', - 'Quimper' => 'Quimper', - 'Reims' => 'Reims', - 'Rennes' => 'Rennes', - 'Roanne' => 'roanne', - 'Rodez' => 'rodez', - 'Rouen' => 'Rouen', - 'Saint-Brieuc' => 'Saint-Brieuc', - 'Saint-Etienne' => 'saint-etienne', - 'Saint-Malo' => 'saint-malo', - 'Saint-Nazaire' => 'saint-nazaire', - 'Saint-Quentin' => 'saint-quentin', - 'Saintes' => 'saintes', - 'Strasbourg' => 'Strasbourg', - 'Tarbes' => 'Tarbes', - 'Toulon' => 'Toulon', - 'Toulouse' => 'Toulouse', - 'Tours' => 'Tours', - 'Troyes' => 'troyes', - 'Valence' => 'valence', - 'Vannes' => 'vannes', - 'Zurich' => 'zurich', - ) - ) - ) - ); - protected function parseItem($item){ - $item = parent::parseItem($item); - $html = getSimpleHTMLDOMCached($item['uri']); - $text = $html->find('div.corpsMax', 0)->innertext; - $item['content'] = utf8_encode($text); - return $item; - } +class OnVaSortirBridge extends FeedExpander +{ + const MAINTAINER = 'AntoineTurmel'; + const NAME = 'OnVaSortir'; + const URI = 'https://www.onvasortir.com'; + const DESCRIPTION = 'Returns the newest events from OnVaSortir (full text)'; + const PARAMETERS = [ + [ + 'city' => [ + 'name' => 'City', + 'type' => 'list', + 'values' => [ + 'Agen' => 'Agen', + 'Ajaccio' => 'Ajaccio', + 'Albi' => 'Albi', + 'Amiens' => 'Amiens', + 'Angers' => 'Angers', + 'Angoulême' => 'Angouleme', + 'Annecy' => 'annecy', + 'Aurillac' => 'aurillac', + 'Auxerre' => 'auxerre', + 'Avignon' => 'avignon', + 'Béziers' => 'Beziers', + 'Bastia' => 'Bastia', + 'Beauvais' => 'Beauvais', + 'Belfort' => 'Belfort', + 'Bergerac' => 'bergerac', + 'Besançon' => 'Besancon', + 'Biarritz' => 'Biarritz', + 'Blois' => 'Blois', + 'Bordeaux' => 'bordeaux', + 'Bourg-en-Bresse' => 'bourg-en-bresse', + 'Bourges' => 'Bourges', + 'Brest' => 'Brest', + 'Brive' => 'brive-la-gaillarde', + 'Bruxelles' => 'bruxelles', + 'Caen' => 'Caen', + 'Calais' => 'Calais', + 'Carcassonne' => 'Carcassonne', + 'Châteauroux' => 'Chateauroux', + 'Chalon-sur-saone' => 'chalon-sur-saone', + 'Chambéry' => 'chambery', + 'Chantilly' => 'chantilly', + 'Charleroi' => 'charleroi', + 'Charleville-Mézières' => 'Charleville-Mezieres', + 'Chartres' => 'Chartres', + 'Cherbourg' => 'Cherbourg', + 'Cholet' => 'cholet', + 'Clermont-Ferrand' => 'Clermont-Ferrand', + 'Compiègne' => 'compiegne', + 'Dieppe' => 'dieppe', + 'Dijon' => 'Dijon', + 'Dunkerque' => 'Dunkerque', + 'Evreux' => 'evreux', + 'Fréjus' => 'frejus', + 'Gap' => 'gap', + 'Genève' => 'geneve', + 'Grenoble' => 'Grenoble', + 'La Roche sur Yon' => 'La-Roche-sur-Yon', + 'La Rochelle' => 'La-Rochelle', + 'Lausanne' => 'lausanne', + 'Laval' => 'Laval', + 'Le Havre' => 'le-havre', + 'Le Mans' => 'le-mans', + 'Liège' => 'liege', + 'Lille' => 'lille', + 'Limoges' => 'Limoges', + 'Lorient' => 'Lorient', + 'Luxembourg' => 'Luxembourg', + 'Lyon' => 'lyon', + 'Marseille' => 'marseille', + 'Metz' => 'Metz', + 'Mons' => 'Mons', + 'Mont de Marsan' => 'mont-de-marsan', + 'Montauban' => 'Montauban', + 'Montluçon' => 'montlucon', + 'Montpellier' => 'montpellier', + 'Mulhouse' => 'Mulhouse', + 'Nîmes' => 'nimes', + 'Namur' => 'Namur', + 'Nancy' => 'Nancy', + 'Nantes' => 'nantes', + 'Nevers' => 'nevers', + 'Nice' => 'nice', + 'Niort' => 'niort', + 'Orléans' => 'orleans', + 'Périgueux' => 'perigueux', + 'Paris' => 'paris', + 'Pau' => 'Pau', + 'Perpignan' => 'Perpignan', + 'Poitiers' => 'Poitiers', + 'Quimper' => 'Quimper', + 'Reims' => 'Reims', + 'Rennes' => 'Rennes', + 'Roanne' => 'roanne', + 'Rodez' => 'rodez', + 'Rouen' => 'Rouen', + 'Saint-Brieuc' => 'Saint-Brieuc', + 'Saint-Etienne' => 'saint-etienne', + 'Saint-Malo' => 'saint-malo', + 'Saint-Nazaire' => 'saint-nazaire', + 'Saint-Quentin' => 'saint-quentin', + 'Saintes' => 'saintes', + 'Strasbourg' => 'Strasbourg', + 'Tarbes' => 'Tarbes', + 'Toulon' => 'Toulon', + 'Toulouse' => 'Toulouse', + 'Tours' => 'Tours', + 'Troyes' => 'troyes', + 'Valence' => 'valence', + 'Vannes' => 'vannes', + 'Zurich' => 'zurich', + ] + ] + ] + ]; - public function collectData(){ - $this->collectExpandableDatas('https://' . - $this->getInput('city') . '.onvasortir.com/rss.php'); - } + protected function parseItem($item) + { + $item = parent::parseItem($item); + $html = getSimpleHTMLDOMCached($item['uri']); + $text = $html->find('div.corpsMax', 0)->innertext; + $item['content'] = utf8_encode($text); + return $item; + } + + public function collectData() + { + $this->collectExpandableDatas('https://' . + $this->getInput('city') . '.onvasortir.com/rss.php'); + } } diff --git a/bridges/OneFortuneADayBridge.php b/bridges/OneFortuneADayBridge.php index 62fe767d..c74f22d0 100644 --- a/bridges/OneFortuneADayBridge.php +++ b/bridges/OneFortuneADayBridge.php @@ -1,76 +1,82 @@ <?php -class OneFortuneADayBridge extends BridgeAbstract { - const NAME = 'One Fortune a Day'; - const URI = 'https://github.com/fulmeek'; - const DESCRIPTION = 'Get a fortune quote every single day.'; - const MAINTAINER = 'fulmeek'; - const PARAMETERS = array(array( - 'time' => array( - 'name' => 'Time in UTC', - 'type' => 'list', - 'values' => array( - '0:00' => 0, - '1:00' => 1, - '2:00' => 2, - '3:00' => 3, - '4:00' => 4, - '5:00' => 5, - '6:00' => 6, - '7:00' => 7, - '8:00' => 8, - '9:00' => 9, - '10:00' => 10, - '11:00' => 11, - '12:00' => 12, - '13:00' => 13, - '14:00' => 14, - '15:00' => 15, - '16:00' => 16, - '17:00' => 17, - '18:00' => 18, - '19:00' => 19, - '20:00' => 20, - '21:00' => 21, - '22:00' => 22, - '23:00' => 23, - ), - 'defaultValue' => 5 - ), - 'lucky' => array( - 'name' => 'Lucky number (optional)', - 'type' => 'text' - ) - )); - const LIMIT_ITEMS = 7; - const DAY_SECS = 86400; +class OneFortuneADayBridge extends BridgeAbstract +{ + const NAME = 'One Fortune a Day'; + const URI = 'https://github.com/fulmeek'; + const DESCRIPTION = 'Get a fortune quote every single day.'; + const MAINTAINER = 'fulmeek'; + const PARAMETERS = [[ + 'time' => [ + 'name' => 'Time in UTC', + 'type' => 'list', + 'values' => [ + '0:00' => 0, + '1:00' => 1, + '2:00' => 2, + '3:00' => 3, + '4:00' => 4, + '5:00' => 5, + '6:00' => 6, + '7:00' => 7, + '8:00' => 8, + '9:00' => 9, + '10:00' => 10, + '11:00' => 11, + '12:00' => 12, + '13:00' => 13, + '14:00' => 14, + '15:00' => 15, + '16:00' => 16, + '17:00' => 17, + '18:00' => 18, + '19:00' => 19, + '20:00' => 20, + '21:00' => 21, + '22:00' => 22, + '23:00' => 23, + ], + 'defaultValue' => 5 + ], + 'lucky' => [ + 'name' => 'Lucky number (optional)', + 'type' => 'text' + ] + ]]; - public function getDescription(){ - return self::DESCRIPTION . '<br/>Set a lucky number to get your personal quotes, like ' . mt_rand(); - } + const LIMIT_ITEMS = 7; + const DAY_SECS = 86400; - public function collectData() { - $time = gmmktime((int)$this->getInput('time'), 0, 0); - if ($time > time()) - $time -= self::DAY_SECS; + public function getDescription() + { + return self::DESCRIPTION . '<br/>Set a lucky number to get your personal quotes, like ' . mt_rand(); + } - for ($i = self::LIMIT_ITEMS; $i > 0; --$i) { - $seed = gmdate('Ymd', $time) . $this->getInput('lucky'); - $quote = $this->getQuote($seed); + public function collectData() + { + $time = gmmktime((int)$this->getInput('time'), 0, 0); + if ($time > time()) { + $time -= self::DAY_SECS; + } - $item['title'] = strftime('%A, %x', $time); - $item['content'] = htmlentities($quote, ENT_QUOTES, 'UTF-8'); - $item['timestamp'] = $time; - $item['uid'] = hash('sha1', $seed); + for ($i = self::LIMIT_ITEMS; $i > 0; --$i) { + $seed = gmdate('Ymd', $time) . $this->getInput('lucky'); + $quote = $this->getQuote($seed); - $this->items[] = $item; + $item['title'] = strftime('%A, %x', $time); + $item['content'] = htmlentities($quote, ENT_QUOTES, 'UTF-8'); + $item['timestamp'] = $time; + $item['uid'] = hash('sha1', $seed); - $time -= self::DAY_SECS; - } - } + $this->items[] = $item; - private function getQuote($seed) { - $quotes = explode('//', <<<QUOTES + $time -= self::DAY_SECS; + } + } + + private function getQuote($seed) + { + $quotes = explode('//', <<<QUOTES People are naturally attracted to you. //You learn from your mistakes... You will learn a lot today. //If you have something good in your life, don't let it go! @@ -954,9 +960,9 @@ you never try. //Working hard will make you live a happy life. //A pleasant surprise is waiting for you. QUOTES - ); + ); - $i = round(fmod(hexdec(hash('crc32', $seed)), count($quotes)), 0); - return trim(str_replace(array("\r\n", "\n", "\r"), ' ', $quotes[$i])); - } + $i = round(fmod(hexdec(hash('crc32', $seed)), count($quotes)), 0); + return trim(str_replace(["\r\n", "\n", "\r"], ' ', $quotes[$i])); + } } diff --git a/bridges/OpenlyBridge.php b/bridges/OpenlyBridge.php index 3395905d..9f54e22a 100644 --- a/bridges/OpenlyBridge.php +++ b/bridges/OpenlyBridge.php @@ -1,247 +1,255 @@ <?php -class OpenlyBridge extends BridgeAbstract { - const NAME = 'Openly Bridge'; - const URI = 'https://www.openlynews.com/'; - const DESCRIPTION = 'Returns news articles'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - 'All News' => array(), - 'All Opinion' => array(), - 'By Region' => array( - 'region' => array( - 'name' => 'Region', - 'type' => 'list', - 'values' => array( - 'Africa' => 'africa', - 'Asia Pacific' => 'asia-pacific', - 'Europe' => 'europe', - 'Latin America' => 'latin-america', - 'Middle Easta' => 'middle-east', - 'North America' => 'north-america' - ) - ), - 'content' => array( - 'name' => 'Content', - 'type' => 'list', - 'values' => array( - 'News' => 'news', - 'Opinion' => 'people' - ), - 'defaultValue' => 'news' - ) - ), - 'By Tag' => array( - 'tag' => array( - 'name' => 'Tag', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'lgbt-law', - ), - 'content' => array( - 'name' => 'Content', - 'type' => 'list', - 'values' => array( - 'News' => 'news', - 'Opinion' => 'people' - ), - 'defaultValue' => 'news' - ) - ), - 'By Author' => array( - 'profileId' => array( - 'name' => 'Profile ID', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '003D000002WZGYRIA5', - ) - ) - ); - - const TEST_DETECT_PARAMETERS = array( - 'https://www.openlynews.com/profile/?id=0033z00002XUTepAAH' => array( - 'context' => 'By Author', 'profileId' => '0033z00002XUTepAAH' - ), - 'https://www.openlynews.com/news/?page=1&theme=lgbt-law' => array( - 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law' - ), - 'https://www.openlynews.com/news/?page=1®ion=north-america' => array( - 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america' - ), - 'https://www.openlynews.com/news/?theme=lgbt-law' => array( - 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law' - ), - 'https://www.openlynews.com/news/?region=north-america' => array( - 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america' - ) - ); - - const CACHE_TIMEOUT = 900; // 15 mins - const ARTICLE_CACHE_TIMEOUT = 3600; // 1 hour - - private $feedTitle = ''; - private $itemLimit = 10; - - private $profileUrlRegex = '/openlynews\.com\/profile\/\?id=([a-zA-Z0-9]+)/'; - private $tagUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?theme=([\w-]+)/'; - private $regionUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?region=([\w-]+)/'; - - public function detectParameters($url) { - $params = array(); - - if(preg_match($this->profileUrlRegex, $url, $matches) > 0) { - $params['context'] = 'By Author'; - $params['profileId'] = $matches[1]; - return $params; - } - - if(preg_match($this->tagUrlRegex, $url, $matches) > 0) { - $params['context'] = 'By Tag'; - $params['content'] = $matches[1]; - $params['tag'] = $matches[2]; - return $params; - } - - if(preg_match($this->regionUrlRegex, $url, $matches) > 0) { - $params['context'] = 'By Region'; - $params['content'] = $matches[1]; - $params['region'] = $matches[2]; - return $params; - } - - return null; - } - - public function collectData() { - $url = $this->getAjaxURI(); - - if ($this->queriedContext === 'By Author') { - $url = $this->getURI(); - } - - $html = getSimpleHTMLDOM($url); - $html = defaultLinkTo($html, $this->getURI()); - - if ($html->find('h1', 0)) { - $this->feedTitle = $html->find('h1', 0)->plaintext; - } - - if ($html->find('h2.title-v4', 0)) { - $html->find('span.tooltiptext', 0)->innertext = ''; - $this->feedTitle = $html->find('a.tooltipitem', 0)->plaintext; - } - - $items = $html->find('div.item'); - $limit = 5; - foreach(array_slice($items, 0, $limit) as $div) { - $this->items[] = $this->getArticle($div->find('a', 0)->href); - - if (count($this->items) >= $this->itemLimit) { - break; - } - } - } - - public function getURI() { - switch ($this->queriedContext) { - case 'All News': - return self::URI . 'news'; - break; - case 'All Opinion': - return self::URI . 'people'; - break; - case 'By Tag': - return self::URI . $this->getInput('content') . '/?theme=' . $this->getInput('tag'); - case 'By Region': - return self::URI . $this->getInput('content') . '/?region=' . $this->getInput('region'); - break; - case 'By Author': - return self::URI . 'profile/?id=' . $this->getInput('profileId'); - break; - default: - return parent::getURI(); - } - } - - public function getName() { - switch ($this->queriedContext) { - case 'All News': - return 'News - Openly'; - break; - case 'All Opinion': - return 'Opinion - Openly'; - break; - case 'By Tag': - if (empty($this->feedTitle)) { - $this->feedTitle = $this->getInput('tag'); - } - - if ($this->getInput('content') === 'people') { - return $this->feedTitle . ' - Opinion - Openly'; - } - - return $this->feedTitle . ' - Openly'; - break; - case 'By Region': - if (empty($this->feedTitle)) { - $this->feedTitle = $this->getInput('region'); - } - - if ($this->getInput('content') === 'people') { - return $this->feedTitle . ' - Opinion - Openly'; - } - - return $this->feedTitle . ' - Openly'; - break; - case 'By Author': - if (empty($this->feedTitle)) { - $this->feedTitle = $this->getInput('profileId'); - } - - return $this->feedTitle . ' - Author - Openly'; - break; - default: - return parent::getName(); - } - } - - private function getAjaxURI() { - $part = '/ajax.html?'; - - switch ($this->queriedContext) { - case 'All News': - return self::URI . 'news' . $part; - break; - case 'All Opinion': - return self::URI . 'people' . $part; - break; - case 'By Tag': - return self::URI . $this->getInput('content') . $part . 'theme=' . $this->getInput('tag'); - break; - case 'By Region': - return self::URI . $this->getInput('content') . $part . 'region=' . $this->getInput('region'); - break; - } - } - - private function getArticle($url) { - $article = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT); - $article = defaultLinkTo($article, $this->getURI()); - - $item = array(); - $item['title'] = $article->find('h1', 0)->plaintext; - $item['uri'] = $url; - $item['content'] = $article->find('div.body-text', 0); - $item['enclosures'][] = $article->find('meta[name="twitter:image"]', 0)->content; - $item['timestamp'] = $article->find('div.meta.small', 0)->plaintext; - - if ($article->find('div.meta a', 0)) { - $item['author'] = $article->find('div.meta a', 0)->plaintext; - } - - foreach($article->find('div.themes li') as $li) { - $item['categories'][] = trim(htmlspecialchars($li->plaintext, ENT_QUOTES)); - } - - return $item; - } + +class OpenlyBridge extends BridgeAbstract +{ + const NAME = 'Openly Bridge'; + const URI = 'https://www.openlynews.com/'; + const DESCRIPTION = 'Returns news articles'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + 'All News' => [], + 'All Opinion' => [], + 'By Region' => [ + 'region' => [ + 'name' => 'Region', + 'type' => 'list', + 'values' => [ + 'Africa' => 'africa', + 'Asia Pacific' => 'asia-pacific', + 'Europe' => 'europe', + 'Latin America' => 'latin-america', + 'Middle Easta' => 'middle-east', + 'North America' => 'north-america' + ] + ], + 'content' => [ + 'name' => 'Content', + 'type' => 'list', + 'values' => [ + 'News' => 'news', + 'Opinion' => 'people' + ], + 'defaultValue' => 'news' + ] + ], + 'By Tag' => [ + 'tag' => [ + 'name' => 'Tag', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'lgbt-law', + ], + 'content' => [ + 'name' => 'Content', + 'type' => 'list', + 'values' => [ + 'News' => 'news', + 'Opinion' => 'people' + ], + 'defaultValue' => 'news' + ] + ], + 'By Author' => [ + 'profileId' => [ + 'name' => 'Profile ID', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '003D000002WZGYRIA5', + ] + ] + ]; + + const TEST_DETECT_PARAMETERS = [ + 'https://www.openlynews.com/profile/?id=0033z00002XUTepAAH' => [ + 'context' => 'By Author', 'profileId' => '0033z00002XUTepAAH' + ], + 'https://www.openlynews.com/news/?page=1&theme=lgbt-law' => [ + 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law' + ], + 'https://www.openlynews.com/news/?page=1®ion=north-america' => [ + 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america' + ], + 'https://www.openlynews.com/news/?theme=lgbt-law' => [ + 'context' => 'By Tag', 'content' => 'news', 'tag' => 'lgbt-law' + ], + 'https://www.openlynews.com/news/?region=north-america' => [ + 'context' => 'By Region', 'content' => 'news', 'region' => 'north-america' + ] + ]; + + const CACHE_TIMEOUT = 900; // 15 mins + const ARTICLE_CACHE_TIMEOUT = 3600; // 1 hour + + private $feedTitle = ''; + private $itemLimit = 10; + + private $profileUrlRegex = '/openlynews\.com\/profile\/\?id=([a-zA-Z0-9]+)/'; + private $tagUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?theme=([\w-]+)/'; + private $regionUrlRegex = '/openlynews\.com\/([a-z]+)\/\?(?:page=(?:[0-9]+)&)?region=([\w-]+)/'; + + public function detectParameters($url) + { + $params = []; + + if (preg_match($this->profileUrlRegex, $url, $matches) > 0) { + $params['context'] = 'By Author'; + $params['profileId'] = $matches[1]; + return $params; + } + + if (preg_match($this->tagUrlRegex, $url, $matches) > 0) { + $params['context'] = 'By Tag'; + $params['content'] = $matches[1]; + $params['tag'] = $matches[2]; + return $params; + } + + if (preg_match($this->regionUrlRegex, $url, $matches) > 0) { + $params['context'] = 'By Region'; + $params['content'] = $matches[1]; + $params['region'] = $matches[2]; + return $params; + } + + return null; + } + + public function collectData() + { + $url = $this->getAjaxURI(); + + if ($this->queriedContext === 'By Author') { + $url = $this->getURI(); + } + + $html = getSimpleHTMLDOM($url); + $html = defaultLinkTo($html, $this->getURI()); + + if ($html->find('h1', 0)) { + $this->feedTitle = $html->find('h1', 0)->plaintext; + } + + if ($html->find('h2.title-v4', 0)) { + $html->find('span.tooltiptext', 0)->innertext = ''; + $this->feedTitle = $html->find('a.tooltipitem', 0)->plaintext; + } + + $items = $html->find('div.item'); + $limit = 5; + foreach (array_slice($items, 0, $limit) as $div) { + $this->items[] = $this->getArticle($div->find('a', 0)->href); + + if (count($this->items) >= $this->itemLimit) { + break; + } + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'All News': + return self::URI . 'news'; + break; + case 'All Opinion': + return self::URI . 'people'; + break; + case 'By Tag': + return self::URI . $this->getInput('content') . '/?theme=' . $this->getInput('tag'); + case 'By Region': + return self::URI . $this->getInput('content') . '/?region=' . $this->getInput('region'); + break; + case 'By Author': + return self::URI . 'profile/?id=' . $this->getInput('profileId'); + break; + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'All News': + return 'News - Openly'; + break; + case 'All Opinion': + return 'Opinion - Openly'; + break; + case 'By Tag': + if (empty($this->feedTitle)) { + $this->feedTitle = $this->getInput('tag'); + } + + if ($this->getInput('content') === 'people') { + return $this->feedTitle . ' - Opinion - Openly'; + } + + return $this->feedTitle . ' - Openly'; + break; + case 'By Region': + if (empty($this->feedTitle)) { + $this->feedTitle = $this->getInput('region'); + } + + if ($this->getInput('content') === 'people') { + return $this->feedTitle . ' - Opinion - Openly'; + } + + return $this->feedTitle . ' - Openly'; + break; + case 'By Author': + if (empty($this->feedTitle)) { + $this->feedTitle = $this->getInput('profileId'); + } + + return $this->feedTitle . ' - Author - Openly'; + break; + default: + return parent::getName(); + } + } + + private function getAjaxURI() + { + $part = '/ajax.html?'; + + switch ($this->queriedContext) { + case 'All News': + return self::URI . 'news' . $part; + break; + case 'All Opinion': + return self::URI . 'people' . $part; + break; + case 'By Tag': + return self::URI . $this->getInput('content') . $part . 'theme=' . $this->getInput('tag'); + break; + case 'By Region': + return self::URI . $this->getInput('content') . $part . 'region=' . $this->getInput('region'); + break; + } + } + + private function getArticle($url) + { + $article = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT); + $article = defaultLinkTo($article, $this->getURI()); + + $item = []; + $item['title'] = $article->find('h1', 0)->plaintext; + $item['uri'] = $url; + $item['content'] = $article->find('div.body-text', 0); + $item['enclosures'][] = $article->find('meta[name="twitter:image"]', 0)->content; + $item['timestamp'] = $article->find('div.meta.small', 0)->plaintext; + + if ($article->find('div.meta a', 0)) { + $item['author'] = $article->find('div.meta a', 0)->plaintext; + } + + foreach ($article->find('div.themes li') as $li) { + $item['categories'][] = trim(htmlspecialchars($li->plaintext, ENT_QUOTES)); + } + + return $item; + } } diff --git a/bridges/OpenwhydBridge.php b/bridges/OpenwhydBridge.php index 865003c6..431173bc 100644 --- a/bridges/OpenwhydBridge.php +++ b/bridges/OpenwhydBridge.php @@ -1,62 +1,66 @@ <?php -class OpenwhydBridge extends BridgeAbstract { - - const MAINTAINER = 'kranack'; - const NAME = 'Openwhyd Bridge'; - const URI = 'https://openwhyd.org'; - const CACHE_TIMEOUT = 600; // 10min - const DESCRIPTION = 'Returns 10 newest music from user profile'; - - const PARAMETERS = array( array( - 'u' => array( - 'name' => 'username/id', - 'exampleValue' => '5247f0267e91c862b2b052d0', - 'required' => true - ) - )); - - private $userName = ''; - - public function getIcon() { - return self::URI . '/images/favicon.ico'; - } - - public function collectData(){ - $html = ''; - if(strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) { - // is input the userid ? - $html = getSimpleHTMLDOM( - self::URI . '/u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u')) - ); - } else { // input may be the username - $html = getSimpleHTMLDOM( - self::URI . '/search?q=' . urlencode($this->getInput('u')) - ); - - for($j = 0; $j < 5; $j++) { - if(strtolower($html->find('div.user', $j)->find('a', 0)->plaintext) == strtolower($this->getInput('u'))) { - $html = getSimpleHTMLDOM( - self::URI . $html->find('div.user', $j)->find('a', 0)->getAttribute('href') - ); - break; - } - } - } - $this->userName = $html->find('div#profileTop', 0)->find('h1', 0)->plaintext; - - for($i = 0; $i < 10; $i++) { - $track = $html->find('div.post', $i); - $item = array(); - $item['author'] = $track->find('h2', 0)->plaintext; - $item['title'] = $track->find('h2', 0)->plaintext; - $item['content'] = $track->find('a.thumb', 0) . '<br/>' . $track->find('h2', 0)->plaintext; - $item['id'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href'); - $item['uri'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href'); - $this->items[] = $item; - } - } - - public function getName(){ - return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Openwhyd Bridge'; - } + +class OpenwhydBridge extends BridgeAbstract +{ + const MAINTAINER = 'kranack'; + const NAME = 'Openwhyd Bridge'; + const URI = 'https://openwhyd.org'; + const CACHE_TIMEOUT = 600; // 10min + const DESCRIPTION = 'Returns 10 newest music from user profile'; + + const PARAMETERS = [ [ + 'u' => [ + 'name' => 'username/id', + 'exampleValue' => '5247f0267e91c862b2b052d0', + 'required' => true + ] + ]]; + + private $userName = ''; + + public function getIcon() + { + return self::URI . '/images/favicon.ico'; + } + + public function collectData() + { + $html = ''; + if (strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) { + // is input the userid ? + $html = getSimpleHTMLDOM( + self::URI . '/u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u')) + ); + } else { // input may be the username + $html = getSimpleHTMLDOM( + self::URI . '/search?q=' . urlencode($this->getInput('u')) + ); + + for ($j = 0; $j < 5; $j++) { + if (strtolower($html->find('div.user', $j)->find('a', 0)->plaintext) == strtolower($this->getInput('u'))) { + $html = getSimpleHTMLDOM( + self::URI . $html->find('div.user', $j)->find('a', 0)->getAttribute('href') + ); + break; + } + } + } + $this->userName = $html->find('div#profileTop', 0)->find('h1', 0)->plaintext; + + for ($i = 0; $i < 10; $i++) { + $track = $html->find('div.post', $i); + $item = []; + $item['author'] = $track->find('h2', 0)->plaintext; + $item['title'] = $track->find('h2', 0)->plaintext; + $item['content'] = $track->find('a.thumb', 0) . '<br/>' . $track->find('h2', 0)->plaintext; + $item['id'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href'); + $item['uri'] = self::URI . $track->find('a.no-ajaxy', 0)->getAttribute('href'); + $this->items[] = $item; + } + } + + public function getName() + { + return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Openwhyd Bridge'; + } } diff --git a/bridges/OpenwrtSecurityBridge.php b/bridges/OpenwrtSecurityBridge.php index 8cdeec01..bfe4e9dd 100644 --- a/bridges/OpenwrtSecurityBridge.php +++ b/bridges/OpenwrtSecurityBridge.php @@ -1,36 +1,40 @@ <?php -class OpenwrtSecurityBridge extends BridgeAbstract { - const NAME = 'OpenWrt Security Advisories'; - const URI = 'https://openwrt.org/advisory/start'; - const DESCRIPTION = 'Security Advisories published by openwrt.org'; - const MAINTAINER = 'mschwld'; - const CACHE_TIMEOUT = 3600; - const WEBROOT = 'https://openwrt.org'; - - public function collectData() { - $item = array(); - $html = getSimpleHTMLDOM(self::URI); - - $advisories = $html->find('div[class=plugin_nspages]', 0); - - foreach($advisories->find('a[class=wikilink1]') as $element) { - $item = array(); - - $row = $element->innertext; - - $item['title'] = substr($row, 0, strpos($row, ' - ')); - $item['timestamp'] = $this->getDate($element->href); - $item['uri'] = self::WEBROOT . $element->href; - $item['uid'] = self::WEBROOT . $element->href; - $item['content'] = substr($row, strpos($row, ' - ') + 3); - $item['author'] = 'OpenWrt Project'; - - $this->items[] = $item; - } - } - - private function getDate($href) { - $date = substr($href, -12); - return $date; - } + +class OpenwrtSecurityBridge extends BridgeAbstract +{ + const NAME = 'OpenWrt Security Advisories'; + const URI = 'https://openwrt.org/advisory/start'; + const DESCRIPTION = 'Security Advisories published by openwrt.org'; + const MAINTAINER = 'mschwld'; + const CACHE_TIMEOUT = 3600; + const WEBROOT = 'https://openwrt.org'; + + public function collectData() + { + $item = []; + $html = getSimpleHTMLDOM(self::URI); + + $advisories = $html->find('div[class=plugin_nspages]', 0); + + foreach ($advisories->find('a[class=wikilink1]') as $element) { + $item = []; + + $row = $element->innertext; + + $item['title'] = substr($row, 0, strpos($row, ' - ')); + $item['timestamp'] = $this->getDate($element->href); + $item['uri'] = self::WEBROOT . $element->href; + $item['uid'] = self::WEBROOT . $element->href; + $item['content'] = substr($row, strpos($row, ' - ') + 3); + $item['author'] = 'OpenWrt Project'; + + $this->items[] = $item; + } + } + + private function getDate($href) + { + $date = substr($href, -12); + return $date; + } } diff --git a/bridges/OtrkeyFinderBridge.php b/bridges/OtrkeyFinderBridge.php index 9c5b8a2a..7920ff9a 100644 --- a/bridges/OtrkeyFinderBridge.php +++ b/bridges/OtrkeyFinderBridge.php @@ -1,180 +1,197 @@ <?php -class OtrkeyFinderBridge extends BridgeAbstract { - const MAINTAINER = 'mibe'; - const NAME = 'OtrkeyFinder'; - const URI = 'https://otrkeyfinder.com'; - const URI_TEMPLATE = 'https://otrkeyfinder.com/en/?search=%s&order=&page=%d'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns the newest .otrkey files matching the search criteria.'; - const PARAMETERS = array( - array( - 'searchterm' => array( - 'name' => 'Search term', - 'exampleValue' => 'Tatort', - 'title' => 'The search term is case-insensitive', - ), - 'station' => array( - 'name' => 'Station name', - 'exampleValue' => 'ARD', - ), - 'type' => array( - 'name' => 'Media type', - 'type' => 'list', - 'values' => array( - 'any' => '', - 'Detail' => array( - 'HD' => 'HD.avi', - 'AC3' => 'HD.ac3', - 'HD & AC3' => 'HD.', - 'HQ' => 'HQ.avi', - 'AVI' => 'g.avi', // 'g.' to exclude HD.avi and HQ.avi (filename always contains 'mpg.') - 'MP4' => '.mp4', - ), - ), - ), - 'minTime' => array( - 'name' => 'Min. running time', - 'type' => 'number', - 'title' => 'The minimum running time in minutes. The resolution is 5 minutes.', - 'exampleValue' => '90', - 'defaultValue' => '0', - ), - 'maxTime' => array( - 'name' => 'Max. running time', - 'type' => 'number', - 'title' => 'The maximum running time in minutes. The resolution is 5 minutes.', - 'exampleValue' => '120', - 'defaultValue' => '0', - ), - 'pages' => array( - 'name' => 'Number of pages', - 'type' => 'number', - 'title' => 'Specifies the number of pages to fetch. Increase this value if you get an empty feed.', - 'exampleValue' => '5', - 'defaultValue' => '5', - ), - ) - ); - // Example: Terminator_20.04.13_02-25_sf2_100_TVOON_DE.mpg.avi.otrkey - // The first group is the running time in minutes - const FILENAME_REGEX = '/_(\d+)_TVOON_DE\.mpg\..+\.otrkey/'; - // year.month.day_hour-minute with leading zeros - const TIME_REGEX = '/\d{2}\.\d{2}\.\d{2}_\d{2}-\d{2}/'; - const CONTENT_TEMPLATE = '<ul>%s</ul>'; - const MIRROR_TEMPLATE = '<li><a href="https://otrkeyfinder.com%s">%s</a></li>'; - - public function collectData() { - $pages = $this->getInput('pages'); - - for($page = 1; $page <= $pages; $page++) { - $uri = $this->buildUri($page); - - $html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT); - - $keys = $html->find('div.otrkey'); - - foreach($keys as $key) { - $temp = $this->buildItem($key); - - if ($temp != null) - $this->items[] = $temp; - } - - // Sleep for 0.5 seconds to don't hammer the server. - usleep(500000); - } - } - - private function buildUri($page) { - $searchterm = $this->getInput('searchterm'); - $station = $this->getInput('station'); - $type = $this->getInput('type'); - - // Combine all three parts to a search query by separating them with white space - $search = implode(' ', array($searchterm, $station, $type)); - $search = trim($search); - $search = urlencode($search); - - return sprintf(self::URI_TEMPLATE, $search, $page); - } - - private function buildItem(simple_html_dom_node $node) { - $file = $this->getFilename($node); - - if ($file == null) - return null; - - $minTime = $this->getInput('minTime'); - $maxTime = $this->getInput('maxTime'); - - // Do we need to check the running time? - if ($minTime != 0 || $maxTime != 0) { - if ($maxTime > 0 && $maxTime < $minTime) - returnClientError('The minimum running time must be less than the maximum running time.'); - - preg_match(self::FILENAME_REGEX, $file, $matches); - - if (!isset($matches[1])) - return null; - - $time = (integer)$matches[1]; - - // Check for minimum running time - if ($minTime > 0 && $minTime > $time) - return null; - - // Check for maximum running time - if ($maxTime > 0 && $maxTime < $time) - return null; - } - - $item = array(); - $item['title'] = $file; - - // The URI_TEMPLATE for querying the site can be reused here - $item['uri'] = sprintf(self::URI_TEMPLATE, $file, 1); - - $content = $this->buildContent($node); - - if ($content != null) - $item['content'] = $content; - - if (preg_match(self::TIME_REGEX, $file, $matches) === 1) { - $item['timestamp'] = DateTime::createFromFormat( - 'y.m.d_H-i', - $matches[0], - new DateTimeZone('Europe/Berlin') - )->getTimestamp(); - } - return $item; - } - - private function getFilename(simple_html_dom_node $node) { - $file = $node->find('.file', 0); - - if ($file == null) - return null; - - // Sometimes there is HTML in the filename - we don't want that. - // To filter that out, enumerate to the node which contains the text only. - foreach($file->nodes as $node) - if ($node->nodetype == HDOM_TYPE_TEXT) - return trim($node->innertext); - - return null; - } - - private function buildContent(simple_html_dom_node $node) { - $mirrors = $node->find('div.mirror'); - $list = ''; - - // Build list of available mirrors - foreach($mirrors as $mirror) { - $anchor = $mirror->find('a', 0); - $list .= sprintf(self::MIRROR_TEMPLATE, $anchor->href, $anchor->innertext); - } - - return sprintf(self::CONTENT_TEMPLATE, $list); - } +class OtrkeyFinderBridge extends BridgeAbstract +{ + const MAINTAINER = 'mibe'; + const NAME = 'OtrkeyFinder'; + const URI = 'https://otrkeyfinder.com'; + const URI_TEMPLATE = 'https://otrkeyfinder.com/en/?search=%s&order=&page=%d'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns the newest .otrkey files matching the search criteria.'; + const PARAMETERS = [ + [ + 'searchterm' => [ + 'name' => 'Search term', + 'exampleValue' => 'Tatort', + 'title' => 'The search term is case-insensitive', + ], + 'station' => [ + 'name' => 'Station name', + 'exampleValue' => 'ARD', + ], + 'type' => [ + 'name' => 'Media type', + 'type' => 'list', + 'values' => [ + 'any' => '', + 'Detail' => [ + 'HD' => 'HD.avi', + 'AC3' => 'HD.ac3', + 'HD & AC3' => 'HD.', + 'HQ' => 'HQ.avi', + 'AVI' => 'g.avi', // 'g.' to exclude HD.avi and HQ.avi (filename always contains 'mpg.') + 'MP4' => '.mp4', + ], + ], + ], + 'minTime' => [ + 'name' => 'Min. running time', + 'type' => 'number', + 'title' => 'The minimum running time in minutes. The resolution is 5 minutes.', + 'exampleValue' => '90', + 'defaultValue' => '0', + ], + 'maxTime' => [ + 'name' => 'Max. running time', + 'type' => 'number', + 'title' => 'The maximum running time in minutes. The resolution is 5 minutes.', + 'exampleValue' => '120', + 'defaultValue' => '0', + ], + 'pages' => [ + 'name' => 'Number of pages', + 'type' => 'number', + 'title' => 'Specifies the number of pages to fetch. Increase this value if you get an empty feed.', + 'exampleValue' => '5', + 'defaultValue' => '5', + ], + ] + ]; + // Example: Terminator_20.04.13_02-25_sf2_100_TVOON_DE.mpg.avi.otrkey + // The first group is the running time in minutes + const FILENAME_REGEX = '/_(\d+)_TVOON_DE\.mpg\..+\.otrkey/'; + // year.month.day_hour-minute with leading zeros + const TIME_REGEX = '/\d{2}\.\d{2}\.\d{2}_\d{2}-\d{2}/'; + const CONTENT_TEMPLATE = '<ul>%s</ul>'; + const MIRROR_TEMPLATE = '<li><a href="https://otrkeyfinder.com%s">%s</a></li>'; + + public function collectData() + { + $pages = $this->getInput('pages'); + + for ($page = 1; $page <= $pages; $page++) { + $uri = $this->buildUri($page); + + $html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT); + + $keys = $html->find('div.otrkey'); + + foreach ($keys as $key) { + $temp = $this->buildItem($key); + + if ($temp != null) { + $this->items[] = $temp; + } + } + + // Sleep for 0.5 seconds to don't hammer the server. + usleep(500000); + } + } + + private function buildUri($page) + { + $searchterm = $this->getInput('searchterm'); + $station = $this->getInput('station'); + $type = $this->getInput('type'); + + // Combine all three parts to a search query by separating them with white space + $search = implode(' ', [$searchterm, $station, $type]); + $search = trim($search); + $search = urlencode($search); + + return sprintf(self::URI_TEMPLATE, $search, $page); + } + + private function buildItem(simple_html_dom_node $node) + { + $file = $this->getFilename($node); + + if ($file == null) { + return null; + } + + $minTime = $this->getInput('minTime'); + $maxTime = $this->getInput('maxTime'); + + // Do we need to check the running time? + if ($minTime != 0 || $maxTime != 0) { + if ($maxTime > 0 && $maxTime < $minTime) { + returnClientError('The minimum running time must be less than the maximum running time.'); + } + + preg_match(self::FILENAME_REGEX, $file, $matches); + + if (!isset($matches[1])) { + return null; + } + + $time = (int)$matches[1]; + + // Check for minimum running time + if ($minTime > 0 && $minTime > $time) { + return null; + } + + // Check for maximum running time + if ($maxTime > 0 && $maxTime < $time) { + return null; + } + } + + $item = []; + $item['title'] = $file; + + // The URI_TEMPLATE for querying the site can be reused here + $item['uri'] = sprintf(self::URI_TEMPLATE, $file, 1); + + $content = $this->buildContent($node); + + if ($content != null) { + $item['content'] = $content; + } + + if (preg_match(self::TIME_REGEX, $file, $matches) === 1) { + $item['timestamp'] = DateTime::createFromFormat( + 'y.m.d_H-i', + $matches[0], + new DateTimeZone('Europe/Berlin') + )->getTimestamp(); + } + + return $item; + } + + private function getFilename(simple_html_dom_node $node) + { + $file = $node->find('.file', 0); + + if ($file == null) { + return null; + } + + // Sometimes there is HTML in the filename - we don't want that. + // To filter that out, enumerate to the node which contains the text only. + foreach ($file->nodes as $node) { + if ($node->nodetype == HDOM_TYPE_TEXT) { + return trim($node->innertext); + } + } + + return null; + } + + private function buildContent(simple_html_dom_node $node) + { + $mirrors = $node->find('div.mirror'); + $list = ''; + + // Build list of available mirrors + foreach ($mirrors as $mirror) { + $anchor = $mirror->find('a', 0); + $list .= sprintf(self::MIRROR_TEMPLATE, $anchor->href, $anchor->innertext); + } + + return sprintf(self::CONTENT_TEMPLATE, $list); + } } diff --git a/bridges/PCGWNewsBridge.php b/bridges/PCGWNewsBridge.php index 92b80fdc..4b3a7c76 100644 --- a/bridges/PCGWNewsBridge.php +++ b/bridges/PCGWNewsBridge.php @@ -1,34 +1,38 @@ <?php -class PCGWNewsBridge extends FeedExpander { - const MAINTAINER = 'somini'; - const NAME = 'PCGamingWiki News'; - const BASE_URI = 'https://www.pcgamingwiki.com'; - const URI = self::BASE_URI . '/wiki/PCGamingWiki:News'; - const DESCRIPTION = 'PCGW News Feed'; - public function getIcon() { - return 'https://static.pcgamingwiki.com/favicons/pcgamingwiki.png'; - } +class PCGWNewsBridge extends FeedExpander +{ + const MAINTAINER = 'somini'; + const NAME = 'PCGamingWiki News'; + const BASE_URI = 'https://www.pcgamingwiki.com'; + const URI = self::BASE_URI . '/wiki/PCGamingWiki:News'; + const DESCRIPTION = 'PCGW News Feed'; - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); + public function getIcon() + { + return 'https://static.pcgamingwiki.com/favicons/pcgamingwiki.png'; + } - $now = strtotime('now'); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); - foreach($html->find('.mw-parser-output .news_li') as $element) { - $item = array(); + $now = strtotime('now'); - $date_string = $element->find('b', 0)->innertext; - $date = strtotime($date_string); - if ($date > $now) { - $date = strtotime($date_string . ' - 1 year'); - } - $item['title'] = self::NAME . ' for ' . date('Y-m-d', $date); - $item['content'] = $element; - $item['uri'] = $this->getURI(); - $item['timestamp'] = $date; + foreach ($html->find('.mw-parser-output .news_li') as $element) { + $item = []; - $this->items[] = $item; - } - } + $date_string = $element->find('b', 0)->innertext; + $date = strtotime($date_string); + if ($date > $now) { + $date = strtotime($date_string . ' - 1 year'); + } + $item['title'] = self::NAME . ' for ' . date('Y-m-d', $date); + $item['content'] = $element; + $item['uri'] = $this->getURI(); + $item['timestamp'] = $date; + + $this->items[] = $item; + } + } } diff --git a/bridges/PanacheDigitalGamesBridge.php b/bridges/PanacheDigitalGamesBridge.php index ba9640ef..61164381 100644 --- a/bridges/PanacheDigitalGamesBridge.php +++ b/bridges/PanacheDigitalGamesBridge.php @@ -1,45 +1,50 @@ <?php -class PanacheDigitalGamesBridge extends BridgeAbstract { - const NAME = 'Panache Digital Games'; - const URI = 'https://www.panachedigitalgames.com'; - const DESCRIPTION = 'Panache Digital Games News Blog'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - ); - - public function getIcon() { - return 'https://www.panachedigitalgames.com/favicon-32x32.png'; - } - - public function getURI() { - return self::URI . '/en/news/'; - } - - public function collectData() { - $articles = self::getURI(); - $html = getSimpleHTMLDOMCached($articles); - - foreach($html->find('.news-item') as $element) { - $item = array(); - - $title = $element->find('.news-item-texts-title', 0); - $link = $element->find('.news-item-texts a', 0); - $timestamp = $element->find('.news-item-texts-date', 0); - - $item['title'] = $title->plaintext; - $item['uri'] = self::URI . $link->href; - $item['timestamp'] = strtotime($timestamp->plaintext); - - $image_html = $element->find('.news-item-thumbnail-image', 0); - if ($image_html) { - $image_strings = explode('\'', $image_html); - /* Debug::log('S: ' . count($image_strings) . '||' . implode('_ _', $image_strings)); */ - if (count($image_strings) == 4) { - $item['content'] = '<img src="' . $image_strings[1] . '" />'; - } - } - - $this->items[] = $item; - } - } + +class PanacheDigitalGamesBridge extends BridgeAbstract +{ + const NAME = 'Panache Digital Games'; + const URI = 'https://www.panachedigitalgames.com'; + const DESCRIPTION = 'Panache Digital Games News Blog'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + ]; + + public function getIcon() + { + return 'https://www.panachedigitalgames.com/favicon-32x32.png'; + } + + public function getURI() + { + return self::URI . '/en/news/'; + } + + public function collectData() + { + $articles = self::getURI(); + $html = getSimpleHTMLDOMCached($articles); + + foreach ($html->find('.news-item') as $element) { + $item = []; + + $title = $element->find('.news-item-texts-title', 0); + $link = $element->find('.news-item-texts a', 0); + $timestamp = $element->find('.news-item-texts-date', 0); + + $item['title'] = $title->plaintext; + $item['uri'] = self::URI . $link->href; + $item['timestamp'] = strtotime($timestamp->plaintext); + + $image_html = $element->find('.news-item-thumbnail-image', 0); + if ($image_html) { + $image_strings = explode('\'', $image_html); + /* Debug::log('S: ' . count($image_strings) . '||' . implode('_ _', $image_strings)); */ + if (count($image_strings) == 4) { + $item['content'] = '<img src="' . $image_strings[1] . '" />'; + } + } + + $this->items[] = $item; + } + } } diff --git a/bridges/ParksOnTheAirBridge.php b/bridges/ParksOnTheAirBridge.php index ceaf78b6..67910f6e 100644 --- a/bridges/ParksOnTheAirBridge.php +++ b/bridges/ParksOnTheAirBridge.php @@ -1,27 +1,28 @@ <?php -class ParksOnTheAirBridge extends BridgeAbstract { - const MAINTAINER = 's0lesurviv0r'; - const NAME = 'Parks On The Air Spots'; - const URI = 'https://pota.app/#'; - const API_URI = 'https://api.pota.app/spot/activator'; - const CACHE_TIMEOUT = 60; // 1m - const DESCRIPTION = 'Parks On The Air Activator Spots'; +class ParksOnTheAirBridge extends BridgeAbstract +{ + const MAINTAINER = 's0lesurviv0r'; + const NAME = 'Parks On The Air Spots'; + const URI = 'https://pota.app/#'; + const API_URI = 'https://api.pota.app/spot/activator'; + const CACHE_TIMEOUT = 60; // 1m + const DESCRIPTION = 'Parks On The Air Activator Spots'; - public function collectData() { + public function collectData() + { + $header = ['Content-type:application/json']; + $opts = [CURLOPT_HTTPGET => 1]; + $json = getContents(self::API_URI, $header, $opts); - $header = array('Content-type:application/json'); - $opts = array(CURLOPT_HTTPGET => 1); - $json = getContents(self::API_URI, $header, $opts); + $spots = json_decode($json, true); - $spots = json_decode($json, true); + foreach ($spots as $spot) { + $title = $spot['activator'] . ' @ ' . $spot['reference'] . ' ' . + $spot['frequency'] . ' kHz'; + $park_link = self::URI . '/park/' . $spot['reference']; - foreach ($spots as $spot) { - $title = $spot['activator'] . ' @ ' . $spot['reference'] . ' ' . - $spot['frequency'] . ' kHz'; - $park_link = self::URI . '/park/' . $spot['reference']; - - $content = <<<EOL + $content = <<<EOL <a href="{$park_link}"> {$spot['reference']}, {$spot['name']}</a><br /> Location: {$spot['locationDesc']}<br /> @@ -30,12 +31,12 @@ Spotter: {$spot['spotter']}<br /> Comments: {$spot['comments']} EOL; - $this->items[] = array( - 'uri' => $park_link, - 'title' => $title, - 'content' => $content, - 'timestamp' => $spot['spotTime'] - ); - } - } + $this->items[] = [ + 'uri' => $park_link, + 'title' => $title, + 'content' => $content, + 'timestamp' => $spot['spotTime'] + ]; + } + } } diff --git a/bridges/ParlerBridge.php b/bridges/ParlerBridge.php index 69b4f9a1..97e9dab0 100644 --- a/bridges/ParlerBridge.php +++ b/bridges/ParlerBridge.php @@ -2,79 +2,79 @@ final class ParlerBridge extends BridgeAbstract { - const NAME = 'Parler.com bridge'; - const URI = 'https://parler.com'; - const DESCRIPTION = 'Fetches the latest posts from a parler user'; - const MAINTAINER = 'dvikan'; - const CACHE_TIMEOUT = 60 * 15; // 15m - const PARAMETERS = [ - [ - 'user' => [ - 'name' => 'User', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'NigelFarage', - ], - 'limit' => self::LIMIT, - ] - ]; + const NAME = 'Parler.com bridge'; + const URI = 'https://parler.com'; + const DESCRIPTION = 'Fetches the latest posts from a parler user'; + const MAINTAINER = 'dvikan'; + const CACHE_TIMEOUT = 60 * 15; // 15m + const PARAMETERS = [ + [ + 'user' => [ + 'name' => 'User', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'NigelFarage', + ], + 'limit' => self::LIMIT, + ] + ]; - public function collectData() - { - $user = trim($this->getInput('user')); + public function collectData() + { + $user = trim($this->getInput('user')); - if (preg_match('#^https?://parler\.com/(\w+)#i', $user, $m)) { - $user = $m[1]; - } + if (preg_match('#^https?://parler\.com/(\w+)#i', $user, $m)) { + $user = $m[1]; + } - $posts = $this->fetchParlerProfileFeed($user); + $posts = $this->fetchParlerProfileFeed($user); - foreach ($posts as $post) { - // For some reason, the post data is placed inside primary attribute - $primary = $post->primary; + foreach ($posts as $post) { + // For some reason, the post data is placed inside primary attribute + $primary = $post->primary; - $item = [ - 'title' => mb_substr($primary->body, 0, 100), - 'uri' => sprintf('https://parler.com/feed/%s', $primary->uuid), - 'author' => $primary->username, - 'uid' => $primary->uuid, - 'content' => nl2br($primary->full_body), - ]; + $item = [ + 'title' => mb_substr($primary->body, 0, 100), + 'uri' => sprintf('https://parler.com/feed/%s', $primary->uuid), + 'author' => $primary->username, + 'uid' => $primary->uuid, + 'content' => nl2br($primary->full_body), + ]; - $date = DateTimeImmutable::createFromFormat('m/d/YH:i A', $primary->date_str . $primary->time_str); - if ($date) { - $item['timestamp'] = $date->getTimestamp(); - } else { - Debug::log(sprintf('Unable to parse data from Parler.com: "%s"', $date)); - } + $date = DateTimeImmutable::createFromFormat('m/d/YH:i A', $primary->date_str . $primary->time_str); + if ($date) { + $item['timestamp'] = $date->getTimestamp(); + } else { + Debug::log(sprintf('Unable to parse data from Parler.com: "%s"', $date)); + } - if (isset($primary->image)) { - $item['enclosures'][] = $primary->image; - $item['content'] .= sprintf('<img loading="lazy" src="%s">', $primary->image); - } + if (isset($primary->image)) { + $item['enclosures'][] = $primary->image; + $item['content'] .= sprintf('<img loading="lazy" src="%s">', $primary->image); + } - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - private function fetchParlerProfileFeed(string $user): array - { - $json = getContents('https://parler.com/open-api/ProfileFeedEndpoint.php', [], [ - CURLOPT_POSTFIELDS => http_build_query([ - 'user' => $user, - 'page' => '1', - ]), - ]); - $response = json_decode($json); - if ($response === false) { - throw new \Exception('Unable to decode json from Parler'); - } - if ($response->status !== 'ok') { - throw new \Exception('Did not get OK from Parler'); - } - if ($response->data === []) { - throw new \Exception('Unknown Parler username'); - } - return $response->data; - } + private function fetchParlerProfileFeed(string $user): array + { + $json = getContents('https://parler.com/open-api/ProfileFeedEndpoint.php', [], [ + CURLOPT_POSTFIELDS => http_build_query([ + 'user' => $user, + 'page' => '1', + ]), + ]); + $response = json_decode($json); + if ($response === false) { + throw new \Exception('Unable to decode json from Parler'); + } + if ($response->status !== 'ok') { + throw new \Exception('Did not get OK from Parler'); + } + if ($response->data === []) { + throw new \Exception('Unknown Parler username'); + } + return $response->data; + } } diff --git a/bridges/ParuVenduImmoBridge.php b/bridges/ParuVenduImmoBridge.php index e7a0c02e..f48e36df 100644 --- a/bridges/ParuVenduImmoBridge.php +++ b/bridges/ParuVenduImmoBridge.php @@ -1,112 +1,115 @@ <?php -class ParuVenduImmoBridge extends BridgeAbstract { - - const MAINTAINER = 'polo2ro'; - const NAME = 'Paru Vendu Immobilier'; - const URI = 'https://www.paruvendu.fr'; - const CACHE_TIMEOUT = 10800; // 3h - const DESCRIPTION = 'Returns the ads from the first page of search result.'; - - const PARAMETERS = array( array( - 'minarea' => array( - 'name' => 'Minimal surface m²', - 'type' => 'number' - ), - 'maxprice' => array( - 'name' => 'Max price', - 'type' => 'number' - ), - 'pa' => array( - 'name' => 'Country code', - 'exampleValue' => 'FR' - ), - 'lo' => array( - 'name' => 'department numbers or postal codes, comma-separated' - ) - )); - - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); - - $elements = $html->find('#bloc_liste > div.ergov3-annonce a'); - - foreach($elements as $element) { - - if(!$element->title) { - continue; - } - - $img = ''; - foreach($element->find('span.img img') as $img) { - if($img->original) { - $img = '<img src="' . $img->original . '" />'; - } - } - - $description = $element->find('p', 0); - if ($description) { - $desc = str_replace("voir l'annonce", '', $description->innertext); - } else { - $desc = ''; - } - - $priceElement = $element->find('div.ergov3-priceannonce', 0); - if ($priceElement) { - $price = $priceElement->innertext; - } else { - $price = ''; - } - - list($href) = explode('#', $element->href); - - $item = array(); - $item['uri'] = self::URI . $href; - $item['title'] = $element->title; - $item['content'] = $img . $desc . $price; - $this->items[] = $item; - } - } - - public function getURI(){ - $appartment = '&tbApp=1&tbDup=1&tbChb=1&tbLof=1&tbAtl=1&tbPla=1'; - $maison = '&tbMai=1&tbVil=1&tbCha=1&tbPro=1&tbHot=1&tbMou=1&tbFer=1'; - $link = self::URI - . '/immobilier/annonceimmofo/liste/listeAnnonces?tt=1' - . $appartment - . $maison; - - if($this->getInput('minarea')) { - $link .= '&sur0=' . urlencode($this->getInput('minarea')); - } - - if($this->getInput('maxprice')) { - $link .= '&px1=' . urlencode($this->getInput('maxprice')); - } - - if($this->getInput('pa')) { - $link .= '&pa=' . urlencode($this->getInput('pa')); - } - - if($this->getInput('lo')) { - $link .= '&lo=' . urlencode($this->getInput('lo')); - } - return $link; - } - - public function getName(){ - if(!is_null($this->getInput('minarea'))) { - $request = ''; - $minarea = $this->getInput('minarea'); - if(!empty($minarea)) { - $request .= ' ' . $minarea . ' m2'; - } - $location = $this->getInput('lo'); - if(!empty($location)) { - $request .= ' In: ' . $location; - } - return 'Paru Vendu Immobilier' . $request; - } - - return parent::getName(); - } + +class ParuVenduImmoBridge extends BridgeAbstract +{ + const MAINTAINER = 'polo2ro'; + const NAME = 'Paru Vendu Immobilier'; + const URI = 'https://www.paruvendu.fr'; + const CACHE_TIMEOUT = 10800; // 3h + const DESCRIPTION = 'Returns the ads from the first page of search result.'; + + const PARAMETERS = [ [ + 'minarea' => [ + 'name' => 'Minimal surface m²', + 'type' => 'number' + ], + 'maxprice' => [ + 'name' => 'Max price', + 'type' => 'number' + ], + 'pa' => [ + 'name' => 'Country code', + 'exampleValue' => 'FR' + ], + 'lo' => [ + 'name' => 'department numbers or postal codes, comma-separated' + ] + ]]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $elements = $html->find('#bloc_liste > div.ergov3-annonce a'); + + foreach ($elements as $element) { + if (!$element->title) { + continue; + } + + $img = ''; + foreach ($element->find('span.img img') as $img) { + if ($img->original) { + $img = '<img src="' . $img->original . '" />'; + } + } + + $description = $element->find('p', 0); + if ($description) { + $desc = str_replace("voir l'annonce", '', $description->innertext); + } else { + $desc = ''; + } + + $priceElement = $element->find('div.ergov3-priceannonce', 0); + if ($priceElement) { + $price = $priceElement->innertext; + } else { + $price = ''; + } + + list($href) = explode('#', $element->href); + + $item = []; + $item['uri'] = self::URI . $href; + $item['title'] = $element->title; + $item['content'] = $img . $desc . $price; + $this->items[] = $item; + } + } + + public function getURI() + { + $appartment = '&tbApp=1&tbDup=1&tbChb=1&tbLof=1&tbAtl=1&tbPla=1'; + $maison = '&tbMai=1&tbVil=1&tbCha=1&tbPro=1&tbHot=1&tbMou=1&tbFer=1'; + $link = self::URI + . '/immobilier/annonceimmofo/liste/listeAnnonces?tt=1' + . $appartment + . $maison; + + if ($this->getInput('minarea')) { + $link .= '&sur0=' . urlencode($this->getInput('minarea')); + } + + if ($this->getInput('maxprice')) { + $link .= '&px1=' . urlencode($this->getInput('maxprice')); + } + + if ($this->getInput('pa')) { + $link .= '&pa=' . urlencode($this->getInput('pa')); + } + + if ($this->getInput('lo')) { + $link .= '&lo=' . urlencode($this->getInput('lo')); + } + return $link; + } + + public function getName() + { + if (!is_null($this->getInput('minarea'))) { + $request = ''; + $minarea = $this->getInput('minarea'); + if (!empty($minarea)) { + $request .= ' ' . $minarea . ' m2'; + } + $location = $this->getInput('lo'); + if (!empty($location)) { + $request .= ' In: ' . $location; + } + return 'Paru Vendu Immobilier' . $request; + } + + return parent::getName(); + } } diff --git a/bridges/PatreonBridge.php b/bridges/PatreonBridge.php index 5f9a4565..a15d0378 100644 --- a/bridges/PatreonBridge.php +++ b/bridges/PatreonBridge.php @@ -1,202 +1,217 @@ <?php -class PatreonBridge extends BridgeAbstract { - const NAME = 'Patreon Bridge'; - const URI = 'https://www.patreon.com/'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Returns posts by creators on Patreon'; - const MAINTAINER = 'Roliga'; - const PARAMETERS = array( array( - 'creator' => array( - 'name' => 'Creator', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'sanityinc', - 'title' => 'Creator name as seen in their page URL' - ) - )); - - public function collectData(){ - $html = getSimpleHTMLDOMCached($this->getURI(), 86400); - $regex = '#/api/campaigns/([0-9]+)#'; - if(preg_match($regex, $html->save(), $matches) > 0) { - $campaign_id = $matches[1]; - } else { - returnServerError('Could not find campaign ID'); - } - - $query = array( - 'include' => implode(',', array( - 'user', - 'attachments', - 'user_defined_tags', - //'campaign', - //'poll.choices', - //'poll.current_user_responses.user', - //'poll.current_user_responses.choice', - //'poll.current_user_responses.poll', - //'access_rules.tier.null', - //'images.null', - //'audio.null' - )), - 'fields' => array( - 'post' => implode(',', array( - //'change_visibility_at', - //'comment_count', - 'content', - //'current_user_can_delete', - //'current_user_can_view', - //'current_user_has_liked', - //'embed', - 'image', - //'is_paid', - //'like_count', - //'min_cents_pledged_to_view', - //'patreon_url', - //'patron_count', - //'pledge_url', - //'post_file', - //'post_metadata', - //'post_type', - 'published_at', - 'teaser_text', - //'thumbnail_url', - 'title', - //'upgrade_url', - 'url', - //'was_posted_by_campaign_owner' - )), - 'user' => implode(',', array( - //'image_url', - 'full_name', - //'url' - )) - ), - 'filter' => array( - 'contains_exclusive_posts' => true, - 'is_draft' => false, - 'campaign_id' => $campaign_id - ), - 'sort' => '-published_at' - ); - $posts = $this->apiGet('posts', $query); - - foreach($posts->data as $post) { - $item = array( - 'uri' => $post->attributes->url, - 'title' => $post->attributes->title, - 'timestamp' => $post->attributes->published_at, - 'content' => '', - 'uid' => 'patreon.com/' . $post->id - ); - - $user = $this->findInclude($posts, - 'user', - $post->relationships->user->data->id); - $item['author'] = $user->full_name; - - if(isset($post->attributes->image)) - $item['content'] .= '<p><a href="' - . $post->attributes->url - . '"><img src="' - . $post->attributes->image->thumb_url - . '" /></a></p>'; - - if(isset($post->attributes->content)) { - $item['content'] .= $post->attributes->content; - } elseif (isset($post->attributes->teaser_text)) { - $item['content'] .= '<p>' - . $post->attributes->teaser_text - . '</p>'; - } - - if(isset($post->relationships->user_defined_tags)) { - $item['categories'] = array(); - foreach($post->relationships->user_defined_tags->data as $tag) { - $attrs = $this->findInclude($posts, 'post_tag', $tag->id); - $item['categories'][] = $attrs->value; - } - } - - if(isset($post->relationships->attachments)) { - $item['enclosures'] = array(); - foreach($post->relationships->attachments->data as $attachment) { - $attrs = $this->findInclude($posts, 'attachment', $attachment->id); - $item['enclosures'][] = $attrs->url; - } - } - - $this->items[] = $item; - } - } - - /* - * Searches the "included" array in an API response and returns attributes - * for the first match. - */ - private function findInclude($data, $type, $id) { - foreach($data->included as $include) - if($include->type === $type && $include->id === $id) - return $include->attributes; - } - - private function apiGet($endpoint, $query_data = array()) { - $query_data['json-api-version'] = 1.0; - $query_data['json-api-use-default-includes'] = 0; - - $url = 'https://www.patreon.com/api/' - . $endpoint - . '?' - . http_build_query($query_data); - - /* - * Accept-Language header and the CURL cipher list are for bypassing the - * Cloudflare anti-bot protection on the Patreon API. If this ever breaks, - * here are some other project that also deal with this: - * https://github.com/mikf/gallery-dl/issues/342 - * https://github.com/daemionfox/patreon-feed/issues/7 - * https://www.patreondevelopers.com/t/api-returning-cloudflare-challenge/2025 - * https://github.com/splitbrain/patreon-rss/issues/4 - */ - $header = array( - 'Accept-Language: en-US', - 'Content-Type: application/json' - ); - $opts = array( - CURLOPT_SSL_CIPHER_LIST => implode(':', array( - 'DEFAULT', - '!DHE-RSA-CHACHA20-POLY1305' - )) - ); - - $data = json_decode(getContents($url, $header, $opts)); - - return $data; - } - - public function getName(){ - if(!is_null($this->getInput('creator'))) - return $this->getInput('creator') . ' posts'; - - return parent::getName(); - } - - public function getURI(){ - if(!is_null($this->getInput('creator'))) - return self::URI . $this->getInput('creator'); - - return parent::getURI(); - } - - public function detectParameters($url){ - $params = array(); - - // Matches e.g. https://www.patreon.com/SomeCreator - $regex = '/^(https?:\/\/)?(www\.)?patreon\.com\/([^\/&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['creator'] = urldecode($matches[3]); - return $params; - } - - return null; - } + +class PatreonBridge extends BridgeAbstract +{ + const NAME = 'Patreon Bridge'; + const URI = 'https://www.patreon.com/'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Returns posts by creators on Patreon'; + const MAINTAINER = 'Roliga'; + const PARAMETERS = [ [ + 'creator' => [ + 'name' => 'Creator', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'sanityinc', + 'title' => 'Creator name as seen in their page URL' + ] + ]]; + + public function collectData() + { + $html = getSimpleHTMLDOMCached($this->getURI(), 86400); + $regex = '#/api/campaigns/([0-9]+)#'; + if (preg_match($regex, $html->save(), $matches) > 0) { + $campaign_id = $matches[1]; + } else { + returnServerError('Could not find campaign ID'); + } + + $query = [ + 'include' => implode(',', [ + 'user', + 'attachments', + 'user_defined_tags', + //'campaign', + //'poll.choices', + //'poll.current_user_responses.user', + //'poll.current_user_responses.choice', + //'poll.current_user_responses.poll', + //'access_rules.tier.null', + //'images.null', + //'audio.null' + ]), + 'fields' => [ + 'post' => implode(',', [ + //'change_visibility_at', + //'comment_count', + 'content', + //'current_user_can_delete', + //'current_user_can_view', + //'current_user_has_liked', + //'embed', + 'image', + //'is_paid', + //'like_count', + //'min_cents_pledged_to_view', + //'patreon_url', + //'patron_count', + //'pledge_url', + //'post_file', + //'post_metadata', + //'post_type', + 'published_at', + 'teaser_text', + //'thumbnail_url', + 'title', + //'upgrade_url', + 'url', + //'was_posted_by_campaign_owner' + ]), + 'user' => implode(',', [ + //'image_url', + 'full_name', + //'url' + ]) + ], + 'filter' => [ + 'contains_exclusive_posts' => true, + 'is_draft' => false, + 'campaign_id' => $campaign_id + ], + 'sort' => '-published_at' + ]; + $posts = $this->apiGet('posts', $query); + + foreach ($posts->data as $post) { + $item = [ + 'uri' => $post->attributes->url, + 'title' => $post->attributes->title, + 'timestamp' => $post->attributes->published_at, + 'content' => '', + 'uid' => 'patreon.com/' . $post->id + ]; + + $user = $this->findInclude( + $posts, + 'user', + $post->relationships->user->data->id + ); + $item['author'] = $user->full_name; + + if (isset($post->attributes->image)) { + $item['content'] .= '<p><a href="' + . $post->attributes->url + . '"><img src="' + . $post->attributes->image->thumb_url + . '" /></a></p>'; + } + + if (isset($post->attributes->content)) { + $item['content'] .= $post->attributes->content; + } elseif (isset($post->attributes->teaser_text)) { + $item['content'] .= '<p>' + . $post->attributes->teaser_text + . '</p>'; + } + + if (isset($post->relationships->user_defined_tags)) { + $item['categories'] = []; + foreach ($post->relationships->user_defined_tags->data as $tag) { + $attrs = $this->findInclude($posts, 'post_tag', $tag->id); + $item['categories'][] = $attrs->value; + } + } + + if (isset($post->relationships->attachments)) { + $item['enclosures'] = []; + foreach ($post->relationships->attachments->data as $attachment) { + $attrs = $this->findInclude($posts, 'attachment', $attachment->id); + $item['enclosures'][] = $attrs->url; + } + } + + $this->items[] = $item; + } + } + + /* + * Searches the "included" array in an API response and returns attributes + * for the first match. + */ + private function findInclude($data, $type, $id) + { + foreach ($data->included as $include) { + if ($include->type === $type && $include->id === $id) { + return $include->attributes; + } + } + } + + private function apiGet($endpoint, $query_data = []) + { + $query_data['json-api-version'] = 1.0; + $query_data['json-api-use-default-includes'] = 0; + + $url = 'https://www.patreon.com/api/' + . $endpoint + . '?' + . http_build_query($query_data); + + /* + * Accept-Language header and the CURL cipher list are for bypassing the + * Cloudflare anti-bot protection on the Patreon API. If this ever breaks, + * here are some other project that also deal with this: + * https://github.com/mikf/gallery-dl/issues/342 + * https://github.com/daemionfox/patreon-feed/issues/7 + * https://www.patreondevelopers.com/t/api-returning-cloudflare-challenge/2025 + * https://github.com/splitbrain/patreon-rss/issues/4 + */ + $header = [ + 'Accept-Language: en-US', + 'Content-Type: application/json' + ]; + $opts = [ + CURLOPT_SSL_CIPHER_LIST => implode(':', [ + 'DEFAULT', + '!DHE-RSA-CHACHA20-POLY1305' + ]) + ]; + + $data = json_decode(getContents($url, $header, $opts)); + + return $data; + } + + public function getName() + { + if (!is_null($this->getInput('creator'))) { + return $this->getInput('creator') . ' posts'; + } + + return parent::getName(); + } + + public function getURI() + { + if (!is_null($this->getInput('creator'))) { + return self::URI . $this->getInput('creator'); + } + + return parent::getURI(); + } + + public function detectParameters($url) + { + $params = []; + + // Matches e.g. https://www.patreon.com/SomeCreator + $regex = '/^(https?:\/\/)?(www\.)?patreon\.com\/([^\/&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['creator'] = urldecode($matches[3]); + return $params; + } + + return null; + } } diff --git a/bridges/PcGamerBridge.php b/bridges/PcGamerBridge.php index 95261d9c..5ea15e8c 100644 --- a/bridges/PcGamerBridge.php +++ b/bridges/PcGamerBridge.php @@ -1,41 +1,42 @@ <?php + class PcGamerBridge extends BridgeAbstract { - const NAME = 'PC Gamer'; - const URI = 'https://www.pcgamer.com/'; - const DESCRIPTION = 'PC Gamer is your source for exclusive reviews, demos, + const NAME = 'PC Gamer'; + const URI = 'https://www.pcgamer.com/'; + const DESCRIPTION = 'PC Gamer is your source for exclusive reviews, demos, updates and news on all your favorite PC gaming franchises.'; - const MAINTAINER = 'IceWreck, mdemoss'; + const MAINTAINER = 'IceWreck, mdemoss'; - const PARAMETERS = [ - [ - 'limit' => self::LIMIT, - ] - ]; + const PARAMETERS = [ + [ + 'limit' => self::LIMIT, + ] + ]; - public function collectData() - { - $html = getSimpleHTMLDOMCached($this->getURI(), 300); - $stories = $html->find('a.article-link'); - $limit = $this->getInput('limit') ?? 10; - foreach (array_slice($stories, 0, $limit) as $element) { - $item = array(); - $item['uri'] = $element->href; - $articleHtml = getSimpleHTMLDOMCached($item['uri']); + public function collectData() + { + $html = getSimpleHTMLDOMCached($this->getURI(), 300); + $stories = $html->find('a.article-link'); + $limit = $this->getInput('limit') ?? 10; + foreach (array_slice($stories, 0, $limit) as $element) { + $item = []; + $item['uri'] = $element->href; + $articleHtml = getSimpleHTMLDOMCached($item['uri']); - // Relying on meta tags ought to be more reliable. - $item['title'] = $articleHtml->find('meta[name=parsely-title]', 0)->content; - $item['content'] = html_entity_decode($articleHtml->find('meta[name=description]', 0)->content); - $item['author'] = $articleHtml->find('meta[name=parsely-author]', 0)->content; - $item['enclosures'][] = $articleHtml->find('meta[name=parsely-image-url]', 0)->content; - /* I don't know why every article has two extra tags, but because - one matches another common tag, "guide," it needs to be removed. */ - $item['categories'] = array_diff( - explode(',', $articleHtml->find('meta[name=parsely-tags]', 0)->content), - array('van_buying_guide_progressive', 'serversidehawk') - ); - $item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content); - $this->items[] = $item; - } - } + // Relying on meta tags ought to be more reliable. + $item['title'] = $articleHtml->find('meta[name=parsely-title]', 0)->content; + $item['content'] = html_entity_decode($articleHtml->find('meta[name=description]', 0)->content); + $item['author'] = $articleHtml->find('meta[name=parsely-author]', 0)->content; + $item['enclosures'][] = $articleHtml->find('meta[name=parsely-image-url]', 0)->content; + /* I don't know why every article has two extra tags, but because + one matches another common tag, "guide," it needs to be removed. */ + $item['categories'] = array_diff( + explode(',', $articleHtml->find('meta[name=parsely-tags]', 0)->content), + ['van_buying_guide_progressive', 'serversidehawk'] + ); + $item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content); + $this->items[] = $item; + } + } } diff --git a/bridges/PepperBridgeAbstract.php b/bridges/PepperBridgeAbstract.php index e6e44acd..123638c5 100644 --- a/bridges/PepperBridgeAbstract.php +++ b/bridges/PepperBridgeAbstract.php @@ -1,175 +1,178 @@ <?php -class PepperBridgeAbstract extends BridgeAbstract { - - const CACHE_TIMEOUT = 3600; - - public function collectData(){ - switch($this->queriedContext) { - case $this->i8n('context-keyword'): - return $this->collectDataKeywords(); - break; - case $this->i8n('context-group'): - return $this->collectDataGroup(); - break; - case $this->i8n('context-talk'): - return $this->collectDataTalk(); - break; - } - } - - /** - * Get the Deal data from the choosen group in the choosed order - */ - protected function collectDataGroup() - { - $url = $this->getGroupURI(); - $this->collectDeals($url); - } - - /** - * Get the Deal data from the choosen keywords and parameters - */ - protected function collectDataKeywords() - { - /* Even if the original website uses POST with the search page, GET works too */ - $url = $this->getSearchURI(); - $this->collectDeals($url); - } - - /** - * Get the Deal data using the given URL - */ - protected function collectDeals($url){ - $html = getSimpleHTMLDOM($url); - $list = $html->find('article[id]'); - - // Deal Image Link CSS Selector - $selectorImageLink = implode( - ' ', /* Notice this is a space! */ - array( - 'cept-thread-image-link', - 'imgFrame', - 'imgFrame--noBorder', - 'thread-listImgCell', - ) - ); - - // Deal Link CSS Selector - $selectorLink = implode( - ' ', /* Notice this is a space! */ - array( - 'cept-tt', - 'thread-link', - 'linkPlain', - ) - ); - - // Deal Hotness CSS Selector - $selectorHot = implode( - ' ', /* Notice this is a space! */ - array( - 'cept-vote-box', - 'vote-box' - ) - ); - - // Deal Description CSS Selector - $selectorDescription = implode( - ' ', /* Notice this is a space! */ - array( - 'overflow--wrap-break' - ) - ); - - // Deal Date CSS Selector - $selectorDate = implode( - ' ', /* Notice this is a space! */ - array( - 'size--all-s', - 'flex', - 'boxAlign-jc--all-fe' - ) - ); - - // If there is no results, we don't parse the content because it display some random deals - $noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0); - if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) { - $this->items = array(); - } else { - foreach ($list as $deal) { - $item = array(); - $item['uri'] = $this->getDealURI($deal); - $item['title'] = $this->getTitle($deal); - $item['author'] = $deal->find('span.thread-username', 0)->plaintext; - - $item['content'] = '<table><tr><td><a href="' - . $item['uri'] - . '"><img src="' - . $this->getImage($deal) - . '"/></td><td>' - . $this->getHTMLTitle($item) - . $this->getPrice($deal) - . $this->getDiscount($deal) - . $this->getShipsFrom($deal) - . $this->getShippingCost($deal) - . $this->getSource($deal) - . $deal->find('div[class*=' . $selectorDescription . ']', 0)->innertext - . '</td><td>' - . $deal->find('div[class*=' . $selectorHot . ']', 0) - ->find('span', 1)->outertext - . '</td></table>'; - - // Check if a clock icon is displayed on the deal - $clocks = $deal->find('svg[class*=icon--clock]'); - if($clocks !== null && count($clocks) > 0) { - // Get the last clock, corresponding to the deal posting date - $clock = end($clocks); - - // Find the text corresponding to the clock - $spanDateDiv = $clock->parent()->find('span[class=hide--toW3]', 0); - $itemDate = $spanDateDiv->plaintext; - // In case of a Local deal, there is no date, but we can use - // this case for other reason (like date not in the last field) - if ($this->contains($itemDate, $this->i8n('localdeal'))) { - $item['timestamp'] = time(); - } else if ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) { - $item['timestamp'] = $this->relativeDateToTimestamp($itemDate); - } else { - $item['timestamp'] = $this->parseDate($itemDate); - } - } - $this->items[] = $item; - } - } - } - - /** - * Get the Talk lastest comments - */ - protected function collectDataTalk(){ - $threadURL = $this->getInput('url'); - $onlyWithUrl = $this->getInput('only_with_url'); - - // Get Thread ID from url passed in parameter - $threadSearch = preg_match('/-([0-9]{1,20})$/', $threadURL, $matches); - - // Show an error message if we can't find the thread ID in the URL sent by the user - if($threadSearch !== 1) { - returnClientError($this->i8n('thread-error')); - } - $threadID = $matches[1]; - - $url = $this->i8n('bridge-uri') . 'graphql'; - - // Get Cookies header to do the query - $cookies = $this->getCookies($url); - - // GraphQL String - // This was extracted from https://www.dealabs.com/assets/js/modern/common_211b99.js - // This string was extracted during a Website visit, and minified using this neat tool : - // https://codepen.io/dangodev/pen/Baoqmoy - $graphqlString = <<<'HEREDOC' +class PepperBridgeAbstract extends BridgeAbstract +{ + const CACHE_TIMEOUT = 3600; + + public function collectData() + { + switch ($this->queriedContext) { + case $this->i8n('context-keyword'): + return $this->collectDataKeywords(); + break; + case $this->i8n('context-group'): + return $this->collectDataGroup(); + break; + case $this->i8n('context-talk'): + return $this->collectDataTalk(); + break; + } + } + + /** + * Get the Deal data from the choosen group in the choosed order + */ + protected function collectDataGroup() + { + $url = $this->getGroupURI(); + $this->collectDeals($url); + } + + /** + * Get the Deal data from the choosen keywords and parameters + */ + protected function collectDataKeywords() + { + /* Even if the original website uses POST with the search page, GET works too */ + $url = $this->getSearchURI(); + $this->collectDeals($url); + } + + /** + * Get the Deal data using the given URL + */ + protected function collectDeals($url) + { + $html = getSimpleHTMLDOM($url); + $list = $html->find('article[id]'); + + // Deal Image Link CSS Selector + $selectorImageLink = implode( + ' ', /* Notice this is a space! */ + [ + 'cept-thread-image-link', + 'imgFrame', + 'imgFrame--noBorder', + 'thread-listImgCell', + ] + ); + + // Deal Link CSS Selector + $selectorLink = implode( + ' ', /* Notice this is a space! */ + [ + 'cept-tt', + 'thread-link', + 'linkPlain', + ] + ); + + // Deal Hotness CSS Selector + $selectorHot = implode( + ' ', /* Notice this is a space! */ + [ + 'cept-vote-box', + 'vote-box' + ] + ); + + // Deal Description CSS Selector + $selectorDescription = implode( + ' ', /* Notice this is a space! */ + [ + 'overflow--wrap-break' + ] + ); + + // Deal Date CSS Selector + $selectorDate = implode( + ' ', /* Notice this is a space! */ + [ + 'size--all-s', + 'flex', + 'boxAlign-jc--all-fe' + ] + ); + + // If there is no results, we don't parse the content because it display some random deals + $noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0); + if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) { + $this->items = []; + } else { + foreach ($list as $deal) { + $item = []; + $item['uri'] = $this->getDealURI($deal); + $item['title'] = $this->getTitle($deal); + $item['author'] = $deal->find('span.thread-username', 0)->plaintext; + + $item['content'] = '<table><tr><td><a href="' + . $item['uri'] + . '"><img src="' + . $this->getImage($deal) + . '"/></td><td>' + . $this->getHTMLTitle($item) + . $this->getPrice($deal) + . $this->getDiscount($deal) + . $this->getShipsFrom($deal) + . $this->getShippingCost($deal) + . $this->getSource($deal) + . $deal->find('div[class*=' . $selectorDescription . ']', 0)->innertext + . '</td><td>' + . $deal->find('div[class*=' . $selectorHot . ']', 0) + ->find('span', 1)->outertext + . '</td></table>'; + + // Check if a clock icon is displayed on the deal + $clocks = $deal->find('svg[class*=icon--clock]'); + if ($clocks !== null && count($clocks) > 0) { + // Get the last clock, corresponding to the deal posting date + $clock = end($clocks); + + // Find the text corresponding to the clock + $spanDateDiv = $clock->parent()->find('span[class=hide--toW3]', 0); + $itemDate = $spanDateDiv->plaintext; + // In case of a Local deal, there is no date, but we can use + // this case for other reason (like date not in the last field) + if ($this->contains($itemDate, $this->i8n('localdeal'))) { + $item['timestamp'] = time(); + } elseif ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) { + $item['timestamp'] = $this->relativeDateToTimestamp($itemDate); + } else { + $item['timestamp'] = $this->parseDate($itemDate); + } + } + $this->items[] = $item; + } + } + } + + /** + * Get the Talk lastest comments + */ + protected function collectDataTalk() + { + $threadURL = $this->getInput('url'); + $onlyWithUrl = $this->getInput('only_with_url'); + + // Get Thread ID from url passed in parameter + $threadSearch = preg_match('/-([0-9]{1,20})$/', $threadURL, $matches); + + // Show an error message if we can't find the thread ID in the URL sent by the user + if ($threadSearch !== 1) { + returnClientError($this->i8n('thread-error')); + } + $threadID = $matches[1]; + + $url = $this->i8n('bridge-uri') . 'graphql'; + + // Get Cookies header to do the query + $cookies = $this->getCookies($url); + + // GraphQL String + // This was extracted from https://www.dealabs.com/assets/js/modern/common_211b99.js + // This string was extracted during a Website visit, and minified using this neat tool : + // https://codepen.io/dangodev/pen/Baoqmoy + $graphqlString = <<<'HEREDOC' query comments($filter:CommentFilter!,$limit:Int,$page:Int){comments(filter:$filter,limit:$limit,page:$page){ items{...commentFields}pagination{...paginationFields}}}fragment commentFields on Comment{commentId threadId url preparedHtmlContent user{...userMediumAvatarFields...userNameFields...userPersonaFields bestBadge{...badgeFields}} @@ -182,501 +185,509 @@ fragment badgeLevelFields on BadgeLevel{key name description}fragment pagination next previous size order} HEREDOC; - // Construct the JSON object to send to the Website - $queryArray = array ( - 'query' => $graphqlString, - 'variables' => array ( - 'filter' => array ( - 'threadId' => array ( - 'eq' => $threadID, - ), - 'order' => array ( - 'direction' => 'Descending', - ), - - ), - 'page' => 1, - ), - ); - $queryJSON = json_encode($queryArray); - - // HTTP headers - $header = array( - 'Content-Type: application/json', - 'Accept: application/json, text/plain, */*', - 'X-Pepper-Txn: threads.show', - 'X-Request-Type: application/vnd.pepper.v1+json', - 'X-Requested-With: XMLHttpRequest', - $cookies, - ); - // CURL Options - $opts = array( - CURLOPT_POST => 1, - CURLOPT_POSTFIELDS => $queryJSON - ); - $json = getContents($url, $header, $opts); - $objects = json_decode($json); - foreach($objects->data->comments->items as $comment) { - $item = array(); - $item['uri'] = $comment->url; - $item['title'] = $comment->user->username . ' - ' . $comment->createdAt; - $item['author'] = $comment->user->username; - $item['content'] = $comment->preparedHtmlContent; - $item['uid'] = $comment->commentId; - // Timestamp handling needs a new parsing function - if($onlyWithUrl == true) { - // Count Links and Quote Links - $content = str_get_html($item['content']); - $countLinks = count($content->find('a[href]')); - $countQuoteLinks = count($content->find('a[href][class=userHtml-quote-source]')); - // Only add element if there are Links ans more links tant Quote links - if($countLinks > 0 && $countLinks > $countQuoteLinks) { - $this->items[] = $item; - } - } else { - $this->items[] = $item; - } - } - } - - /** - * Extract the cookies obtained from the URL - * @return array the array containing the cookies set by the URL - */ - private function getCookies($url) - { - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - // get headers too with this line - curl_setopt($ch, CURLOPT_HEADER, 1); - $result = curl_exec($ch); - // get cookie - // multi-cookie variant contributed by @Combuster in comments - preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $matches); - $cookies = array(); - foreach($matches[1] as $item) { - parse_str($item, $cookie); - $cookies = array_merge($cookies, $cookie); - } - $header = 'Cookie: '; - foreach($cookies as $name => $content) { - $header .= $name . '=' . $content . '; '; - } - return $header; - } - - /** - * Check if the string $str contains any of the string of the array $arr - * @return boolean true if the string matched anything otherwise false - */ - private function contains($str, array $arr) - { - foreach ($arr as $a) { - if (stripos($str, $a) !== false) { - return true; - } - } - return false; - } - - /** - * Get the Price from a Deal if it exists - * @return string String of the deal price - */ - private function getPrice($deal) - { - if ($deal->find( - 'span[class*=thread-price]', 0) != null) { - return '<div>' . $this->i8n('price') . ' : ' - . $deal->find( - 'span[class*=thread-price]', 0 - )->plaintext - . '</div>'; - } else { - return ''; - } - } - - /** - * Get the Title from a Deal if it exists - * @return string String of the deal title - */ - private function getTitle($deal) - { - - $titleRoot = $deal->find('div[class*=threadGrid-title]', 0); - $titleA = $titleRoot->find('a[class*=thread-link]', 0); - $titleFirstChild = $titleRoot->first_child(); - if($titleA !== null) { - $title = $titleA->plaintext; - } else { - // In some case, expired deals have a different format - $title = $titleRoot->find('span', 0)->plaintext; - } - - return $title; - - } - - /** - * Get the Title from a Talk if it exists - * @return string String of the Talk title - */ - private function getTalkTitle() - { - $html = getSimpleHTMLDOMCached($this->getInput('url')); - $title = $html->find('h1[class=thread-title]', 0)->plaintext; - return $title; - - } - - /** - * Get the HTML Title code from an item - * @return string String of the deal title - */ - private function getHTMLTitle($item) - { - if($item['uri'] == '') { - $html = '<h2>' . $item['title'] . '</h2>'; - } else { - $html = '<h2><a href="' . $item['uri'] . '">' - . $item['title'] . '</a></h2>'; - } - - return $html; - - } - - /** - * Get the URI from a Deal if it exists - * @return string String of the deal URI - */ - private function getDealURI($deal) - { - - $uriA = $deal->find('div[class*=threadGrid-title]', 0)->find('a[class*=thread-link]', 0); - if($uriA === null) { - $uri = ''; - } else { - $uri = $uriA->href; - } - - return $uri; - - } - - /** - * Get the Shipping costs from a Deal if it exists - * @return string String of the deal shipping Cost - */ - private function getShippingCost($deal) - { - if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0) != null) { - if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1) != null) { - return '<div>' . $this->i8n('shipping') . ' : ' - . $deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1)->innertext - . '</div>'; - } else { - return '<div>' . $this->i8n('shipping') . ' : ' - . $deal->find('span[class*=text--color-greyShade flex--inline]', 0)->innertext - . '</div>'; - } - } else { - return ''; - } - } - - /** - * Get the source of a Deal if it exists - * @return string String of the deal source - */ - private function getSource($deal) - { - if ($deal->find('a[class*=text--color-greyShade]', 0) != null) { - return '<div>' . $this->i8n('origin') . ' : ' - . $deal->find('a[class*=text--color-greyShade]', 0)->outertext - . '</div>'; - } else { - return ''; - } - } - - /** - * Get the original Price and discout from a Deal if it exists - * @return string String of the deal original price and discount - */ - private function getDiscount($deal) - { - if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) { - $discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0); - if ($discountHtml != null) { - $discount = $discountHtml->plaintext; - } else { - $discount = ''; - } - return '<div>' . $this->i8n('discount') . ' : <span style="text-decoration: line-through;">' - . $deal->find( - 'span[class*=mute--text text--lineThrough]', 0 - )->plaintext - . '</span> ' - . $discount - . '</div>'; - } else { - return ''; - } - } - - /** - * Get the Picture URL from a Deal if it exists - * @return string String of the deal Picture URL - */ - private function getImage($deal) - { - $selectorLazy = implode( - ' ', /* Notice this is a space! */ - array( - 'thread-image', - 'width--all-auto', - 'height--all-auto', - 'imgFrame-img', - 'img--dummy', - 'js-lazy-img' - ) - ); - - $selectorPlain = implode( - ' ', /* Notice this is a space! */ - array( - 'thread-image', - 'width--all-auto', - 'height--all-auto', - 'imgFrame-img', - ) - ); - if ($deal->find('img[class=' . $selectorLazy . ']', 0) != null) { - return json_decode( - html_entity_decode( - $deal->find('img[class=' . $selectorLazy . ']', 0) - ->getAttribute('data-lazy-img')))->{'src'}; - } else { - return $deal->find('img[class*=' . $selectorPlain . ']', 0 )->src; - } - } - - /** - * Get the originating country from a Deal if it exists - * @return string String of the deal originating country - */ - private function getShipsFrom($deal) - { - $selector = implode( - ' ', /* Notice this is a space! */ - array( - 'hide--toW2', - 'metaRibbon', - ) - ); - if ($deal->find('span[class*=' . $selector . ']', 0) != null) { - return '<div>' - . $deal->find('span[class*=' . $selector . ']', 0)->children(2)->plaintext - . '</div>'; - } else { - return ''; - } - } - - /** - * Transforms a local date into a timestamp - * @return int timestamp of the input date - */ - private function parseDate($string) - { - $month_local = $this->i8n('local-months'); - $month_en = array( - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' - ); - - // A date can be prfixed with some words, we remove theme - $string = $this->removeDatePrefixes($string); - // We translate the local months name in the english one - $date_str = trim(str_replace($month_local, $month_en, $string)); - - // If the date does not contain any year, we add the current year - if (!preg_match('/[0-9]{4}/', $string)) { - $date_str .= ' ' . date('Y'); - } - - // Add the Hour and minutes - $date_str .= ' 00:00'; - $date = DateTime::createFromFormat('j F Y H:i', $date_str); - // In some case, the date is not recognized : as a workaround the actual date is taken - if($date === false) { - $date = new DateTime(); - } - return $date->getTimestamp(); - } - - /** - * Remove the prefix of a date if it has one - * @return the date without prefiux - */ - private function removeDatePrefixes($string) - { - $string = str_replace($this->i8n('date-prefixes'), array(), $string); - return $string; - } - - /** - * Remove the suffix of a relative date if it has one - * @return the relative date without suffixes - */ - private function removeRelativeDateSuffixes($string) - { - if (count($this->i8n('relative-date-ignore-suffix')) > 0) { - $string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string); - } - return $string; - } - - /** - * Transforms a relative local date into a timestamp - * @return int timestamp of the input date - */ - private function relativeDateToTimestamp($str) { - $date = new DateTime(); - - // In case of update date, replace it by the regular relative date first word - $str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str); - - $str = $this->removeRelativeDateSuffixes($str); - - $search = $this->i8n('local-time-relative'); - - $replace = array( - '-', - 'minute', - 'hour', - 'day', - 'month', - 'year', - '' - ); - $date->modify(str_replace($search, $replace, $str)); - return $date->getTimestamp(); - } - - /** - * Returns the RSS Feed title according to the parameters - * @return string the RSS feed Tiyle - */ - public function getName(){ - switch($this->queriedContext) { - case $this->i8n('context-keyword'): - return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-keyword') . ' : ' . $this->getInput('q'); - break; - case $this->i8n('context-group'): - $values = $this->getParameters()[$this->i8n('context-group')]['group']['values']; - $group = array_search($this->getInput('group'), $values); - return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-group') . ' : ' . $group; - break; - case $this->i8n('context-talk'): - return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-talk') . ' : ' . $this->getTalkTitle(); - break; - default: // Return default value - return static::NAME; - } - } - - /** - * Returns the RSS Feed URI according to the parameters - * @return string the RSS feed Title - */ - public function getURI(){ - switch($this->queriedContext) { - case $this->i8n('context-keyword'): - return $this->getSearchURI(); - break; - case $this->i8n('context-group'): - return $this->getGroupURI(); - break; - case $this->i8n('context-talk'): - return $this->getTalkURI(); - break; - default: // Return default value - return static::URI; - } - } - - /** - * Returns the RSS Feed URI for a keyword Feed - * @return string the RSS feed URI - */ - private function getSearchURI(){ - $q = $this->getInput('q'); - $hide_expired = $this->getInput('hide_expired'); - $hide_local = $this->getInput('hide_local'); - $priceFrom = $this->getInput('priceFrom'); - $priceTo = $this->getInput('priceTo'); - $url = $this->i8n('bridge-uri') - . 'search/advanced?q=' - . urlencode($q) - . '&hide_expired=' . $hide_expired - . '&hide_local=' . $hide_local - . '&priceFrom=' . $priceFrom - . '&priceTo=' . $priceTo - /* Some default parameters - * search_fields : Search in Titres & Descriptions & Codes - * sort_by : Sort the search by new deals - * time_frame : Search will not be on a limited timeframe - */ - . '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0'; - return $url; - } - - /** - * Returns the RSS Feed URI for a group Feed - * @return string the RSS feed URI - */ - private function getGroupURI(){ - $group = $this->getInput('group'); - $order = $this->getInput('order'); - - $url = $this->i8n('bridge-uri') - . $this->i8n('uri-group') . $group . $order; - return $url; - } - - /** - * Returns the RSS Feed URI for a Talk Feed - * @return string the RSS feed URI - */ - private function getTalkURI(){ - $url = $this->getInput('url'); - return $url; - } - - /** - * This is some "localisation" function that returns the needed content using - * the "$lang" class variable in the local class - * @return various the local content needed - */ - protected function i8n($key) - { - if (array_key_exists($key, $this->lang)) { - return $this->lang[$key]; - } else { - return null; - } - } + // Construct the JSON object to send to the Website + $queryArray = [ + 'query' => $graphqlString, + 'variables' => [ + 'filter' => [ + 'threadId' => [ + 'eq' => $threadID, + ], + 'order' => [ + 'direction' => 'Descending', + ], + + ], + 'page' => 1, + ], + ]; + $queryJSON = json_encode($queryArray); + + // HTTP headers + $header = [ + 'Content-Type: application/json', + 'Accept: application/json, text/plain, */*', + 'X-Pepper-Txn: threads.show', + 'X-Request-Type: application/vnd.pepper.v1+json', + 'X-Requested-With: XMLHttpRequest', + $cookies, + ]; + // CURL Options + $opts = [ + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => $queryJSON + ]; + $json = getContents($url, $header, $opts); + $objects = json_decode($json); + foreach ($objects->data->comments->items as $comment) { + $item = []; + $item['uri'] = $comment->url; + $item['title'] = $comment->user->username . ' - ' . $comment->createdAt; + $item['author'] = $comment->user->username; + $item['content'] = $comment->preparedHtmlContent; + $item['uid'] = $comment->commentId; + // Timestamp handling needs a new parsing function + if ($onlyWithUrl == true) { + // Count Links and Quote Links + $content = str_get_html($item['content']); + $countLinks = count($content->find('a[href]')); + $countQuoteLinks = count($content->find('a[href][class=userHtml-quote-source]')); + // Only add element if there are Links ans more links tant Quote links + if ($countLinks > 0 && $countLinks > $countQuoteLinks) { + $this->items[] = $item; + } + } else { + $this->items[] = $item; + } + } + } + + /** + * Extract the cookies obtained from the URL + * @return array the array containing the cookies set by the URL + */ + private function getCookies($url) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + // get headers too with this line + curl_setopt($ch, CURLOPT_HEADER, 1); + $result = curl_exec($ch); + // get cookie + // multi-cookie variant contributed by @Combuster in comments + preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $matches); + $cookies = []; + foreach ($matches[1] as $item) { + parse_str($item, $cookie); + $cookies = array_merge($cookies, $cookie); + } + $header = 'Cookie: '; + foreach ($cookies as $name => $content) { + $header .= $name . '=' . $content . '; '; + } + return $header; + } + + /** + * Check if the string $str contains any of the string of the array $arr + * @return boolean true if the string matched anything otherwise false + */ + private function contains($str, array $arr) + { + foreach ($arr as $a) { + if (stripos($str, $a) !== false) { + return true; + } + } + return false; + } + + /** + * Get the Price from a Deal if it exists + * @return string String of the deal price + */ + private function getPrice($deal) + { + if ( + $deal->find( + 'span[class*=thread-price]', + 0 + ) != null + ) { + return '<div>' . $this->i8n('price') . ' : ' + . $deal->find( + 'span[class*=thread-price]', + 0 + )->plaintext + . '</div>'; + } else { + return ''; + } + } + + /** + * Get the Title from a Deal if it exists + * @return string String of the deal title + */ + private function getTitle($deal) + { + $titleRoot = $deal->find('div[class*=threadGrid-title]', 0); + $titleA = $titleRoot->find('a[class*=thread-link]', 0); + $titleFirstChild = $titleRoot->first_child(); + if ($titleA !== null) { + $title = $titleA->plaintext; + } else { + // In some case, expired deals have a different format + $title = $titleRoot->find('span', 0)->plaintext; + } + + return $title; + } + + /** + * Get the Title from a Talk if it exists + * @return string String of the Talk title + */ + private function getTalkTitle() + { + $html = getSimpleHTMLDOMCached($this->getInput('url')); + $title = $html->find('h1[class=thread-title]', 0)->plaintext; + return $title; + } + + /** + * Get the HTML Title code from an item + * @return string String of the deal title + */ + private function getHTMLTitle($item) + { + if ($item['uri'] == '') { + $html = '<h2>' . $item['title'] . '</h2>'; + } else { + $html = '<h2><a href="' . $item['uri'] . '">' + . $item['title'] . '</a></h2>'; + } + + return $html; + } + + /** + * Get the URI from a Deal if it exists + * @return string String of the deal URI + */ + private function getDealURI($deal) + { + $uriA = $deal->find('div[class*=threadGrid-title]', 0)->find('a[class*=thread-link]', 0); + if ($uriA === null) { + $uri = ''; + } else { + $uri = $uriA->href; + } + + return $uri; + } + + /** + * Get the Shipping costs from a Deal if it exists + * @return string String of the deal shipping Cost + */ + private function getShippingCost($deal) + { + if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0) != null) { + if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1) != null) { + return '<div>' . $this->i8n('shipping') . ' : ' + . $deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1)->innertext + . '</div>'; + } else { + return '<div>' . $this->i8n('shipping') . ' : ' + . $deal->find('span[class*=text--color-greyShade flex--inline]', 0)->innertext + . '</div>'; + } + } else { + return ''; + } + } + + /** + * Get the source of a Deal if it exists + * @return string String of the deal source + */ + private function getSource($deal) + { + if ($deal->find('a[class*=text--color-greyShade]', 0) != null) { + return '<div>' . $this->i8n('origin') . ' : ' + . $deal->find('a[class*=text--color-greyShade]', 0)->outertext + . '</div>'; + } else { + return ''; + } + } + + /** + * Get the original Price and discout from a Deal if it exists + * @return string String of the deal original price and discount + */ + private function getDiscount($deal) + { + if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) { + $discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0); + if ($discountHtml != null) { + $discount = $discountHtml->plaintext; + } else { + $discount = ''; + } + return '<div>' . $this->i8n('discount') . ' : <span style="text-decoration: line-through;">' + . $deal->find( + 'span[class*=mute--text text--lineThrough]', + 0 + )->plaintext + . '</span> ' + . $discount + . '</div>'; + } else { + return ''; + } + } + + /** + * Get the Picture URL from a Deal if it exists + * @return string String of the deal Picture URL + */ + private function getImage($deal) + { + $selectorLazy = implode( + ' ', /* Notice this is a space! */ + [ + 'thread-image', + 'width--all-auto', + 'height--all-auto', + 'imgFrame-img', + 'img--dummy', + 'js-lazy-img' + ] + ); + + $selectorPlain = implode( + ' ', /* Notice this is a space! */ + [ + 'thread-image', + 'width--all-auto', + 'height--all-auto', + 'imgFrame-img', + ] + ); + if ($deal->find('img[class=' . $selectorLazy . ']', 0) != null) { + return json_decode( + html_entity_decode( + $deal->find('img[class=' . $selectorLazy . ']', 0) + ->getAttribute('data-lazy-img') + ) + )->{'src'}; + } else { + return $deal->find('img[class*=' . $selectorPlain . ']', 0)->src; + } + } + + /** + * Get the originating country from a Deal if it exists + * @return string String of the deal originating country + */ + private function getShipsFrom($deal) + { + $selector = implode( + ' ', /* Notice this is a space! */ + [ + 'hide--toW2', + 'metaRibbon', + ] + ); + if ($deal->find('span[class*=' . $selector . ']', 0) != null) { + return '<div>' + . $deal->find('span[class*=' . $selector . ']', 0)->children(2)->plaintext + . '</div>'; + } else { + return ''; + } + } + + /** + * Transforms a local date into a timestamp + * @return int timestamp of the input date + */ + private function parseDate($string) + { + $month_local = $this->i8n('local-months'); + $month_en = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + + // A date can be prfixed with some words, we remove theme + $string = $this->removeDatePrefixes($string); + // We translate the local months name in the english one + $date_str = trim(str_replace($month_local, $month_en, $string)); + + // If the date does not contain any year, we add the current year + if (!preg_match('/[0-9]{4}/', $string)) { + $date_str .= ' ' . date('Y'); + } + + // Add the Hour and minutes + $date_str .= ' 00:00'; + $date = DateTime::createFromFormat('j F Y H:i', $date_str); + // In some case, the date is not recognized : as a workaround the actual date is taken + if ($date === false) { + $date = new DateTime(); + } + return $date->getTimestamp(); + } + + /** + * Remove the prefix of a date if it has one + * @return the date without prefiux + */ + private function removeDatePrefixes($string) + { + $string = str_replace($this->i8n('date-prefixes'), [], $string); + return $string; + } + + /** + * Remove the suffix of a relative date if it has one + * @return the relative date without suffixes + */ + private function removeRelativeDateSuffixes($string) + { + if (count($this->i8n('relative-date-ignore-suffix')) > 0) { + $string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string); + } + return $string; + } + + /** + * Transforms a relative local date into a timestamp + * @return int timestamp of the input date + */ + private function relativeDateToTimestamp($str) + { + $date = new DateTime(); + + // In case of update date, replace it by the regular relative date first word + $str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str); + + $str = $this->removeRelativeDateSuffixes($str); + + $search = $this->i8n('local-time-relative'); + + $replace = [ + '-', + 'minute', + 'hour', + 'day', + 'month', + 'year', + '' + ]; + $date->modify(str_replace($search, $replace, $str)); + return $date->getTimestamp(); + } + + /** + * Returns the RSS Feed title according to the parameters + * @return string the RSS feed Tiyle + */ + public function getName() + { + switch ($this->queriedContext) { + case $this->i8n('context-keyword'): + return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-keyword') . ' : ' . $this->getInput('q'); + break; + case $this->i8n('context-group'): + $values = $this->getParameters()[$this->i8n('context-group')]['group']['values']; + $group = array_search($this->getInput('group'), $values); + return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-group') . ' : ' . $group; + break; + case $this->i8n('context-talk'): + return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-talk') . ' : ' . $this->getTalkTitle(); + break; + default: // Return default value + return static::NAME; + } + } + + /** + * Returns the RSS Feed URI according to the parameters + * @return string the RSS feed Title + */ + public function getURI() + { + switch ($this->queriedContext) { + case $this->i8n('context-keyword'): + return $this->getSearchURI(); + break; + case $this->i8n('context-group'): + return $this->getGroupURI(); + break; + case $this->i8n('context-talk'): + return $this->getTalkURI(); + break; + default: // Return default value + return static::URI; + } + } + + /** + * Returns the RSS Feed URI for a keyword Feed + * @return string the RSS feed URI + */ + private function getSearchURI() + { + $q = $this->getInput('q'); + $hide_expired = $this->getInput('hide_expired'); + $hide_local = $this->getInput('hide_local'); + $priceFrom = $this->getInput('priceFrom'); + $priceTo = $this->getInput('priceTo'); + $url = $this->i8n('bridge-uri') + . 'search/advanced?q=' + . urlencode($q) + . '&hide_expired=' . $hide_expired + . '&hide_local=' . $hide_local + . '&priceFrom=' . $priceFrom + . '&priceTo=' . $priceTo + /* Some default parameters + * search_fields : Search in Titres & Descriptions & Codes + * sort_by : Sort the search by new deals + * time_frame : Search will not be on a limited timeframe + */ + . '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0'; + return $url; + } + + /** + * Returns the RSS Feed URI for a group Feed + * @return string the RSS feed URI + */ + private function getGroupURI() + { + $group = $this->getInput('group'); + $order = $this->getInput('order'); + + $url = $this->i8n('bridge-uri') + . $this->i8n('uri-group') . $group . $order; + return $url; + } + + /** + * Returns the RSS Feed URI for a Talk Feed + * @return string the RSS feed URI + */ + private function getTalkURI() + { + $url = $this->getInput('url'); + return $url; + } + + /** + * This is some "localisation" function that returns the needed content using + * the "$lang" class variable in the local class + * @return various the local content needed + */ + protected function i8n($key) + { + if (array_key_exists($key, $this->lang)) { + return $this->lang[$key]; + } else { + return null; + } + } } diff --git a/bridges/PhoronixBridge.php b/bridges/PhoronixBridge.php index 76d35f5d..620fda66 100644 --- a/bridges/PhoronixBridge.php +++ b/bridges/PhoronixBridge.php @@ -1,65 +1,69 @@ <?php -class PhoronixBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'Phoronix Bridge'; - const URI = 'https://www.phoronix.com'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'RSS feed for Linux news website Phoronix'; - const PARAMETERS = array(array( - 'n' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Maximum number of items to return', - 'defaultValue' => 10 - ), - 'svgAsImg' => array( - 'name' => 'SVG in "image" tag', - 'type' => 'checkbox', - 'title' => 'Some benchmarks are exported as SVG with "object" tag, +class PhoronixBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'Phoronix Bridge'; + const URI = 'https://www.phoronix.com'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'RSS feed for Linux news website Phoronix'; + const PARAMETERS = [[ + 'n' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Maximum number of items to return', + 'defaultValue' => 10 + ], + 'svgAsImg' => [ + 'name' => 'SVG in "image" tag', + 'type' => 'checkbox', + 'title' => 'Some benchmarks are exported as SVG with "object" tag, but some RSS readers don\'t support this. "img" tag are supported by most browsers', - 'defaultValue' => false - ), - )); + 'defaultValue' => false + ], + ]]; - public function collectData(){ - $this->collectExpandableDatas('https://www.phoronix.com/rss.php', $this->getInput('n')); - } + public function collectData() + { + $this->collectExpandableDatas('https://www.phoronix.com/rss.php', $this->getInput('n')); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); - $articlePage = defaultLinkTo($articlePage, $this->getURI()); - // Extract final link. From Facebook's like plugin. - parse_str(parse_url($articlePage->find('iframe[src^=//www.facebook.com/plugins]', 0), PHP_URL_QUERY), $facebookQuery); - if (array_key_exists('href', $facebookQuery)) { - $newsItem->link = $facebookQuery['href']; - } - $item['content'] = $this->extractContent($articlePage); + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + $articlePage = defaultLinkTo($articlePage, $this->getURI()); + // Extract final link. From Facebook's like plugin. + parse_str(parse_url($articlePage->find('iframe[src^=//www.facebook.com/plugins]', 0), PHP_URL_QUERY), $facebookQuery); + if (array_key_exists('href', $facebookQuery)) { + $newsItem->link = $facebookQuery['href']; + } + $item['content'] = $this->extractContent($articlePage); - $pages = $articlePage->find('.pagination a[!title]'); - foreach ($pages as $page) { - $pageURI = urljoin($newsItem->link, html_entity_decode($page->href)); - $page = getSimpleHTMLDOM($pageURI); - $item['content'] .= $this->extractContent($page); - } - return $item; - } + $pages = $articlePage->find('.pagination a[!title]'); + foreach ($pages as $page) { + $pageURI = urljoin($newsItem->link, html_entity_decode($page->href)); + $page = getSimpleHTMLDOM($pageURI); + $item['content'] .= $this->extractContent($page); + } + return $item; + } - private function extractContent($page){ - $content = $page->find('.content', 0); - $objects = $content->find('script[src^=//openbenchmarking.org]'); - foreach ($objects as $object) { - $objectSrc = preg_replace('/p=0/', 'p=2', $object->src); - if ($this->getInput('svgAsImg')) { - $object->outertext = '<a href="' . $objectSrc . '"><img src="' . $objectSrc . '"/></a>'; - } else { - $object->outertext = '<object data="' . $objectSrc . '" type="image/svg+xml"></object>'; - } - } - $content = stripWithDelimiters($content, '<script', '</script>'); - return $content; - } + private function extractContent($page) + { + $content = $page->find('.content', 0); + $objects = $content->find('script[src^=//openbenchmarking.org]'); + foreach ($objects as $object) { + $objectSrc = preg_replace('/p=0/', 'p=2', $object->src); + if ($this->getInput('svgAsImg')) { + $object->outertext = '<a href="' . $objectSrc . '"><img src="' . $objectSrc . '"/></a>'; + } else { + $object->outertext = '<object data="' . $objectSrc . '" type="image/svg+xml"></object>'; + } + } + $content = stripWithDelimiters($content, '<script', '</script>'); + return $content; + } } diff --git a/bridges/PicalaBridge.php b/bridges/PicalaBridge.php index 46e2edbb..35f73d0a 100644 --- a/bridges/PicalaBridge.php +++ b/bridges/PicalaBridge.php @@ -1,69 +1,75 @@ <?php -class PicalaBridge extends BridgeAbstract { - const TYPES = array( - 'Actualités' => 'actualites', - 'Économie' => 'economie', - 'Tests' => 'tests', - 'Pratique' => 'pratique', - ); - const NAME = 'Picala Bridge'; - const URI = 'https://www.picala.fr'; - const DESCRIPTION = 'Dernière nouvelles du média indépendant sur le vélo électrique'; - const MAINTAINER = 'Chouchen'; - const PARAMETERS = array( - array( - 'type' => array( - 'name' => 'Type', - 'type' => 'list', - 'values' => self::TYPES, - ), - ), - ); +class PicalaBridge extends BridgeAbstract +{ + const TYPES = [ + 'Actualités' => 'actualites', + 'Économie' => 'economie', + 'Tests' => 'tests', + 'Pratique' => 'pratique', + ]; + const NAME = 'Picala Bridge'; + const URI = 'https://www.picala.fr'; + const DESCRIPTION = 'Dernière nouvelles du média indépendant sur le vélo électrique'; + const MAINTAINER = 'Chouchen'; + const PARAMETERS = [ + [ + 'type' => [ + 'name' => 'Type', + 'type' => 'list', + 'values' => self::TYPES, + ], + ], + ]; - public function getURI() { - if(!is_null($this->getInput('type'))) { - return sprintf('%s/%s', static::URI, $this->getInput('type')); - } + public function getURI() + { + if (!is_null($this->getInput('type'))) { + return sprintf('%s/%s', static::URI, $this->getInput('type')); + } - return parent::getURI(); - } + return parent::getURI(); + } - public function getIcon() { - return 'https://picala-static.s3.amazonaws.com/static/img/favicon/favicon-32x32.png'; - } + public function getIcon() + { + return 'https://picala-static.s3.amazonaws.com/static/img/favicon/favicon-32x32.png'; + } - public function getDescription() { - if(!is_null($this->getInput('type'))) { - return sprintf('%s - %s', static::DESCRIPTION, array_search($this->getInput('type'), self::TYPES)); - } + public function getDescription() + { + if (!is_null($this->getInput('type'))) { + return sprintf('%s - %s', static::DESCRIPTION, array_search($this->getInput('type'), self::TYPES)); + } - return parent::getDescription(); - } + return parent::getDescription(); + } - public function getName() { - if(!is_null($this->getInput('type'))) { - return sprintf('%s - %s', static::NAME, array_search($this->getInput('type'), self::TYPES)); - } + public function getName() + { + if (!is_null($this->getInput('type'))) { + return sprintf('%s - %s', static::NAME, array_search($this->getInput('type'), self::TYPES)); + } - return parent::getName(); - } + return parent::getName(); + } - public function collectData() { - $fullhtml = getSimpleHTMLDOM($this->getURI()); - foreach($fullhtml->find('.list-container-category a') as $article) { - $srcsets = explode(',', $article->find('img', 0)->getAttribute('srcset')); - $image = explode(' ', trim(array_shift($srcsets)))[0]; + public function collectData() + { + $fullhtml = getSimpleHTMLDOM($this->getURI()); + foreach ($fullhtml->find('.list-container-category a') as $article) { + $srcsets = explode(',', $article->find('img', 0)->getAttribute('srcset')); + $image = explode(' ', trim(array_shift($srcsets)))[0]; - $item = array(); - $item['uri'] = self::URI . $article->href; - $item['title'] = $article->find('h2', 0)->plaintext; - $item['content'] = sprintf( - '<img src="%s" /><br>%s', - $image, - $article->find('.teaser__text', 0)->plaintext - ); - $this->items[] = $item; - } - } + $item = []; + $item['uri'] = self::URI . $article->href; + $item['title'] = $article->find('h2', 0)->plaintext; + $item['content'] = sprintf( + '<img src="%s" /><br>%s', + $image, + $article->find('.teaser__text', 0)->plaintext + ); + $this->items[] = $item; + } + } } diff --git a/bridges/PickyWallpapersBridge.php b/bridges/PickyWallpapersBridge.php index 2c4f0be3..29fdc1aa 100644 --- a/bridges/PickyWallpapersBridge.php +++ b/bridges/PickyWallpapersBridge.php @@ -1,101 +1,106 @@ <?php -class PickyWallpapersBridge extends BridgeAbstract { - const MAINTAINER = 'nel50n'; - const NAME = 'PickyWallpapers Bridge'; - const URI = 'https://www.pickywallpapers.com/'; - const CACHE_TIMEOUT = 43200; // 12h - const DESCRIPTION = 'Returns the latests wallpapers from PickyWallpapers'; +class PickyWallpapersBridge extends BridgeAbstract +{ + const MAINTAINER = 'nel50n'; + const NAME = 'PickyWallpapers Bridge'; + const URI = 'https://www.pickywallpapers.com/'; + const CACHE_TIMEOUT = 43200; // 12h + const DESCRIPTION = 'Returns the latests wallpapers from PickyWallpapers'; - const PARAMETERS = array( array( - 'c' => array( - 'name' => 'category', - 'exampleValue' => 'funny', - 'required' => true - ), - 's' => array( - 'name' => 'subcategory' - ), - 'm' => array( - 'name' => 'Max number of wallpapers', - 'defaultValue' => 12, - 'type' => 'number' - ), - 'r' => array( - 'name' => 'resolution', - 'exampleValue' => '1920x1200, 1680x1050,…', - 'defaultValue' => '1920x1200', - 'pattern' => '[0-9]{3,4}x[0-9]{3,4}' - ) - )); + const PARAMETERS = [ [ + 'c' => [ + 'name' => 'category', + 'exampleValue' => 'funny', + 'required' => true + ], + 's' => [ + 'name' => 'subcategory' + ], + 'm' => [ + 'name' => 'Max number of wallpapers', + 'defaultValue' => 12, + 'type' => 'number' + ], + 'r' => [ + 'name' => 'resolution', + 'exampleValue' => '1920x1200, 1680x1050,…', + 'defaultValue' => '1920x1200', + 'pattern' => '[0-9]{3,4}x[0-9]{3,4}' + ] + ]]; - public function collectData(){ - $lastpage = 1; - $num = 0; - $max = $this->getInput('m'); - $resolution = $this->getInput('r'); // Wide wallpaper default + public function collectData() + { + $lastpage = 1; + $num = 0; + $max = $this->getInput('m'); + $resolution = $this->getInput('r'); // Wide wallpaper default - for($page = 1; $page <= $lastpage; $page++) { - $html = getSimpleHTMLDOM($this->getURI() . '/page-' . $page . '/'); + for ($page = 1; $page <= $lastpage; $page++) { + $html = getSimpleHTMLDOM($this->getURI() . '/page-' . $page . '/'); - if($page === 1) { - preg_match('/page-(\d+)\/$/', $html->find('.pages li a', -2)->href, $matches); - $lastpage = min($matches[1], ceil($max / 12)); - } + if ($page === 1) { + preg_match('/page-(\d+)\/$/', $html->find('.pages li a', -2)->href, $matches); + $lastpage = min($matches[1], ceil($max / 12)); + } - foreach($html->find('.items li img') as $element) { - $item = array(); - $item['uri'] = str_replace('www', 'wallpaper', self::URI) - . '/' - . $resolution - . '/' - . basename($element->src); + foreach ($html->find('.items li img') as $element) { + $item = []; + $item['uri'] = str_replace('www', 'wallpaper', self::URI) + . '/' + . $resolution + . '/' + . basename($element->src); - $item['timestamp'] = time(); - $item['title'] = $element->alt; - $item['content'] = $item['title'] - . '<br><a href="' - . $item['uri'] - . '">' - . $element - . '</a>'; + $item['timestamp'] = time(); + $item['title'] = $element->alt; + $item['content'] = $item['title'] + . '<br><a href="' + . $item['uri'] + . '">' + . $element + . '</a>'; - $this->items[] = $item; + $this->items[] = $item; - $num++; - if ($num >= $max) - break 2; - } - } - } + $num++; + if ($num >= $max) { + break 2; + } + } + } + } - public function getURI(){ - if(!is_null($this->getInput('s')) && !is_null($this->getInput('r')) && !is_null($this->getInput('c'))) { - $subcategory = $this->getInput('s'); - $link = self::URI - . $this->getInput('r') - . '/' - . $this->getInput('c') - . '/' - . $subcategory; + public function getURI() + { + if (!is_null($this->getInput('s')) && !is_null($this->getInput('r')) && !is_null($this->getInput('c'))) { + $subcategory = $this->getInput('s'); + $link = self::URI + . $this->getInput('r') + . '/' + . $this->getInput('c') + . '/' + . $subcategory; - return $link; - } + return $link; + } - return parent::getURI(); - } + return parent::getURI(); + } - public function getName(){ - if(!is_null($this->getInput('s'))) { - $subcategory = $this->getInput('s'); - return 'PickyWallpapers - ' - . $this->getInput('c') - . ($subcategory ? ' > ' . $subcategory : '') - . ' [' - . $this->getInput('r') - . ']'; - } + public function getName() + { + if (!is_null($this->getInput('s'))) { + $subcategory = $this->getInput('s'); + return 'PickyWallpapers - ' + . $this->getInput('c') + . ($subcategory ? ' > ' . $subcategory : '') + . ' [' + . $this->getInput('r') + . ']'; + } - return parent::getName(); - } + return parent::getName(); + } } diff --git a/bridges/PicukiBridge.php b/bridges/PicukiBridge.php index 3c2df739..9f9acf6b 100644 --- a/bridges/PicukiBridge.php +++ b/bridges/PicukiBridge.php @@ -1,103 +1,104 @@ <?php + class PicukiBridge extends BridgeAbstract { - const MAINTAINER = 'marcus-at-localhost'; - const NAME = 'Picuki Bridge'; - const URI = 'https://www.picuki.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns Picuki posts by user and by hashtag'; - - const PARAMETERS = array( - 'Username' => array( - 'u' => array( - 'name' => 'username', - 'exampleValue' => 'aesoprockwins', - 'required' => true, - ), - ), - 'Hashtag' => array( - 'h' => array( - 'name' => 'hashtag', - 'exampleValue' => 'beautifulday', - 'required' => true, - ), - ) - ); - - public function getURI() - { - if (!is_null($this->getInput('u'))) { - return urljoin(self::URI, '/profile/' . $this->getInput('u')); - } - - if (!is_null($this->getInput('h'))) { - return urljoin(self::URI, '/tag/' . trim($this->getInput('h'), '#')); - } - - return parent::getURI(); - } - - public function collectData() - { - $html = getSimpleHTMLDOM($this->getURI()); - - foreach ($html->find('.box-photos .box-photo') as $element) { - // skip ad items - if (in_array('adv', explode(' ', $element->class))) { - continue; - } - - $url = urljoin(self::URI, $element->find('a', 0)->href); - - $author = trim($element->find('.user-nickname', 0)->plaintext); - - $date = date_create(); - $relativeDate = str_replace(' ago', '', $element->find('.time', 0)->plaintext); - date_sub($date, date_interval_create_from_date_string($relativeDate)); - - $description = trim($element->find('.photo-description', 0)->plaintext); - - $isVideo = (bool) $element->find('.video-icon', 0); - $videoNote = $isVideo ? '<p><i>(video)</i></p>' : ''; - - $imageUrl = $element->find('.post-image', 0)->src; - - // the last path segment needs to be encoded, because it contains special characters like + or | - $imageUrlParts = explode('/', $imageUrl); - $imageUrlParts[count($imageUrlParts) - 1] = urlencode($imageUrlParts[count($imageUrlParts) - 1]); - $imageUrl = implode('/', $imageUrlParts); - - // add fake file extension for it to be recognized as image/jpeg instead of application/octet-stream - $imageUrl = $imageUrl . '#.jpg'; - - $this->items[] = array( - 'uri' => $url, - 'author' => $author, - 'timestamp' => date_format($date, 'r'), - 'title' => strlen($description) > 60 ? mb_substr($description, 0, 57) . '...' : $description, - 'thumbnail' => $imageUrl, - 'enclosures' => [$imageUrl], - 'content' => <<<HTML + const MAINTAINER = 'marcus-at-localhost'; + const NAME = 'Picuki Bridge'; + const URI = 'https://www.picuki.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns Picuki posts by user and by hashtag'; + + const PARAMETERS = [ + 'Username' => [ + 'u' => [ + 'name' => 'username', + 'exampleValue' => 'aesoprockwins', + 'required' => true, + ], + ], + 'Hashtag' => [ + 'h' => [ + 'name' => 'hashtag', + 'exampleValue' => 'beautifulday', + 'required' => true, + ], + ] + ]; + + public function getURI() + { + if (!is_null($this->getInput('u'))) { + return urljoin(self::URI, '/profile/' . $this->getInput('u')); + } + + if (!is_null($this->getInput('h'))) { + return urljoin(self::URI, '/tag/' . trim($this->getInput('h'), '#')); + } + + return parent::getURI(); + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + foreach ($html->find('.box-photos .box-photo') as $element) { + // skip ad items + if (in_array('adv', explode(' ', $element->class))) { + continue; + } + + $url = urljoin(self::URI, $element->find('a', 0)->href); + + $author = trim($element->find('.user-nickname', 0)->plaintext); + + $date = date_create(); + $relativeDate = str_replace(' ago', '', $element->find('.time', 0)->plaintext); + date_sub($date, date_interval_create_from_date_string($relativeDate)); + + $description = trim($element->find('.photo-description', 0)->plaintext); + + $isVideo = (bool) $element->find('.video-icon', 0); + $videoNote = $isVideo ? '<p><i>(video)</i></p>' : ''; + + $imageUrl = $element->find('.post-image', 0)->src; + + // the last path segment needs to be encoded, because it contains special characters like + or | + $imageUrlParts = explode('/', $imageUrl); + $imageUrlParts[count($imageUrlParts) - 1] = urlencode($imageUrlParts[count($imageUrlParts) - 1]); + $imageUrl = implode('/', $imageUrlParts); + + // add fake file extension for it to be recognized as image/jpeg instead of application/octet-stream + $imageUrl = $imageUrl . '#.jpg'; + + $this->items[] = [ + 'uri' => $url, + 'author' => $author, + 'timestamp' => date_format($date, 'r'), + 'title' => strlen($description) > 60 ? mb_substr($description, 0, 57) . '...' : $description, + 'thumbnail' => $imageUrl, + 'enclosures' => [$imageUrl], + 'content' => <<<HTML <a href="{$url}"> <img loading="lazy" src="{$imageUrl}" /> </a> {$videoNote} <p>{$description}<p> HTML - ); - } - } - - public function getName() - { - if (!is_null($this->getInput('u'))) { - return $this->getInput('u') . ' - Picuki Bridge'; - } - - if (!is_null($this->getInput('h'))) { - return $this->getInput('h') . ' - Picuki Bridge'; - } - - return parent::getName(); - } + ]; + } + } + + public function getName() + { + if (!is_null($this->getInput('u'))) { + return $this->getInput('u') . ' - Picuki Bridge'; + } + + if (!is_null($this->getInput('h'))) { + return $this->getInput('h') . ' - Picuki Bridge'; + } + + return parent::getName(); + } } diff --git a/bridges/PikabuBridge.php b/bridges/PikabuBridge.php index 671d7a15..237f2a94 100644 --- a/bridges/PikabuBridge.php +++ b/bridges/PikabuBridge.php @@ -1,150 +1,158 @@ <?php -class PikabuBridge extends BridgeAbstract { - - const NAME = 'Пикабу'; - const URI = 'https://pikabu.ru'; - const DESCRIPTION = 'Выводит посты по тегу, сообществу или пользователю'; - const MAINTAINER = 'em92'; - - const PARAMETERS_FILTER = array( - 'name' => 'Фильтр', - 'type' => 'list', - 'values' => array( - 'Горячее' => 'hot', - 'Свежее' => 'new', - ), - 'defaultValue' => 'hot' - ); - - const PARAMETERS = array( - 'По тегу' => array( - 'tag' => array( - 'name' => 'Тег', - 'exampleValue' => 'it', - 'required' => true - ), - 'filter' => self::PARAMETERS_FILTER - ), - 'По сообществу' => array( - 'community' => array( - 'name' => 'Сообщество', - 'exampleValue' => 'linux', - 'required' => true - ), - 'filter' => self::PARAMETERS_FILTER - ), - 'По пользователю' => array( - 'user' => array( - 'name' => 'Пользователь', - 'exampleValue' => 'admin', - 'required' => true - ) - ) - ); - - protected $title = null; - - public function getURI() { - if ($this->getInput('tag')) { - return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter')); - } else if ($this->getInput('user')) { - return self::URI . '/@' . rawurlencode($this->getInput('user')); - } else if ($this->getInput('community')) { - $uri = self::URI . '/community/' . rawurlencode($this->getInput('community')); - if ($this->getInput('filter') != 'hot') { - $uri .= '/' . rawurlencode($this->getInput('filter')); - } - return $uri; - } else { - return parent::getURI(); - } - } - - public function getIcon() { - return 'https://cs.pikabu.ru/assets/favicon.ico'; - } - - public function getName() { - if (is_null($this->title)) { - return parent::getName(); - } else { - return $this->title . ' - ' . parent::getName(); - } - } - - public function collectData(){ - $link = $this->getURI(); - - $text_html = getContents($link); - $text_html = iconv('windows-1251', 'utf-8', $text_html); - $html = str_get_html($text_html); - - $this->title = $html->find('title', 0)->innertext; - - foreach($html->find('article.story') as $post) { - $time = $post->find('time.story__datetime', 0); - if (is_null($time)) continue; - - $el_to_remove_selectors = array( - '.story__read-more', - 'script', - 'svg.story-image__stretch', - ); - - foreach($el_to_remove_selectors as $el_to_remove_selector) { - foreach($post->find($el_to_remove_selector) as $el) { - $el->outertext = ''; - } - } - - foreach($post->find('[data-type=gifx]') as $el) { - $src = $el->getAttribute('data-source'); - $el->outertext = '<img src="' . $src . '">'; - } - - foreach($post->find('img') as $img) { - $src = $img->getAttribute('src'); - if (!$src) { - $src = $img->getAttribute('data-src'); - if (!$src) { - continue; - } - } - $img->outertext = '<img src="' . $src . '">'; - - // it is assumed, that img's parents are links to post itself - // we don't need them - $img->parent()->outertext = $img->outertext; - } - - $categories = array(); - foreach($post->find('.tags__tag') as $tag) { - if ($tag->getAttribute('data-tag')) { - $categories[] = $tag->innertext; - } - } - - $title_element = $post->find('.story__title-link', 0); - - $title = $title_element->plaintext; - $community_link = $post->find('.story__community-link', 0); - // adding special marker for "Maybe News" section - // these posts are fake - if (!is_null($community_link) && $community_link->getAttribute('href') == '/community/maybenews') { - $title = '[' . trim($community_link->plaintext) . '] ' . $title; - } - - $item = array(); - $item['categories'] = $categories; - $item['author'] = $post->find('.user__nick', 0)->innertext; - $item['title'] = $title; - $item['content'] = strip_tags( - backgroundToImg($post->find('.story__content-inner', 0)->innertext), - '<br><p><img><a><s> - '); - $item['uri'] = $title_element->href; - $item['timestamp'] = strtotime($time->getAttribute('datetime')); - $this->items[] = $item; - } - } + +class PikabuBridge extends BridgeAbstract +{ + const NAME = 'Пикабу'; + const URI = 'https://pikabu.ru'; + const DESCRIPTION = 'Выводит посты по тегу, сообществу или пользователю'; + const MAINTAINER = 'em92'; + + const PARAMETERS_FILTER = [ + 'name' => 'Фильтр', + 'type' => 'list', + 'values' => [ + 'Горячее' => 'hot', + 'Свежее' => 'new', + ], + 'defaultValue' => 'hot' + ]; + + const PARAMETERS = [ + 'По тегу' => [ + 'tag' => [ + 'name' => 'Тег', + 'exampleValue' => 'it', + 'required' => true + ], + 'filter' => self::PARAMETERS_FILTER + ], + 'По сообществу' => [ + 'community' => [ + 'name' => 'Сообщество', + 'exampleValue' => 'linux', + 'required' => true + ], + 'filter' => self::PARAMETERS_FILTER + ], + 'По пользователю' => [ + 'user' => [ + 'name' => 'Пользователь', + 'exampleValue' => 'admin', + 'required' => true + ] + ] + ]; + + protected $title = null; + + public function getURI() + { + if ($this->getInput('tag')) { + return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter')); + } elseif ($this->getInput('user')) { + return self::URI . '/@' . rawurlencode($this->getInput('user')); + } elseif ($this->getInput('community')) { + $uri = self::URI . '/community/' . rawurlencode($this->getInput('community')); + if ($this->getInput('filter') != 'hot') { + $uri .= '/' . rawurlencode($this->getInput('filter')); + } + return $uri; + } else { + return parent::getURI(); + } + } + + public function getIcon() + { + return 'https://cs.pikabu.ru/assets/favicon.ico'; + } + + public function getName() + { + if (is_null($this->title)) { + return parent::getName(); + } else { + return $this->title . ' - ' . parent::getName(); + } + } + + public function collectData() + { + $link = $this->getURI(); + + $text_html = getContents($link); + $text_html = iconv('windows-1251', 'utf-8', $text_html); + $html = str_get_html($text_html); + + $this->title = $html->find('title', 0)->innertext; + + foreach ($html->find('article.story') as $post) { + $time = $post->find('time.story__datetime', 0); + if (is_null($time)) { + continue; + } + + $el_to_remove_selectors = [ + '.story__read-more', + 'script', + 'svg.story-image__stretch', + ]; + + foreach ($el_to_remove_selectors as $el_to_remove_selector) { + foreach ($post->find($el_to_remove_selector) as $el) { + $el->outertext = ''; + } + } + + foreach ($post->find('[data-type=gifx]') as $el) { + $src = $el->getAttribute('data-source'); + $el->outertext = '<img src="' . $src . '">'; + } + + foreach ($post->find('img') as $img) { + $src = $img->getAttribute('src'); + if (!$src) { + $src = $img->getAttribute('data-src'); + if (!$src) { + continue; + } + } + $img->outertext = '<img src="' . $src . '">'; + + // it is assumed, that img's parents are links to post itself + // we don't need them + $img->parent()->outertext = $img->outertext; + } + + $categories = []; + foreach ($post->find('.tags__tag') as $tag) { + if ($tag->getAttribute('data-tag')) { + $categories[] = $tag->innertext; + } + } + + $title_element = $post->find('.story__title-link', 0); + + $title = $title_element->plaintext; + $community_link = $post->find('.story__community-link', 0); + // adding special marker for "Maybe News" section + // these posts are fake + if (!is_null($community_link) && $community_link->getAttribute('href') == '/community/maybenews') { + $title = '[' . trim($community_link->plaintext) . '] ' . $title; + } + + $item = []; + $item['categories'] = $categories; + $item['author'] = $post->find('.user__nick', 0)->innertext; + $item['title'] = $title; + $item['content'] = strip_tags( + backgroundToImg($post->find('.story__content-inner', 0)->innertext), + '<br><p><img><a><s> + ' + ); + $item['uri'] = $title_element->href; + $item['timestamp'] = strtotime($time->getAttribute('datetime')); + $this->items[] = $item; + } + } } diff --git a/bridges/PillowfortBridge.php b/bridges/PillowfortBridge.php index 527cc1c7..07cdbdd8 100644 --- a/bridges/PillowfortBridge.php +++ b/bridges/PillowfortBridge.php @@ -1,98 +1,109 @@ <?php -class PillowfortBridge extends BridgeAbstract { - const NAME = 'Pillowfort'; - const URI = 'https://www.pillowfort.social'; - const DESCRIPTION = 'Returns recent posts from a user'; - const MAINTAINER = 'KamaleiZestri'; - const PARAMETERS = array(array( - 'username' => array( - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'Staff' - ), - 'noava' => array( - 'name' => 'Hide avatar', - 'type' => 'checkbox', - 'title' => 'Check to hide user avatars.' - ), - 'noreblog' => array( - 'name' => 'Hide reblogs', - 'type' => 'checkbox', - 'title' => 'Check to only show original posts.' - ), - 'noretags' => array( - 'name' => 'Prefer original tags', - 'type' => 'checkbox', - 'title' => 'Check to use tags from original post(if available) instead of reblog\'s tags' - ), - 'image' => array( - 'name' => 'Select image type', - 'type' => 'list', - 'title' => 'Decides how the image is displayed, if at all.', - 'values' => array( - 'None' => 'None', - 'Small' => 'Small', - 'Full' => 'Full' - ), - 'defaultValue' => 'Full' - ) - )); - - /** - * The Pillowfort bridge. - * - * Pillowfort pages are dynamically generated from a json file - * which holds the last 20 or so posts from the given user. - * This bridge uses that json file and HTML/CSS similar - * to the Twitter bridge for formatting. - */ - public function collectData() { - $jsonSite = getContents($this->getJSONURI()); - - $jsonFile = json_decode($jsonSite, true); - $posts = $jsonFile['posts']; - - foreach($posts as $post) { - $item = $this->getItemFromPost($post); - - //empty when 'noreblogs' is checked and current post is a reblog. - if(!empty($item)) - $this->items[] = $item; - } - } - - public function getName() { - $name = $this -> getUsername(); - if($name != '') - return $name . ' - ' . self::NAME; - else - return parent::getName(); - } - - public function getURI() { - $name = $this -> getUsername(); - if($name != '') - return self::URI . '/' . $name; - else - return parent::getURI(); - } - - protected function getJSONURI() { - return $this -> getURI() . '/json/?p=1'; - } - - protected function getUsername() { - return $this -> getInput('username'); - } - - protected function genAvatarText($author, $avatar_url, $title) { - $noava = $this -> getInput('noava'); - - if($noava) - return ''; - else - return <<<EOD + +class PillowfortBridge extends BridgeAbstract +{ + const NAME = 'Pillowfort'; + const URI = 'https://www.pillowfort.social'; + const DESCRIPTION = 'Returns recent posts from a user'; + const MAINTAINER = 'KamaleiZestri'; + const PARAMETERS = [[ + 'username' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'Staff' + ], + 'noava' => [ + 'name' => 'Hide avatar', + 'type' => 'checkbox', + 'title' => 'Check to hide user avatars.' + ], + 'noreblog' => [ + 'name' => 'Hide reblogs', + 'type' => 'checkbox', + 'title' => 'Check to only show original posts.' + ], + 'noretags' => [ + 'name' => 'Prefer original tags', + 'type' => 'checkbox', + 'title' => 'Check to use tags from original post(if available) instead of reblog\'s tags' + ], + 'image' => [ + 'name' => 'Select image type', + 'type' => 'list', + 'title' => 'Decides how the image is displayed, if at all.', + 'values' => [ + 'None' => 'None', + 'Small' => 'Small', + 'Full' => 'Full' + ], + 'defaultValue' => 'Full' + ] + ]]; + + /** + * The Pillowfort bridge. + * + * Pillowfort pages are dynamically generated from a json file + * which holds the last 20 or so posts from the given user. + * This bridge uses that json file and HTML/CSS similar + * to the Twitter bridge for formatting. + */ + public function collectData() + { + $jsonSite = getContents($this->getJSONURI()); + + $jsonFile = json_decode($jsonSite, true); + $posts = $jsonFile['posts']; + + foreach ($posts as $post) { + $item = $this->getItemFromPost($post); + + //empty when 'noreblogs' is checked and current post is a reblog. + if (!empty($item)) { + $this->items[] = $item; + } + } + } + + public function getName() + { + $name = $this -> getUsername(); + if ($name != '') { + return $name . ' - ' . self::NAME; + } else { + return parent::getName(); + } + } + + public function getURI() + { + $name = $this -> getUsername(); + if ($name != '') { + return self::URI . '/' . $name; + } else { + return parent::getURI(); + } + } + + protected function getJSONURI() + { + return $this -> getURI() . '/json/?p=1'; + } + + protected function getUsername() + { + return $this -> getInput('username'); + } + + protected function genAvatarText($author, $avatar_url, $title) + { + $noava = $this -> getInput('noava'); + + if ($noava) { + return ''; + } else { + return <<<EOD <a href="{self::URI}/posts/{$author}"> <img style="align:top; width:75px; border:1px solid black;" @@ -101,30 +112,32 @@ class PillowfortBridge extends BridgeAbstract { title="{$title}" /> </a> EOD; - } + } + } - protected function genImagesText ($media) { - $dimensions = $this -> getInput('image'); - $text = ''; + protected function genImagesText($media) + { + $dimensions = $this -> getInput('image'); + $text = ''; - //preg_replace used for images with spaces in the url + //preg_replace used for images with spaces in the url - switch($dimensions) { - case 'None': - foreach($media as $image) { - $imageURL = preg_replace('[ ]', '%20', $image['url']); - $text .= <<<EOD + switch ($dimensions) { + case 'None': + foreach ($media as $image) { + $imageURL = preg_replace('[ ]', '%20', $image['url']); + $text .= <<<EOD <a href="{$imageURL}"> {$imageURL} </a> EOD; - } - break; + } + break; - case 'Small': - foreach($media as $image) { - $imageURL = preg_replace('[ ]', '%20', $image['small_image_url']); - $text .= <<<EOD + case 'Small': + foreach ($media as $image) { + $imageURL = preg_replace('[ ]', '%20', $image['small_image_url']); + $text .= <<<EOD <a href="{$imageURL}"> <img style="align:top; max-width:558px; border:1px solid black;" @@ -132,13 +145,13 @@ EOD; /> </a> EOD; - } - break; + } + break; - case 'Full': - foreach($media as $image) { - $imageURL = preg_replace('[ ]', '%20', $image['url']); - $text .= <<<EOD + case 'Full': + foreach ($media as $image) { + $imageURL = preg_replace('[ ]', '%20', $image['url']); + $text .= <<<EOD <a href="{$imageURL}"> <img style="align:top; max-width:558px; border:1px solid black;" @@ -146,66 +159,74 @@ EOD; /> </a> EOD; - } - break; - - default: - break; - } - - return $text; - } - - protected function getItemFromPost($post) { - //check if its a reblog. - if($post['original_post_id'] == null) - $embPost = false; - else - $embPost = true; - - if($this -> getInput('noreblog') && $embPost) - return array(); - - $item = array(); - - $item['uid'] = $post['id']; - $item['timestamp'] = strtotime($post['created_at']); - - if($embPost) { - $item['uri'] = self::URI . '/posts/' . $post['original_post']['id']; - $item['author'] = $post['original_username']; - if($post['original_post']['title'] != '') - $item['title'] = $post['original_post']['title']; - else - $item['title'] = '[NO TITLE]'; - } else { - $item['uri'] = self::URI . '/posts/' . $post['id']; - $item['author'] = $post['username']; - if($post['title'] != '') - $item['title'] = $post['title']; - else - $item['title'] = '[NO TITLE]'; - } - - /** - * 4 cases if it is a reblog. - * 1: reblog has tags, original has tags. defer to option. - * 2: reblog has tags, original has no tags. use reblog tags. - * 3: reblog has no tags, original has tags. use original tags. - * 4: reblog has no tags, original has no tags. use reblog tags not that it matters. - */ - $item['categories'] = $post['tags']; - if($embPost) { - if($this -> getInput('noretags') || ($post['tags'] == null )) - $item['categories'] = $post['original_post']['tag_list']; - } - - $avatarText = $this -> genAvatarText($item['author'], - $post['avatar_url'], - $item['title']); - $imagesText = $this -> genImagesText($post['media']); - - $item['content'] = <<<EOD + } + break; + + default: + break; + } + + return $text; + } + + protected function getItemFromPost($post) + { + //check if its a reblog. + if ($post['original_post_id'] == null) { + $embPost = false; + } else { + $embPost = true; + } + + if ($this -> getInput('noreblog') && $embPost) { + return []; + } + + $item = []; + + $item['uid'] = $post['id']; + $item['timestamp'] = strtotime($post['created_at']); + + if ($embPost) { + $item['uri'] = self::URI . '/posts/' . $post['original_post']['id']; + $item['author'] = $post['original_username']; + if ($post['original_post']['title'] != '') { + $item['title'] = $post['original_post']['title']; + } else { + $item['title'] = '[NO TITLE]'; + } + } else { + $item['uri'] = self::URI . '/posts/' . $post['id']; + $item['author'] = $post['username']; + if ($post['title'] != '') { + $item['title'] = $post['title']; + } else { + $item['title'] = '[NO TITLE]'; + } + } + + /** + * 4 cases if it is a reblog. + * 1: reblog has tags, original has tags. defer to option. + * 2: reblog has tags, original has no tags. use reblog tags. + * 3: reblog has no tags, original has tags. use original tags. + * 4: reblog has no tags, original has no tags. use reblog tags not that it matters. + */ + $item['categories'] = $post['tags']; + if ($embPost) { + if ($this -> getInput('noretags') || ($post['tags'] == null )) { + $item['categories'] = $post['original_post']['tag_list']; + } + } + + $avatarText = $this -> genAvatarText( + $item['author'], + $post['avatar_url'], + $item['title'] + ); + $imagesText = $this -> genImagesText($post['media']); + + $item['content'] = <<<EOD <div style="display: inline-block; vertical-align: top;"> {$avatarText} </div> @@ -217,6 +238,6 @@ EOD; </div> EOD; - return $item; - } + return $item; + } } diff --git a/bridges/PinterestBridge.php b/bridges/PinterestBridge.php index 1f8f86cd..fc5b1c19 100644 --- a/bridges/PinterestBridge.php +++ b/bridges/PinterestBridge.php @@ -1,63 +1,64 @@ <?php -class PinterestBridge extends FeedExpander { - - const MAINTAINER = 'pauder'; - const NAME = 'Pinterest Bridge'; - const URI = 'https://www.pinterest.com'; - const DESCRIPTION = 'Returns the newest images on a board'; - - const PARAMETERS = array( - 'By username and board' => array( - 'u' => array( - 'name' => 'username', - 'exampleValue' => 'VIGOIndustries', - 'required' => true - ), - 'b' => array( - 'name' => 'board', - 'exampleValue' => 'bathroom-remodels', - 'required' => true - ) - ) - ); - - public function getIcon() { - return 'https://s.pinimg.com/webapp/style/images/favicon-9f8f9adf.png'; - } - - public function collectData() { - $this->collectExpandableDatas($this->getURI() . '.rss'); - $this->fixLowRes(); - } - - private function fixLowRes() { - - $newitems = array(); - $pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//'; - foreach($this->items as $item) { - - $item['content'] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item['content']); - $newitems[] = $item; - } - $this->items = $newitems; - - } - - public function getURI() { - - if ($this->queriedContext === 'By username and board') { - return self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b')); - } - - return parent::getURI(); - } - - public function getName() { - - if ($this->queriedContext === 'By username and board') { - return $this->getInput('u') . ' - ' . $this->getInput('b') . ' - ' . self::NAME; - } - - return parent::getName(); - } + +class PinterestBridge extends FeedExpander +{ + const MAINTAINER = 'pauder'; + const NAME = 'Pinterest Bridge'; + const URI = 'https://www.pinterest.com'; + const DESCRIPTION = 'Returns the newest images on a board'; + + const PARAMETERS = [ + 'By username and board' => [ + 'u' => [ + 'name' => 'username', + 'exampleValue' => 'VIGOIndustries', + 'required' => true + ], + 'b' => [ + 'name' => 'board', + 'exampleValue' => 'bathroom-remodels', + 'required' => true + ] + ] + ]; + + public function getIcon() + { + return 'https://s.pinimg.com/webapp/style/images/favicon-9f8f9adf.png'; + } + + public function collectData() + { + $this->collectExpandableDatas($this->getURI() . '.rss'); + $this->fixLowRes(); + } + + private function fixLowRes() + { + $newitems = []; + $pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//'; + foreach ($this->items as $item) { + $item['content'] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item['content']); + $newitems[] = $item; + } + $this->items = $newitems; + } + + public function getURI() + { + if ($this->queriedContext === 'By username and board') { + return self::URI . '/' . urlencode($this->getInput('u')) . '/' . urlencode($this->getInput('b')); + } + + return parent::getURI(); + } + + public function getName() + { + if ($this->queriedContext === 'By username and board') { + return $this->getInput('u') . ' - ' . $this->getInput('b') . ' - ' . self::NAME; + } + + return parent::getName(); + } } diff --git a/bridges/PirateCommunityBridge.php b/bridges/PirateCommunityBridge.php index ce861015..a1a9d8f5 100644 --- a/bridges/PirateCommunityBridge.php +++ b/bridges/PirateCommunityBridge.php @@ -1,88 +1,102 @@ <?php -class PirateCommunityBridge extends BridgeAbstract { - const NAME = 'Pirate-Community Bridge'; - const URI = 'https://raymanpc.com/'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Returns replies to topics'; - const MAINTAINER = 'Roliga'; - const PARAMETERS = array( array( - 't' => array( - 'name' => 'Topic ID', - 'type' => 'number', - 'exampleValue' => '12651', - 'title' => 'Topic ID from topic URL. If the URL contains t=12 the ID is 12.', - 'required' => true - ))); - - private $feedName = ''; - - public function detectParameters($url){ - $parsed_url = parse_url($url); - - if($parsed_url['host'] !== 'raymanpc.com') - return null; - - parse_str($parsed_url['query'], $parsed_query); - - if($parsed_url['path'] === '/forum/viewtopic.php' - && array_key_exists('t', $parsed_query)) { - return array('t' => $parsed_query['t']); - } - - return null; - } - - public function getName() { - if(!empty($this->feedName)) - return $this->feedName; - - return parent::getName(); - } - - public function getURI(){ - if(!is_null($this->getInput('t'))) { - return self::URI - . 'forum/viewtopic.php?t=' - . $this->getInput('t') - . '&sd=d'; // sort posts decending by ate so first page has latest posts - } - - return parent::getURI(); - } - - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); - - $this->feedName = $html->find('head title', 0)->plaintext; - - foreach($html->find('.post') as $reply) { - $item = array(); - - $item['uri'] = $this->getURI() - . $reply->find('h3 a', 0)->getAttribute('href'); - - $item['title'] = $reply->find('h3 a', 0)->plaintext; - - $author_html = $reply->find('.author', 0); - // author_html contains the timestamp as text directly inside it, - // so delete all other child elements - foreach($author_html->children as $child) - $child->outertext = ''; - // Timestamps are always in UTC+1 - $item['timestamp'] = trim($author_html->innertext) . ' +01:00'; - - $item['author'] = $reply - ->find('.username, .username-coloured', 0) - ->plaintext; - - $item['content'] = defaultLinkTo($reply->find('.content', 0)->innertext, - $this->getURI()); - - $item['enclosures'] = array(); - foreach($reply->find('.attachbox img.postimage') as $img) - $item['enclosures'][] = urljoin($this->getURI(), $img->src); - - $this->items[] = $item; - } - } + +class PirateCommunityBridge extends BridgeAbstract +{ + const NAME = 'Pirate-Community Bridge'; + const URI = 'https://raymanpc.com/'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Returns replies to topics'; + const MAINTAINER = 'Roliga'; + const PARAMETERS = [ [ + 't' => [ + 'name' => 'Topic ID', + 'type' => 'number', + 'exampleValue' => '12651', + 'title' => 'Topic ID from topic URL. If the URL contains t=12 the ID is 12.', + 'required' => true + ]]]; + + private $feedName = ''; + + public function detectParameters($url) + { + $parsed_url = parse_url($url); + + if ($parsed_url['host'] !== 'raymanpc.com') { + return null; + } + + parse_str($parsed_url['query'], $parsed_query); + + if ( + $parsed_url['path'] === '/forum/viewtopic.php' + && array_key_exists('t', $parsed_query) + ) { + return ['t' => $parsed_query['t']]; + } + + return null; + } + + public function getName() + { + if (!empty($this->feedName)) { + return $this->feedName; + } + + return parent::getName(); + } + + public function getURI() + { + if (!is_null($this->getInput('t'))) { + return self::URI + . 'forum/viewtopic.php?t=' + . $this->getInput('t') + . '&sd=d'; // sort posts decending by ate so first page has latest posts + } + + return parent::getURI(); + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $this->feedName = $html->find('head title', 0)->plaintext; + + foreach ($html->find('.post') as $reply) { + $item = []; + + $item['uri'] = $this->getURI() + . $reply->find('h3 a', 0)->getAttribute('href'); + + $item['title'] = $reply->find('h3 a', 0)->plaintext; + + $author_html = $reply->find('.author', 0); + // author_html contains the timestamp as text directly inside it, + // so delete all other child elements + foreach ($author_html->children as $child) { + $child->outertext = ''; + } + // Timestamps are always in UTC+1 + $item['timestamp'] = trim($author_html->innertext) . ' +01:00'; + + $item['author'] = $reply + ->find('.username, .username-coloured', 0) + ->plaintext; + + $item['content'] = defaultLinkTo( + $reply->find('.content', 0)->innertext, + $this->getURI() + ); + + $item['enclosures'] = []; + foreach ($reply->find('.attachbox img.postimage') as $img) { + $item['enclosures'][] = urljoin($this->getURI(), $img->src); + } + + $this->items[] = $item; + } + } } diff --git a/bridges/PixivBridge.php b/bridges/PixivBridge.php index 6a3fd573..55e00984 100644 --- a/bridges/PixivBridge.php +++ b/bridges/PixivBridge.php @@ -1,215 +1,229 @@ <?php -class PixivBridge extends BridgeAbstract { - - // Good resource on API return values (Ex: illustType): - // https://hackage.haskell.org/package/pixiv-0.1.0/docs/Web-Pixiv-Types.html - const NAME = 'Pixiv Bridge'; - const URI = 'https://www.pixiv.net/'; - const DESCRIPTION = 'Returns the tag search from pixiv.net'; - - - const PARAMETERS = array( - 'global' => array( - 'posts' => array( - 'name' => 'Post Limit', - 'type' => 'number', - 'defaultValue' => '10' - ), - 'fullsize' => array( - 'name' => 'Full-size Image', - 'type' => 'checkbox' - ), - 'mode' => array( - 'name' => 'Post Type', - 'type' => 'list', - 'values' => array('All Works' => 'all', - 'Illustrations' => 'illustrations/', - 'Manga' => 'manga/', - 'Novels' => 'novels/') - ), - ), - 'Tag' => array( - 'tag' => array( - 'name' => 'Query to search', - 'exampleValue' => 'オリジナル', - 'required' => true - ) - ), - 'User' => array( - 'userid' => array( - 'name' => 'User ID from profile URL', - 'exampleValue' => '11', - 'required' => true - ) - ) - ); - - // maps from URLs to json keys by context - const JSON_KEY_MAP = array( - 'Tag' => array( - 'illustrations/' => 'illust', - 'manga/' => 'manga', - 'novels/' => 'novel' - ), - 'User' => array( - 'illustrations/' => 'illusts', - 'manga/' => 'manga', - 'novels/' => 'novels' - ) - ); - - // Hold the username for getName() - private $username = null; - - public function getName() { - switch($this->queriedContext) { - case 'Tag': - $context = 'Tag'; - $query = $this->getInput('tag'); - break; - case 'User': - $context = 'User'; - $query = $this->username ?? $this->getInput('userid'); - break; - default: - return parent::getName(); - } - $mode = array_search($this->getInput('mode'), - self::PARAMETERS['global']['mode']['values']); - return "Pixiv ${mode} from ${context} ${query}"; - } - - public function getURI() { - switch($this->queriedContext) { - case 'Tag': - $uri = static::URI . 'tags/' . urlencode($this->getInput('tag') ?? ''); - break; - case 'User': - $uri = static::URI . 'users/' . $this->getInput('userid'); - break; - default: - return parent::getURI(); - } - if ($this->getInput('mode') != 'all') { - $uri = $uri . '/' . $this->getInput('mode'); - } - return $uri; - } - - private function getSearchURI($mode) { - switch($this->queriedContext) { - case 'Tag': - $query = urlencode($this->getInput('tag')); - $uri = static::URI . 'ajax/search/top/' . $query; - break; - case 'User': - $uri = static::URI . 'ajax/user/' . $this->getInput('userid') - . '/profile/top'; - break; - default: - returnClientError('Invalid Context'); - } - return $uri; - } - - private function getDataFromJSON($json, $json_key) { - $json = $json['body'][$json_key]; - // Tags context contains subkey - if ($this->queriedContext == 'Tag') { - $json = $json['data']; - } - return $json; - } - - private function collectWorksArray() { - $content = getContents($this->getSearchURI($this->getInput('mode'))); - $content = json_decode($content, true); - if ($this->getInput('mode') == 'all') { - $total = array(); - foreach(self::JSON_KEY_MAP[$this->queriedContext] as $mode => $json_key) { - $current = $this->getDataFromJSON($content, $json_key); - $total = array_merge($total, $current); - } - $content = $total; - } else { - $json_key = self::JSON_KEY_MAP[$this->queriedContext][$this->getInput('mode')]; - $content = $this->getDataFromJSON($content, $json_key); - } - return $content; - } - - public function collectData() { - $content = $this->collectWorksArray(); - - $content = array_filter($content, function($v, $k) { - return !array_key_exists('isAdContainer', $v); - }, ARRAY_FILTER_USE_BOTH); - // Sort by updateDate to get newest works - usort($content, function($a, $b) { - return $b['updateDate'] <=> $a['updateDate']; - }); - $content = array_slice($content, 0, $this->getInput('posts')); - - foreach($content as $result) { - // Store username for getName() - if (!$this->username) - $this->username = $result['userName']; - - $item = array(); - $item['uid'] = $result['id']; - $subpath = array_key_exists('illustType', $result) ? 'artworks/' : 'novel/show.php?id='; - $item['uri'] = static::URI . $subpath . $result['id']; - $item['title'] = $result['title']; - $item['author'] = $result['userName']; - $item['timestamp'] = $result['updateDate']; - $item['categories'] = $result['tags']; - $cached_image = $this->cacheImage($result['url'], $result['id'], - array_key_exists('illustType', $result)); - $item['content'] = "<img src='" . $cached_image . "' />"; - - // Additional content items - if (array_key_exists('pageCount', $result)) { - $item['content'] .= '<br>Page Count: ' . $result['pageCount']; - } else { - $item['content'] .= '<br>Word Count: ' . $result['wordCount']; - } - - $this->items[] = $item; - } - } - - private function cacheImage($url, $illustId, $isImage) { - $illustId = preg_replace('/[^0-9]/', '', $illustId); - $thumbnailurl = $url; - - $path = PATH_CACHE . 'pixiv_img/'; - if(!is_dir($path)) - mkdir($path, 0755, true); - - $path .= $illustId; - if ($this->getInput('fullsize')) { - $path .= '_fullsize'; - } - $path .= '.jpg'; - - if(!is_file($path)) { - - // Get fullsize URL - if ($isImage && $this->getInput('fullsize')) { - $ajax_uri = static::URI . 'ajax/illust/' . $illustId; - $imagejson = json_decode(getContents($ajax_uri), true); - $url = $imagejson['body']['urls']['original']; - } - - $headers = array('Referer: ' . static::URI); - try { - $illust = getContents($url, $headers); - } catch (Exception $e) { - $illust = getContents($thumbnailurl, $headers); // Original thumbnail - } - file_put_contents($path, $illust); - } - - return 'cache/pixiv_img/' . preg_replace('/.*\//', '', $path); - } + +class PixivBridge extends BridgeAbstract +{ + // Good resource on API return values (Ex: illustType): + // https://hackage.haskell.org/package/pixiv-0.1.0/docs/Web-Pixiv-Types.html + const NAME = 'Pixiv Bridge'; + const URI = 'https://www.pixiv.net/'; + const DESCRIPTION = 'Returns the tag search from pixiv.net'; + + + const PARAMETERS = [ + 'global' => [ + 'posts' => [ + 'name' => 'Post Limit', + 'type' => 'number', + 'defaultValue' => '10' + ], + 'fullsize' => [ + 'name' => 'Full-size Image', + 'type' => 'checkbox' + ], + 'mode' => [ + 'name' => 'Post Type', + 'type' => 'list', + 'values' => ['All Works' => 'all', + 'Illustrations' => 'illustrations/', + 'Manga' => 'manga/', + 'Novels' => 'novels/'] + ], + ], + 'Tag' => [ + 'tag' => [ + 'name' => 'Query to search', + 'exampleValue' => 'オリジナル', + 'required' => true + ] + ], + 'User' => [ + 'userid' => [ + 'name' => 'User ID from profile URL', + 'exampleValue' => '11', + 'required' => true + ] + ] + ]; + + // maps from URLs to json keys by context + const JSON_KEY_MAP = [ + 'Tag' => [ + 'illustrations/' => 'illust', + 'manga/' => 'manga', + 'novels/' => 'novel' + ], + 'User' => [ + 'illustrations/' => 'illusts', + 'manga/' => 'manga', + 'novels/' => 'novels' + ] + ]; + + // Hold the username for getName() + private $username = null; + + public function getName() + { + switch ($this->queriedContext) { + case 'Tag': + $context = 'Tag'; + $query = $this->getInput('tag'); + break; + case 'User': + $context = 'User'; + $query = $this->username ?? $this->getInput('userid'); + break; + default: + return parent::getName(); + } + $mode = array_search( + $this->getInput('mode'), + self::PARAMETERS['global']['mode']['values'] + ); + return "Pixiv ${mode} from ${context} ${query}"; + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'Tag': + $uri = static::URI . 'tags/' . urlencode($this->getInput('tag') ?? ''); + break; + case 'User': + $uri = static::URI . 'users/' . $this->getInput('userid'); + break; + default: + return parent::getURI(); + } + if ($this->getInput('mode') != 'all') { + $uri = $uri . '/' . $this->getInput('mode'); + } + return $uri; + } + + private function getSearchURI($mode) + { + switch ($this->queriedContext) { + case 'Tag': + $query = urlencode($this->getInput('tag')); + $uri = static::URI . 'ajax/search/top/' . $query; + break; + case 'User': + $uri = static::URI . 'ajax/user/' . $this->getInput('userid') + . '/profile/top'; + break; + default: + returnClientError('Invalid Context'); + } + return $uri; + } + + private function getDataFromJSON($json, $json_key) + { + $json = $json['body'][$json_key]; + // Tags context contains subkey + if ($this->queriedContext == 'Tag') { + $json = $json['data']; + } + return $json; + } + + private function collectWorksArray() + { + $content = getContents($this->getSearchURI($this->getInput('mode'))); + $content = json_decode($content, true); + if ($this->getInput('mode') == 'all') { + $total = []; + foreach (self::JSON_KEY_MAP[$this->queriedContext] as $mode => $json_key) { + $current = $this->getDataFromJSON($content, $json_key); + $total = array_merge($total, $current); + } + $content = $total; + } else { + $json_key = self::JSON_KEY_MAP[$this->queriedContext][$this->getInput('mode')]; + $content = $this->getDataFromJSON($content, $json_key); + } + return $content; + } + + public function collectData() + { + $content = $this->collectWorksArray(); + + $content = array_filter($content, function ($v, $k) { + return !array_key_exists('isAdContainer', $v); + }, ARRAY_FILTER_USE_BOTH); + // Sort by updateDate to get newest works + usort($content, function ($a, $b) { + return $b['updateDate'] <=> $a['updateDate']; + }); + $content = array_slice($content, 0, $this->getInput('posts')); + + foreach ($content as $result) { + // Store username for getName() + if (!$this->username) { + $this->username = $result['userName']; + } + + $item = []; + $item['uid'] = $result['id']; + $subpath = array_key_exists('illustType', $result) ? 'artworks/' : 'novel/show.php?id='; + $item['uri'] = static::URI . $subpath . $result['id']; + $item['title'] = $result['title']; + $item['author'] = $result['userName']; + $item['timestamp'] = $result['updateDate']; + $item['categories'] = $result['tags']; + $cached_image = $this->cacheImage( + $result['url'], + $result['id'], + array_key_exists('illustType', $result) + ); + $item['content'] = "<img src='" . $cached_image . "' />"; + + // Additional content items + if (array_key_exists('pageCount', $result)) { + $item['content'] .= '<br>Page Count: ' . $result['pageCount']; + } else { + $item['content'] .= '<br>Word Count: ' . $result['wordCount']; + } + + $this->items[] = $item; + } + } + + private function cacheImage($url, $illustId, $isImage) + { + $illustId = preg_replace('/[^0-9]/', '', $illustId); + $thumbnailurl = $url; + + $path = PATH_CACHE . 'pixiv_img/'; + if (!is_dir($path)) { + mkdir($path, 0755, true); + } + + $path .= $illustId; + if ($this->getInput('fullsize')) { + $path .= '_fullsize'; + } + $path .= '.jpg'; + + if (!is_file($path)) { + // Get fullsize URL + if ($isImage && $this->getInput('fullsize')) { + $ajax_uri = static::URI . 'ajax/illust/' . $illustId; + $imagejson = json_decode(getContents($ajax_uri), true); + $url = $imagejson['body']['urls']['original']; + } + + $headers = ['Referer: ' . static::URI]; + try { + $illust = getContents($url, $headers); + } catch (Exception $e) { + $illust = getContents($thumbnailurl, $headers); // Original thumbnail + } + file_put_contents($path, $illust); + } + + return 'cache/pixiv_img/' . preg_replace('/.*\//', '', $path); + } } diff --git a/bridges/PlantUMLReleasesBridge.php b/bridges/PlantUMLReleasesBridge.php index fbf9211b..bc1cca20 100644 --- a/bridges/PlantUMLReleasesBridge.php +++ b/bridges/PlantUMLReleasesBridge.php @@ -5,42 +5,45 @@ * @author nicolas-delsaux * */ -class PlantUMLReleasesBridge extends BridgeAbstract { - const MAINTAINER = 'Riduidel'; - const NAME = 'PlantUML Releases'; - const AUTHOR = 'PlantUML team'; - const URI = 'https://plantuml.com/changes'; +class PlantUMLReleasesBridge extends BridgeAbstract +{ + const MAINTAINER = 'Riduidel'; + const NAME = 'PlantUML Releases'; + const AUTHOR = 'PlantUML team'; + const URI = 'https://plantuml.com/changes'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'PlantUML releases bridge, showing for each release the changelog'; - const ITEM_LIMIT = 10; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'PlantUML releases bridge, showing for each release the changelog'; + const ITEM_LIMIT = 10; - public function getURI() { - return self::URI; - } + public function getURI() + { + return self::URI; + } - public function collectData() { - $html = defaultLinkTo(getSimpleHTMLDOM($this->getURI()), self::URI); + public function collectData() + { + $html = defaultLinkTo(getSimpleHTMLDOM($this->getURI()), self::URI); - $num_items = 0; - $main = $html->find('div[id=root]', 0); - foreach ($main->find('h2') as $release) { - // Limit to $ITEM_LIMIT number of results - if ($num_items++ >= self::ITEM_LIMIT) { - break; - } - $item = array(); - $item['author'] = self::AUTHOR; - $release_text = $release->innertext; - if (preg_match('/(.+) \((.*)\)/', $release_text, $matches)) { - $item['title'] = $matches[1]; - $item['timestamp'] = preg_replace('/(\d+) (\w{3})\w*, (\d+)/', '${1} ${2} ${3}', $matches[2]); - } else { - $item['title'] = $release_text; - } - $item['uri'] = $this->getURI(); - $item['content'] = $release->next_sibling(); - $this->items[] = $item; - } - } + $num_items = 0; + $main = $html->find('div[id=root]', 0); + foreach ($main->find('h2') as $release) { + // Limit to $ITEM_LIMIT number of results + if ($num_items++ >= self::ITEM_LIMIT) { + break; + } + $item = []; + $item['author'] = self::AUTHOR; + $release_text = $release->innertext; + if (preg_match('/(.+) \((.*)\)/', $release_text, $matches)) { + $item['title'] = $matches[1]; + $item['timestamp'] = preg_replace('/(\d+) (\w{3})\w*, (\d+)/', '${1} ${2} ${3}', $matches[2]); + } else { + $item['title'] = $release_text; + } + $item['uri'] = $this->getURI(); + $item['content'] = $release->next_sibling(); + $this->items[] = $item; + } + } } diff --git a/bridges/PokemonTVBridge.php b/bridges/PokemonTVBridge.php index 4a34e58e..2c6b8bdc 100644 --- a/bridges/PokemonTVBridge.php +++ b/bridges/PokemonTVBridge.php @@ -1,143 +1,147 @@ <?php -class PokemonTVBridge extends BridgeAbstract { - const NAME = 'PokemonTV Bridge'; - const URI = 'https://www.pokemon.com/'; - const DESCRIPTION = 'Returns latest episodes from PokemonTV'; - const MAINTAINER = 'Bockiii'; - const CACHE_TIMEOUT = 3600; - const PARAMETERS = array( array( - 'language' => array( - 'name' => 'Language', - 'type' => 'list', - 'title' => 'Select your language', - 'values' => array( - 'Danish' => 'dk', - 'Dutch' => 'nl', - 'English (UK)' => 'uk', - 'English (US)' => 'us', - 'Finish' => 'fi', - 'French' => 'fr', - 'German' => 'de', - 'Italian' => 'it', - 'Latin America' => 'el', - 'Norwegian' => 'no', - 'Portoguese' => 'br', - 'Russian' => 'ru', - 'Spanish' => 'es', - 'Swedish' => 'se' - ), - 'defaultValue' => 'English (US)' - ), - 'filtername' => array( - 'name' => 'Series Name Filter', - 'exampleValue' => 'Ultra', - 'required' => false - ), - 'filterseason' => array( - 'name' => 'Series Season Filter', - 'exampleValue' => '22', - 'required' => false - ) - )); +class PokemonTVBridge extends BridgeAbstract +{ + const NAME = 'PokemonTV Bridge'; + const URI = 'https://www.pokemon.com/'; + const DESCRIPTION = 'Returns latest episodes from PokemonTV'; + const MAINTAINER = 'Bockiii'; + const CACHE_TIMEOUT = 3600; + const PARAMETERS = [ [ + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'title' => 'Select your language', + 'values' => [ + 'Danish' => 'dk', + 'Dutch' => 'nl', + 'English (UK)' => 'uk', + 'English (US)' => 'us', + 'Finish' => 'fi', + 'French' => 'fr', + 'German' => 'de', + 'Italian' => 'it', + 'Latin America' => 'el', + 'Norwegian' => 'no', + 'Portoguese' => 'br', + 'Russian' => 'ru', + 'Spanish' => 'es', + 'Swedish' => 'se' + ], + 'defaultValue' => 'English (US)' + ], + 'filtername' => [ + 'name' => 'Series Name Filter', + 'exampleValue' => 'Ultra', + 'required' => false + ], + 'filterseason' => [ + 'name' => 'Series Season Filter', + 'exampleValue' => '22', + 'required' => false + ] + ]]; - public function collectData(){ - $link = 'https://www.pokemon.com/api/pokemontv/v2/channels/' . $this->getInput('language'); + public function collectData() + { + $link = 'https://www.pokemon.com/api/pokemontv/v2/channels/' . $this->getInput('language'); - $html = getSimpleHTMLDOM($link); - $parsed_json = json_decode($html); + $html = getSimpleHTMLDOM($link); + $parsed_json = json_decode($html); - $filtername = $this->getInput('filtername'); - $filterseason = $this->getInput('filterseason'); + $filtername = $this->getInput('filtername'); + $filterseason = $this->getInput('filterseason'); - foreach($parsed_json as $element) { - if(strlen($filtername) >= 1) { - if (!(stristr($element->{'channel_name'}, $filtername) !== false)) { - continue; - } - } - foreach($element->{'media'} as $mediaelement) { - if(strlen($filterseason) >= 1) { - if ($mediaelement->{'season'} != $filterseason) { - continue; - } - } - switch($element->{'media_type'}) { - case 'movie': - $itemtitle = $element->{'channel_name'}; - break; - case 'episode': - $season = str_pad($mediaelement->{'season'}, 2, '0', STR_PAD_LEFT); - $episode = str_pad($mediaelement->{'episode'}, 2, '0', STR_PAD_LEFT); - $itemtitle = $element->{'channel_name'} . ' - S' . $season . 'E' . $episode; - break; - } - $streamurl = 'https://watch.pokemon.com/' . $this->getCountryCode() . '/#/player?id=' . $mediaelement->{'id'}; - $item = array(); - $item['uri'] = $streamurl; - $item['title'] = $itemtitle; - $item['timestamp'] = $mediaelement->{'last_modified'}; - $item['content'] = '<h1>' . $itemtitle . ' ' . $mediaelement->{'title'} - . '</h1><br><br><a href="' - . $streamurl - . '"><img src="' - . $mediaelement->{'images'}->{'medium'} - . '" /></a><br><br>' - . $mediaelement->{'description'} - . '<br><br><a href="' . $mediaelement->{'offline_url'} . '">Download</a>'; - $this->items[] = $item; - } - } - } + foreach ($parsed_json as $element) { + if (strlen($filtername) >= 1) { + if (!(stristr($element->{'channel_name'}, $filtername) !== false)) { + continue; + } + } + foreach ($element->{'media'} as $mediaelement) { + if (strlen($filterseason) >= 1) { + if ($mediaelement->{'season'} != $filterseason) { + continue; + } + } + switch ($element->{'media_type'}) { + case 'movie': + $itemtitle = $element->{'channel_name'}; + break; + case 'episode': + $season = str_pad($mediaelement->{'season'}, 2, '0', STR_PAD_LEFT); + $episode = str_pad($mediaelement->{'episode'}, 2, '0', STR_PAD_LEFT); + $itemtitle = $element->{'channel_name'} . ' - S' . $season . 'E' . $episode; + break; + } + $streamurl = 'https://watch.pokemon.com/' . $this->getCountryCode() . '/#/player?id=' . $mediaelement->{'id'}; + $item = []; + $item['uri'] = $streamurl; + $item['title'] = $itemtitle; + $item['timestamp'] = $mediaelement->{'last_modified'}; + $item['content'] = '<h1>' . $itemtitle . ' ' . $mediaelement->{'title'} + . '</h1><br><br><a href="' + . $streamurl + . '"><img src="' + . $mediaelement->{'images'}->{'medium'} + . '" /></a><br><br>' + . $mediaelement->{'description'} + . '<br><br><a href="' . $mediaelement->{'offline_url'} . '">Download</a>'; + $this->items[] = $item; + } + } + } - private function getCountryCode() { - switch($this->getInput('language')) { - case 'us': - return 'en-us'; - break; - case 'de': - return 'de-de'; - break; - case 'fr': - return 'fr-fr'; - break; - case 'es': - return 'es-es'; - break; - case 'el': - return 'es-xl'; - break; - case 'it': - return 'it-it'; - break; - case 'dk': - return 'da-dk'; - break; - case 'fi': - return 'fi-fi'; - break; - case 'br': - return 'pt-br'; - break; - case 'uk': - return 'en-gb'; - break; - case 'ru': - return 'ru-ru'; - break; - case 'nl': - return 'nl-nl'; - break; - case 'no': - return 'nb-no'; - break; - case 'se': - return 'sv-se'; - break; - } - } + private function getCountryCode() + { + switch ($this->getInput('language')) { + case 'us': + return 'en-us'; + break; + case 'de': + return 'de-de'; + break; + case 'fr': + return 'fr-fr'; + break; + case 'es': + return 'es-es'; + break; + case 'el': + return 'es-xl'; + break; + case 'it': + return 'it-it'; + break; + case 'dk': + return 'da-dk'; + break; + case 'fi': + return 'fi-fi'; + break; + case 'br': + return 'pt-br'; + break; + case 'uk': + return 'en-gb'; + break; + case 'ru': + return 'ru-ru'; + break; + case 'nl': + return 'nl-nl'; + break; + case 'no': + return 'nb-no'; + break; + case 'se': + return 'sv-se'; + break; + } + } - public function getIcon() { - return 'https://assets.pokemon.com/static2/_ui/img/favicon.ico'; - } + public function getIcon() + { + return 'https://assets.pokemon.com/static2/_ui/img/favicon.ico'; + } } diff --git a/bridges/PornhubBridge.php b/bridges/PornhubBridge.php index 40ad3bdd..c15db064 100644 --- a/bridges/PornhubBridge.php +++ b/bridges/PornhubBridge.php @@ -1,99 +1,102 @@ <?php -class PornhubBridge extends BridgeAbstract { - - const MAINTAINER = 'Mitsukarenai'; - const NAME = 'Pornhub'; - const URI = 'https://www.pornhub.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns videos from specified user,model,pornstar'; - - const PARAMETERS = array(array( - 'q' => array( - 'name' => 'User name', - 'exampleValue' => 'asa-akira', - 'required' => true, - ), - 'type' => array( - 'name' => 'User type', - 'type' => 'list', - 'values' => array( - 'user' => 'users', - 'model' => 'model', - 'pornstar' => 'pornstar', - ), - 'defaultValue' => 'pornstar', - ), - 'sort' => array( - 'name' => 'Sort by', - 'type' => 'list', - 'values' => array( - 'Most recent' => '?', - 'Most views' => '?o=mv', - 'Top rated' => '?o=tr', - 'Longest' => '?o=lg', - ), - 'defaultValue' => '?', - ), - 'show_images' => array( - 'name' => 'Show thumbnails', - 'type' => 'checkbox', - ), - )); - - public function getName(){ - if(!is_null($this->getInput('type')) && !is_null($this->getInput('q'))) { - return 'PornHub ' . $this->getInput('type') . ':' . $this->getInput('q'); - } - - return parent::getName(); - } - - public function collectData() { - - $uri = 'https://www.pornhub.com/' . $this->getInput('type') . '/'; - switch($this->getInput('type')) { // select proper permalink format per user type... - case 'model': - $uri .= urlencode($this->getInput('q')) . '/videos' . $this->getInput('sort'); break; - case 'users': - $uri .= urlencode($this->getInput('q')) . '/videos/public' . $this->getInput('sort'); break; - case 'pornstar': - $uri .= urlencode($this->getInput('q')) . '/videos/upload' . $this->getInput('sort'); break; - } - - $show_images = $this->getInput('show_images'); - - $html = getSimpleHTMLDOM($uri); - - foreach($html->find('div.videoUList ul.videos li.videoblock') as $element) { - - $item = array(); - - $item['author'] = $this->getInput('q'); - - // Title - $title = $element->find('a', 0)->getAttribute('title'); - if (is_null($title)) { - continue; - } - $item['title'] = $title; - - // Url - $url = $element->find('a', 0)->href; - $item['uri'] = 'https://www.pornhub.com' . $url; - - // Content - $image = $element->find('img', 0)->getAttribute('data-src'); - if($show_images === true) { - $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $image . '"></a>'; - } - - // date hack, guess upload YYYYMMDD from thumbnail URL (format: https://ci.phncdn.com/videos/201907/25/--- ) - $uploaded = explode('/', $image); - $uploaded = strtotime($uploaded[4] . $uploaded[5]); - $item['timestamp'] = $uploaded; - - $this->items[] = $item; - } - } +class PornhubBridge extends BridgeAbstract +{ + const MAINTAINER = 'Mitsukarenai'; + const NAME = 'Pornhub'; + const URI = 'https://www.pornhub.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns videos from specified user,model,pornstar'; + + const PARAMETERS = [[ + 'q' => [ + 'name' => 'User name', + 'exampleValue' => 'asa-akira', + 'required' => true, + ], + 'type' => [ + 'name' => 'User type', + 'type' => 'list', + 'values' => [ + 'user' => 'users', + 'model' => 'model', + 'pornstar' => 'pornstar', + ], + 'defaultValue' => 'pornstar', + ], + 'sort' => [ + 'name' => 'Sort by', + 'type' => 'list', + 'values' => [ + 'Most recent' => '?', + 'Most views' => '?o=mv', + 'Top rated' => '?o=tr', + 'Longest' => '?o=lg', + ], + 'defaultValue' => '?', + ], + 'show_images' => [ + 'name' => 'Show thumbnails', + 'type' => 'checkbox', + ], + ]]; + + public function getName() + { + if (!is_null($this->getInput('type')) && !is_null($this->getInput('q'))) { + return 'PornHub ' . $this->getInput('type') . ':' . $this->getInput('q'); + } + + return parent::getName(); + } + + public function collectData() + { + $uri = 'https://www.pornhub.com/' . $this->getInput('type') . '/'; + switch ($this->getInput('type')) { // select proper permalink format per user type... + case 'model': + $uri .= urlencode($this->getInput('q')) . '/videos' . $this->getInput('sort'); + break; + case 'users': + $uri .= urlencode($this->getInput('q')) . '/videos/public' . $this->getInput('sort'); + break; + case 'pornstar': + $uri .= urlencode($this->getInput('q')) . '/videos/upload' . $this->getInput('sort'); + break; + } + + $show_images = $this->getInput('show_images'); + + $html = getSimpleHTMLDOM($uri); + + foreach ($html->find('div.videoUList ul.videos li.videoblock') as $element) { + $item = []; + + $item['author'] = $this->getInput('q'); + + // Title + $title = $element->find('a', 0)->getAttribute('title'); + if (is_null($title)) { + continue; + } + $item['title'] = $title; + + // Url + $url = $element->find('a', 0)->href; + $item['uri'] = 'https://www.pornhub.com' . $url; + + // Content + $image = $element->find('img', 0)->getAttribute('data-src'); + if ($show_images === true) { + $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $image . '"></a>'; + } + + // date hack, guess upload YYYYMMDD from thumbnail URL (format: https://ci.phncdn.com/videos/201907/25/--- ) + $uploaded = explode('/', $image); + $uploaded = strtotime($uploaded[4] . $uploaded[5]); + $item['timestamp'] = $uploaded; + + $this->items[] = $item; + } + } } diff --git a/bridges/PresidenciaPTBridge.php b/bridges/PresidenciaPTBridge.php index e7b016ea..a0baa57f 100644 --- a/bridges/PresidenciaPTBridge.php +++ b/bridges/PresidenciaPTBridge.php @@ -1,77 +1,86 @@ <?php -class PresidenciaPTBridge extends BridgeAbstract { - const NAME = 'Presidência da República Portuguesa'; - const URI = 'https://www.presidencia.pt'; - const DESCRIPTION = 'Presidência da República Portuguesa'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - 'Section' => array( - '/atualidade/noticias' => array( - 'name' => 'Notícias', - 'type' => 'checkbox', - 'defaultValue' => 'checked', - ), - '/atualidade/mensagens' => array( - 'name' => 'Mensagens', - 'type' => 'checkbox', - 'defaultValue' => 'checked', - ), - '/atualidade/atividade-legislativa' => array( - 'name' => 'Atividade Legislativa', - 'type' => 'checkbox', - 'defaultValue' => 'checked', - ), - '/atualidade/notas-informativas' => array( - 'name' => 'Notas Informativas', - 'type' => 'checkbox', - 'defaultValue' => 'checked', - ) - ) - ); - const PT_MONTH_NAMES = array( - 'janeiro', - 'fevereiro', - 'março', - 'abril', - 'maio', - 'junho', - 'julho', - 'agosto', - 'setembro', - 'outubro', - 'novembro', - 'dezembro'); +class PresidenciaPTBridge extends BridgeAbstract +{ + const NAME = 'Presidência da República Portuguesa'; + const URI = 'https://www.presidencia.pt'; + const DESCRIPTION = 'Presidência da República Portuguesa'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + 'Section' => [ + '/atualidade/noticias' => [ + 'name' => 'Notícias', + 'type' => 'checkbox', + 'defaultValue' => 'checked', + ], + '/atualidade/mensagens' => [ + 'name' => 'Mensagens', + 'type' => 'checkbox', + 'defaultValue' => 'checked', + ], + '/atualidade/atividade-legislativa' => [ + 'name' => 'Atividade Legislativa', + 'type' => 'checkbox', + 'defaultValue' => 'checked', + ], + '/atualidade/notas-informativas' => [ + 'name' => 'Notas Informativas', + 'type' => 'checkbox', + 'defaultValue' => 'checked', + ] + ] + ]; - public function getIcon(){ - return 'https://www.presidencia.pt/Theme/favicon/apple-touch-icon.png'; - } + const PT_MONTH_NAMES = [ + 'janeiro', + 'fevereiro', + 'março', + 'abril', + 'maio', + 'junho', + 'julho', + 'agosto', + 'setembro', + 'outubro', + 'novembro', + 'dezembro']; - public function collectData() { - foreach(array_keys($this->getParameters()['Section']) as $k) { - Debug::log('Key: ' . var_export($k, true)); - if($this->getInput($k)) { - $html = getSimpleHTMLDOMCached($this->getURI() . $k); + public function getIcon() + { + return 'https://www.presidencia.pt/Theme/favicon/apple-touch-icon.png'; + } - foreach($html->find('#atualidade-list article.card-block') as $element) { - $item = array(); + public function collectData() + { + foreach (array_keys($this->getParameters()['Section']) as $k) { + Debug::log('Key: ' . var_export($k, true)); + if ($this->getInput($k)) { + $html = getSimpleHTMLDOMCached($this->getURI() . $k); - $link = $element->find('a', 0); - $etitle = $element->find('.content-box h2', 0); - $edts = $element->find('p', 1); - $edt = html_entity_decode($edts->innertext, ENT_HTML5); + foreach ($html->find('#atualidade-list article.card-block') as $element) { + $item = []; - $item['title'] = strip_tags($etitle->innertext); - $item['uri'] = self::URI . $link->href; - $item['description'] = $element; - $item['timestamp'] = str_ireplace( - array_map(function($name) { return ' de ' . $name . ' de '; }, self::PT_MONTH_NAMES), - array_map(function($num) { return sprintf('-%02d-', $num); }, range(1, sizeof(self::PT_MONTH_NAMES))), - $edt); + $link = $element->find('a', 0); + $etitle = $element->find('.content-box h2', 0); + $edts = $element->find('p', 1); + $edt = html_entity_decode($edts->innertext, ENT_HTML5); - $this->items[] = $item; - } - } - } - } + $item['title'] = strip_tags($etitle->innertext); + $item['uri'] = self::URI . $link->href; + $item['description'] = $element; + $item['timestamp'] = str_ireplace( + array_map(function ($name) { + return ' de ' . $name . ' de '; + }, self::PT_MONTH_NAMES), + array_map(function ($num) { + return sprintf('-%02d-', $num); + }, range(1, sizeof(self::PT_MONTH_NAMES))), + $edt + ); + + $this->items[] = $item; + } + } + } + } } diff --git a/bridges/RaceDepartmentBridge.php b/bridges/RaceDepartmentBridge.php index b8b6e6fb..c33ee67a 100644 --- a/bridges/RaceDepartmentBridge.php +++ b/bridges/RaceDepartmentBridge.php @@ -1,41 +1,44 @@ <?php -class RaceDepartmentBridge extends FeedExpander { - const NAME = 'RaceDepartment News'; - const URI = 'https://racedepartment.com/'; - const DESCRIPTION = 'Get the latest (sim)racing news from RaceDepartment.'; - const MAINTAINER = 't0stiman'; - public function collectData() { - $this->collectExpandableDatas('https://www.racedepartment.com/ams/index.rss', 10); - } - - protected function parseItem($feedItem) { - $item = parent::parseRss2Item($feedItem); - - //fetch page - $articlePage = getSimpleHTMLDOMCached($feedItem->link); - - $coverImage = $articlePage->find('img.js-articleCoverImage', 0); - #relative url -> absolute url - $coverImage = str_replace('src="/', 'src="' . $this->getURI() . '/', $coverImage); - $article = $articlePage->find('article.articleBody-main > div.bbWrapper', 0); - $item['content'] = str_get_html($coverImage . $article); - - //convert iframes to links. meant for embedded videos. - foreach($item['content']->find('iframe') as $found) { - - $iframeUrl = $found->getAttribute('src'); - - if ($iframeUrl) { - $found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>'; - } - } - - $item['categories'] = array(); - foreach($articlePage->find('a.tagItem') as $tag) { - array_push($item['categories'], $tag->innertext); - } - - return $item; - } +class RaceDepartmentBridge extends FeedExpander +{ + const NAME = 'RaceDepartment News'; + const URI = 'https://racedepartment.com/'; + const DESCRIPTION = 'Get the latest (sim)racing news from RaceDepartment.'; + const MAINTAINER = 't0stiman'; + + public function collectData() + { + $this->collectExpandableDatas('https://www.racedepartment.com/ams/index.rss', 10); + } + + protected function parseItem($feedItem) + { + $item = parent::parseRss2Item($feedItem); + + //fetch page + $articlePage = getSimpleHTMLDOMCached($feedItem->link); + + $coverImage = $articlePage->find('img.js-articleCoverImage', 0); + #relative url -> absolute url + $coverImage = str_replace('src="/', 'src="' . $this->getURI() . '/', $coverImage); + $article = $articlePage->find('article.articleBody-main > div.bbWrapper', 0); + $item['content'] = str_get_html($coverImage . $article); + + //convert iframes to links. meant for embedded videos. + foreach ($item['content']->find('iframe') as $found) { + $iframeUrl = $found->getAttribute('src'); + + if ($iframeUrl) { + $found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>'; + } + } + + $item['categories'] = []; + foreach ($articlePage->find('a.tagItem') as $tag) { + array_push($item['categories'], $tag->innertext); + } + + return $item; + } } diff --git a/bridges/RadioMelodieBridge.php b/bridges/RadioMelodieBridge.php index 6b392394..a402fe45 100644 --- a/bridges/RadioMelodieBridge.php +++ b/bridges/RadioMelodieBridge.php @@ -1,196 +1,197 @@ <?php -class RadioMelodieBridge extends BridgeAbstract { - const NAME = 'Radio Melodie Actu'; - const URI = 'https://www.radiomelodie.com'; - const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie'; - const MAINTAINER = 'sysadminstory'; - - public function getIcon() { - return self::URI . '/img/favicon.png'; - } - - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI . '/actu/'); - $list = $html->find('div[class=listArticles]', 0)->children(); - - foreach($list as $element) { - if($element->tag == 'a') { - $articleURL = self::URI . $element->href; - $article = getSimpleHTMLDOM($articleURL); - $this->rewriteAudioPlayers($article); - // Reload the modified content - $article = str_get_html($article->save()); - $textDOM = $article->find('article', 0); - - // Initialise arrays - $item = array(); - $audio = array(); - $picture = array(); - - // Get the Main picture URL - $picture[] = self::URI . $article->find('figure[class=photoviewer]', 0)->find('img', 0)->src; - $audioHTML = $article->find('audio'); - - // Add the audio element to the enclosure - foreach($audioHTML as $audioElement) { - $audioURL = $audioElement->src; - $audio[] = $audioURL; - } - - // Rewrite pictures URL - $imgs = $textDOM->find('img[src^="http://www.radiomelodie.com/image.php]'); - foreach($imgs as $img) { - $img->src = $this->rewriteImage($img->src); - $article->save(); - } - - // Remove Google Ads - $ads = $article->find('div[class=adInline]'); - foreach($ads as $ad) { - $ad->outertext = ''; - $article->save(); - } - - // Extract the author - $author = $article->find('div[class=author]', 0)->children(1)->children(0)->plaintext; - - // Handle date to timestamp - $dateHTML = $article->find('div[class=author]', 0)->children(1)->plaintext; - - preg_match('/([a-z]{4,10}[ ]{1,2}[0-9]{1,2} [\p{L}]{3,10} [0-9]{4} à [0-9]{2}:[0-9]{2})/mus', $dateHTML, $matches); - $dateText = $matches[1]; - - $timestamp = $this->parseDate($dateText); - - $item['enclosures'] = array_merge($picture, $audio); - $item['author'] = $author; - $item['uri'] = $articleURL; - $item['title'] = $article->find('meta[property=og:title]', 0)->content; - if($timestamp !== false) { - $item['timestamp'] = $timestamp; - } - - // Remove the share article part - $textDOM->find('div[class=share]', 0)->outertext = ''; - - // Rewrite relative Links - $textDOM = defaultLinkTo($textDOM, self::URI . '/'); - - $article->save(); - $text = $textDOM->innertext; - $item['content'] = '<h1>' . $item['title'] . '</h1>' . $dateText . '<br/>' . $text; - $this->items[] = $item; - } - } - } - - /* - * Function to rewrite image URL to use the real Image URL and not the resized one (which is very slow) - */ - private function rewriteImage($url) - { - $parts = explode('?', $url); - parse_str(html_entity_decode($parts[1]), $params); - return self::URI . '/' . $params['image']; - - } - - /* - * Function to rewrite Audio Players to use the <audio> tag and not the javascript audio player - */ - private function rewriteAudioPlayers($html) - { - // Find all audio Players - $audioPlayers = $html->find('div[class=audioPlayer]'); - - foreach($audioPlayers as $audioPlayer) { - // Get the javascript content below the player - $js = $audioPlayer->next_sibling(); - - // Extract the audio file URL - preg_match('/wavesurfer[0-9]+.load\(\'(.*)\'\)/m', $js->innertext, $urls); - - // Create the plain HTML <audio> content to play this audio file - $content = '<audio style="width: 100%" src="' . $urls[1] . '" controls ></audio>'; - - // Replace the <script> tag by the <audio> tag - $js->outertext = $content; - // Remove the initial Audio Player - $audioPlayer->outertext = ''; - } - - } - - /* - * Function to parse the article date - */ - private function parseDate($date_fr) - { - // French date texts - $search_fr = array( - 'janvier', - 'février', - 'mars', - 'avril', - 'mai', - 'juin', - 'juillet', - 'août', - 'septembre', - 'octobre', - 'novembre', - 'décembre', - 'lundi', - 'mardi', - 'mercredi', - 'jeudi', - 'vendredi', - 'samedi', - 'dimanche' - ); - - // English replacement date text - $replace_en = array( - 'january', - 'february', - 'march', - 'april', - 'may', - 'june', - 'july', - 'august', - 'september', - 'october', - 'november', - 'december', - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'sunday' - ); - - $dateFormat = 'l j F Y \à H:i'; - - // Convert the date from French to English - $date_en = str_replace($search_fr, $replace_en, $date_fr); - - // Parse the date and convert it to an array - $date_array = date_parse_from_format($dateFormat, $date_en); - - // Convert the array to a unix timestamp - $timestamp = mktime( - $date_array['hour'], - $date_array['minute'], - $date_array['second'], - $date_array['month'], - $date_array['day'], - $date_array['year'] - ); - - return $timestamp; - - } + +class RadioMelodieBridge extends BridgeAbstract +{ + const NAME = 'Radio Melodie Actu'; + const URI = 'https://www.radiomelodie.com'; + const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie'; + const MAINTAINER = 'sysadminstory'; + + public function getIcon() + { + return self::URI . '/img/favicon.png'; + } + + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI . '/actu/'); + $list = $html->find('div[class=listArticles]', 0)->children(); + + foreach ($list as $element) { + if ($element->tag == 'a') { + $articleURL = self::URI . $element->href; + $article = getSimpleHTMLDOM($articleURL); + $this->rewriteAudioPlayers($article); + // Reload the modified content + $article = str_get_html($article->save()); + $textDOM = $article->find('article', 0); + + // Initialise arrays + $item = []; + $audio = []; + $picture = []; + + // Get the Main picture URL + $picture[] = self::URI . $article->find('figure[class=photoviewer]', 0)->find('img', 0)->src; + $audioHTML = $article->find('audio'); + + // Add the audio element to the enclosure + foreach ($audioHTML as $audioElement) { + $audioURL = $audioElement->src; + $audio[] = $audioURL; + } + + // Rewrite pictures URL + $imgs = $textDOM->find('img[src^="http://www.radiomelodie.com/image.php]'); + foreach ($imgs as $img) { + $img->src = $this->rewriteImage($img->src); + $article->save(); + } + + // Remove Google Ads + $ads = $article->find('div[class=adInline]'); + foreach ($ads as $ad) { + $ad->outertext = ''; + $article->save(); + } + + // Extract the author + $author = $article->find('div[class=author]', 0)->children(1)->children(0)->plaintext; + + // Handle date to timestamp + $dateHTML = $article->find('div[class=author]', 0)->children(1)->plaintext; + + preg_match('/([a-z]{4,10}[ ]{1,2}[0-9]{1,2} [\p{L}]{3,10} [0-9]{4} à [0-9]{2}:[0-9]{2})/mus', $dateHTML, $matches); + $dateText = $matches[1]; + + $timestamp = $this->parseDate($dateText); + + $item['enclosures'] = array_merge($picture, $audio); + $item['author'] = $author; + $item['uri'] = $articleURL; + $item['title'] = $article->find('meta[property=og:title]', 0)->content; + if ($timestamp !== false) { + $item['timestamp'] = $timestamp; + } + + // Remove the share article part + $textDOM->find('div[class=share]', 0)->outertext = ''; + + // Rewrite relative Links + $textDOM = defaultLinkTo($textDOM, self::URI . '/'); + + $article->save(); + $text = $textDOM->innertext; + $item['content'] = '<h1>' . $item['title'] . '</h1>' . $dateText . '<br/>' . $text; + $this->items[] = $item; + } + } + } + + /* + * Function to rewrite image URL to use the real Image URL and not the resized one (which is very slow) + */ + private function rewriteImage($url) + { + $parts = explode('?', $url); + parse_str(html_entity_decode($parts[1]), $params); + return self::URI . '/' . $params['image']; + } + + /* + * Function to rewrite Audio Players to use the <audio> tag and not the javascript audio player + */ + private function rewriteAudioPlayers($html) + { + // Find all audio Players + $audioPlayers = $html->find('div[class=audioPlayer]'); + + foreach ($audioPlayers as $audioPlayer) { + // Get the javascript content below the player + $js = $audioPlayer->next_sibling(); + + // Extract the audio file URL + preg_match('/wavesurfer[0-9]+.load\(\'(.*)\'\)/m', $js->innertext, $urls); + + // Create the plain HTML <audio> content to play this audio file + $content = '<audio style="width: 100%" src="' . $urls[1] . '" controls ></audio>'; + + // Replace the <script> tag by the <audio> tag + $js->outertext = $content; + // Remove the initial Audio Player + $audioPlayer->outertext = ''; + } + } + + /* + * Function to parse the article date + */ + private function parseDate($date_fr) + { + // French date texts + $search_fr = [ + 'janvier', + 'février', + 'mars', + 'avril', + 'mai', + 'juin', + 'juillet', + 'août', + 'septembre', + 'octobre', + 'novembre', + 'décembre', + 'lundi', + 'mardi', + 'mercredi', + 'jeudi', + 'vendredi', + 'samedi', + 'dimanche' + ]; + + // English replacement date text + $replace_en = [ + 'january', + 'february', + 'march', + 'april', + 'may', + 'june', + 'july', + 'august', + 'september', + 'october', + 'november', + 'december', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday' + ]; + + $dateFormat = 'l j F Y \à H:i'; + + // Convert the date from French to English + $date_en = str_replace($search_fr, $replace_en, $date_fr); + + // Parse the date and convert it to an array + $date_array = date_parse_from_format($dateFormat, $date_en); + + // Convert the array to a unix timestamp + $timestamp = mktime( + $date_array['hour'], + $date_array['minute'], + $date_array['second'], + $date_array['month'], + $date_array['day'], + $date_array['year'] + ); + + return $timestamp; + } } diff --git a/bridges/RainbowSixSiegeBridge.php b/bridges/RainbowSixSiegeBridge.php index 2d0762bd..73e2bdc4 100644 --- a/bridges/RainbowSixSiegeBridge.php +++ b/bridges/RainbowSixSiegeBridge.php @@ -1,48 +1,51 @@ <?php -class RainbowSixSiegeBridge extends BridgeAbstract { - - const MAINTAINER = 'corenting'; - const NAME = 'Rainbow Six Siege News'; - const URI = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege/news-updates'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Latest news about Rainbow Six Siege'; - - // API key to call Ubisoft API, extracted from the React frontend - const NIMBUS_API_KEY = '3u0FfSBUaTSew-2NVfAOSYWevVQHWtY9q3VM8Xx9Lto'; - - public function getIcon() { - return 'https://static-dm.akamaized.net/siege/prod/favicon.ico'; - } - - public function collectData(){ - $dlUrl = 'https://nimbus.ubisoft.com/api/v1/items?categoriesFilter=all'; - $dlUrl = $dlUrl . '&limit=6&mediaFilter=all&skip=0&startIndex=0&tags=BR-rainbow-six%20GA-siege'; - $dlUrl = $dlUrl . '&locale=en-us&fallbackLocale=en-us&environment=master'; - $jsonString = getContents($dlUrl, array( - 'Authorization: ' . self::NIMBUS_API_KEY - )); - - $json = json_decode($jsonString, true); - $json = $json['items']; - - // Start at index 2 to remove highlighted articles - for($i = 0; $i < count($json); $i++) { - $jsonItem = $json[$i]; - - $uri = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege'; - $uri = $uri . $jsonItem['button']['buttonUrl']; - - $thumbnail = '<img src="' . $jsonItem['thumbnail']['url'] . '" alt="Thumbnail">'; - $content = $thumbnail . '<br />' . markdownToHtml($jsonItem['content']); - - $item = array(); - $item['uri'] = $uri; - $item['id'] = $jsonItem['id']; - $item['title'] = $jsonItem['title']; - $item['content'] = $content; - $item['timestamp'] = strtotime($jsonItem['date']); - - $this->items[] = $item; - } - } + +class RainbowSixSiegeBridge extends BridgeAbstract +{ + const MAINTAINER = 'corenting'; + const NAME = 'Rainbow Six Siege News'; + const URI = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege/news-updates'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Latest news about Rainbow Six Siege'; + + // API key to call Ubisoft API, extracted from the React frontend + const NIMBUS_API_KEY = '3u0FfSBUaTSew-2NVfAOSYWevVQHWtY9q3VM8Xx9Lto'; + + public function getIcon() + { + return 'https://static-dm.akamaized.net/siege/prod/favicon.ico'; + } + + public function collectData() + { + $dlUrl = 'https://nimbus.ubisoft.com/api/v1/items?categoriesFilter=all'; + $dlUrl = $dlUrl . '&limit=6&mediaFilter=all&skip=0&startIndex=0&tags=BR-rainbow-six%20GA-siege'; + $dlUrl = $dlUrl . '&locale=en-us&fallbackLocale=en-us&environment=master'; + $jsonString = getContents($dlUrl, [ + 'Authorization: ' . self::NIMBUS_API_KEY + ]); + + $json = json_decode($jsonString, true); + $json = $json['items']; + + // Start at index 2 to remove highlighted articles + for ($i = 0; $i < count($json); $i++) { + $jsonItem = $json[$i]; + + $uri = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege'; + $uri = $uri . $jsonItem['button']['buttonUrl']; + + $thumbnail = '<img src="' . $jsonItem['thumbnail']['url'] . '" alt="Thumbnail">'; + $content = $thumbnail . '<br />' . markdownToHtml($jsonItem['content']); + + $item = []; + $item['uri'] = $uri; + $item['id'] = $jsonItem['id']; + $item['title'] = $jsonItem['title']; + $item['content'] = $content; + $item['timestamp'] = strtotime($jsonItem['date']); + + $this->items[] = $item; + } + } } diff --git a/bridges/RedditBridge.php b/bridges/RedditBridge.php index 538d810b..1a643283 100644 --- a/bridges/RedditBridge.php +++ b/bridges/RedditBridge.php @@ -1,289 +1,290 @@ <?php -class RedditBridge extends BridgeAbstract { - - const MAINTAINER = 'dawidsowa'; - const NAME = 'Reddit Bridge'; - const URI = 'https://www.reddit.com'; - const DESCRIPTION = 'Return hot submissions from Reddit'; - - const PARAMETERS = array( - 'global' => array( - 'score' => array( - 'name' => 'Minimal score', - 'required' => false, - 'type' => 'number', - 'exampleValue' => 100, - 'title' => 'Filter out posts with lower score' - ), - 'd' => array( - 'name' => 'Sort By', - 'type' => 'list', - 'title' => 'Sort by new, hot, top or relevancy', - 'values' => array( - 'Hot' => 'hot', - 'Relevance' => 'relevance', - 'New' => 'new', - 'Top' => 'top' - ), - 'defaultValue' => 'Hot' - ), - 'search' => array( - 'name' => 'Keyword search', - 'required' => false, - 'exampleValue' => 'cats, dogs', - 'title' => 'Keyword search, separated by commas' - ) - ), - 'single' => array( - 'r' => array( - 'name' => 'SubReddit', - 'required' => true, - 'exampleValue' => 'selfhosted', - 'title' => 'SubReddit name' - ) - ), - 'multi' => array( - 'rs' => array( - 'name' => 'SubReddits', - 'required' => true, - 'exampleValue' => 'selfhosted, php', - 'title' => 'SubReddit names, separated by commas' - ) - ), - 'user' => array( - 'u' => array( - 'name' => 'User', - 'required' => true, - 'exampleValue' => 'shwikibot', - 'title' => 'User name' - ), - 'comments' => array( - 'type' => 'checkbox', - 'name' => 'Comments', - 'title' => 'Whether to return comments', - 'defaultValue' => false - ) - ) - ); - - public function detectParameters($url) { - $parsed_url = parse_url($url); - - if ($parsed_url['host'] != 'www.reddit.com' && $parsed_url['host'] != 'old.reddit.com') return null; - - $path = explode('/', $parsed_url['path']); - - if ($path[1] == 'r') { - return array( - 'r' => $path[2] - ); - } elseif ($path[1] == 'user') { - return array( - 'u' => $path[2] - ); - } else { - return null; - } - } - - public function getIcon() { - return 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png'; - } - - public function getName() { - if ($this->queriedContext == 'single') { - return 'Reddit r/' . $this->getInput('r'); - } elseif ($this->queriedContext == 'user') { - return 'Reddit u/' . $this->getInput('u'); - } else { - return self::NAME; - } - } - - public function collectData() { - - $user = false; - $comments = false; - $section = $this->getInput('d'); - - switch ($this->queriedContext) { - case 'single': - $subreddits[] = $this->getInput('r'); - break; - case 'multi': - $subreddits = explode(',', $this->getInput('rs')); - break; - case 'user': - $subreddits[] = $this->getInput('u'); - $user = true; - $comments = $this->getInput('comments'); - break; - } - - if(!($this->getInput('search') === '')) { - $keywords = $this->getInput('search'); - $keywords = str_replace(array(',', ' '), '%20', $keywords); - $keywords = $keywords . '%20'; - } else { - $keywords = ''; - } - - foreach ($subreddits as $subreddit) { - $name = trim($subreddit); - $values = getContents(self::URI - . '/search.json?q=' - . $keywords - . ($user ? 'author%3A' : 'subreddit%3A') - . $name - . '&sort=' - . $this->getInput('d') - . '&include_over_18=on'); - $decodedValues = json_decode($values); - - foreach ($decodedValues->data->children as $post) { - if ($post->kind == 't1' && !$comments) { - continue; - } - - $data = $post->data; - - if ($data->score < $this->getInput('score')) { - continue; - } - - $item = array(); - $item['author'] = $data->author; - $item['uid'] = $data->id; - $item['timestamp'] = $data->created_utc; - $item['uri'] = $this->encodePermalink($data->permalink); - - $item['categories'] = array(); - - if ($post->kind == 't1') { - $item['title'] = 'Comment: ' . $data->link_title; - } else { - $item['title'] = $data->title; - - $item['categories'][] = $data->link_flair_text; - $item['categories'][] = $data->pinned ? 'Pinned' : null; - $item['categories'][] = $data->spoiler ? 'Spoiler' : null; - } - - $item['categories'][] = $data->over_18 ? 'NSFW' : null; - $item['categories'] = array_filter($item['categories']); - - if ($post->kind == 't1') { - // Comment - - $item['content'] - = htmlspecialchars_decode($data->body_html); - - } elseif ($data->is_self) { - // Text post - - $item['content'] - = htmlspecialchars_decode($data->selftext_html); - - } elseif (isset($data->post_hint) ? $data->post_hint == 'link' : false) { - // Link with preview - - if (isset($data->media)) { - // Reddit embeds content for some sites (e.g. Twitter) - $embed = htmlspecialchars_decode( - $data->media->oembed->html - ); - } else { - $embed = ''; - } - - $item['content'] = $this->template( - $data->url, - $data->thumbnail, - $data->domain - ) . $embed; - - } elseif (isset($data->post_hint) ? $data->post_hint == 'image' : false) { - // Single image - - $item['content'] = $this->link( - $this->encodePermalink($data->permalink), - '<img src="' . $data->url . '" />' - ); - - } elseif (isset($data->is_gallery) ? $data->is_gallery : false) { - // Multiple images - - $images = array(); - foreach ($data->gallery_data->items as $media) { - $id = $media->media_id; - $type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u'; - $src = $data->media_metadata->$id->s->$type; - $images[] = '<figure><img src="' . $src . '"/></figure><br>'; - } - - $item['content'] = implode('', $images); - - } elseif ($data->is_video) { - // Video - - // Higher index -> Higher resolution - end($data->preview->images[0]->resolutions); - $index = key($data->preview->images[0]->resolutions); - - $item['content'] = $this->template( - $data->url, - $data->preview->images[0]->resolutions[$index]->url, - 'Video' - ); - - } elseif (isset($data->media) ? $data->media->type == 'youtube.com' : false) { - // Youtube link - - $item['content'] = $this->template( - $data->url, - $data->media->oembed->thumbnail_url, - 'YouTube'); - - } elseif (explode('.', $data->domain)[0] == 'self') { - // Crossposted text post - // TODO (optionally?) Fetch content of the original post. - - $item['content'] = $this->link( - $this->encodePermalink($data->permalink), - 'Crossposted from r/' - . explode('.', $data->domain)[1] - ); - - } else { - // Link WITHOUT preview - - $item['content'] = $this->link($data->url, $data->domain); - } - - $this->items[] = $item; - } - } - // Sort the order to put the latest posts first, even for mixed subreddits - usort($this->items, function($a, $b) { - return $a['timestamp'] < $b['timestamp']; - }); - } - - private function encodePermalink($link) { - return self::URI . implode( - '/', - array_map('urlencode', explode('/', $link)) - ); - } - - private function template($href, $src, $caption) { - return '<a href="' . $href . '"><figure><figcaption>' - . $caption . '</figcaption><img src="' - . $src . '"/></figure></a>'; - } - - private function link($href, $text) { - return '<a href="' . $href . '">' . $text . '</a>'; - } +class RedditBridge extends BridgeAbstract +{ + const MAINTAINER = 'dawidsowa'; + const NAME = 'Reddit Bridge'; + const URI = 'https://www.reddit.com'; + const DESCRIPTION = 'Return hot submissions from Reddit'; + + const PARAMETERS = [ + 'global' => [ + 'score' => [ + 'name' => 'Minimal score', + 'required' => false, + 'type' => 'number', + 'exampleValue' => 100, + 'title' => 'Filter out posts with lower score' + ], + 'd' => [ + 'name' => 'Sort By', + 'type' => 'list', + 'title' => 'Sort by new, hot, top or relevancy', + 'values' => [ + 'Hot' => 'hot', + 'Relevance' => 'relevance', + 'New' => 'new', + 'Top' => 'top' + ], + 'defaultValue' => 'Hot' + ], + 'search' => [ + 'name' => 'Keyword search', + 'required' => false, + 'exampleValue' => 'cats, dogs', + 'title' => 'Keyword search, separated by commas' + ] + ], + 'single' => [ + 'r' => [ + 'name' => 'SubReddit', + 'required' => true, + 'exampleValue' => 'selfhosted', + 'title' => 'SubReddit name' + ] + ], + 'multi' => [ + 'rs' => [ + 'name' => 'SubReddits', + 'required' => true, + 'exampleValue' => 'selfhosted, php', + 'title' => 'SubReddit names, separated by commas' + ] + ], + 'user' => [ + 'u' => [ + 'name' => 'User', + 'required' => true, + 'exampleValue' => 'shwikibot', + 'title' => 'User name' + ], + 'comments' => [ + 'type' => 'checkbox', + 'name' => 'Comments', + 'title' => 'Whether to return comments', + 'defaultValue' => false + ] + ] + ]; + + public function detectParameters($url) + { + $parsed_url = parse_url($url); + + if ($parsed_url['host'] != 'www.reddit.com' && $parsed_url['host'] != 'old.reddit.com') { + return null; + } + + $path = explode('/', $parsed_url['path']); + + if ($path[1] == 'r') { + return [ + 'r' => $path[2] + ]; + } elseif ($path[1] == 'user') { + return [ + 'u' => $path[2] + ]; + } else { + return null; + } + } + + public function getIcon() + { + return 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png'; + } + + public function getName() + { + if ($this->queriedContext == 'single') { + return 'Reddit r/' . $this->getInput('r'); + } elseif ($this->queriedContext == 'user') { + return 'Reddit u/' . $this->getInput('u'); + } else { + return self::NAME; + } + } + + public function collectData() + { + $user = false; + $comments = false; + $section = $this->getInput('d'); + + switch ($this->queriedContext) { + case 'single': + $subreddits[] = $this->getInput('r'); + break; + case 'multi': + $subreddits = explode(',', $this->getInput('rs')); + break; + case 'user': + $subreddits[] = $this->getInput('u'); + $user = true; + $comments = $this->getInput('comments'); + break; + } + + if (!($this->getInput('search') === '')) { + $keywords = $this->getInput('search'); + $keywords = str_replace([',', ' '], '%20', $keywords); + $keywords = $keywords . '%20'; + } else { + $keywords = ''; + } + + foreach ($subreddits as $subreddit) { + $name = trim($subreddit); + $values = getContents(self::URI + . '/search.json?q=' + . $keywords + . ($user ? 'author%3A' : 'subreddit%3A') + . $name + . '&sort=' + . $this->getInput('d') + . '&include_over_18=on'); + $decodedValues = json_decode($values); + + foreach ($decodedValues->data->children as $post) { + if ($post->kind == 't1' && !$comments) { + continue; + } + + $data = $post->data; + + if ($data->score < $this->getInput('score')) { + continue; + } + + $item = []; + $item['author'] = $data->author; + $item['uid'] = $data->id; + $item['timestamp'] = $data->created_utc; + $item['uri'] = $this->encodePermalink($data->permalink); + + $item['categories'] = []; + + if ($post->kind == 't1') { + $item['title'] = 'Comment: ' . $data->link_title; + } else { + $item['title'] = $data->title; + + $item['categories'][] = $data->link_flair_text; + $item['categories'][] = $data->pinned ? 'Pinned' : null; + $item['categories'][] = $data->spoiler ? 'Spoiler' : null; + } + + $item['categories'][] = $data->over_18 ? 'NSFW' : null; + $item['categories'] = array_filter($item['categories']); + + if ($post->kind == 't1') { + // Comment + + $item['content'] + = htmlspecialchars_decode($data->body_html); + } elseif ($data->is_self) { + // Text post + + $item['content'] + = htmlspecialchars_decode($data->selftext_html); + } elseif (isset($data->post_hint) ? $data->post_hint == 'link' : false) { + // Link with preview + + if (isset($data->media)) { + // Reddit embeds content for some sites (e.g. Twitter) + $embed = htmlspecialchars_decode( + $data->media->oembed->html + ); + } else { + $embed = ''; + } + + $item['content'] = $this->template( + $data->url, + $data->thumbnail, + $data->domain + ) . $embed; + } elseif (isset($data->post_hint) ? $data->post_hint == 'image' : false) { + // Single image + + $item['content'] = $this->link( + $this->encodePermalink($data->permalink), + '<img src="' . $data->url . '" />' + ); + } elseif (isset($data->is_gallery) ? $data->is_gallery : false) { + // Multiple images + + $images = []; + foreach ($data->gallery_data->items as $media) { + $id = $media->media_id; + $type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u'; + $src = $data->media_metadata->$id->s->$type; + $images[] = '<figure><img src="' . $src . '"/></figure><br>'; + } + + $item['content'] = implode('', $images); + } elseif ($data->is_video) { + // Video + + // Higher index -> Higher resolution + end($data->preview->images[0]->resolutions); + $index = key($data->preview->images[0]->resolutions); + + $item['content'] = $this->template( + $data->url, + $data->preview->images[0]->resolutions[$index]->url, + 'Video' + ); + } elseif (isset($data->media) ? $data->media->type == 'youtube.com' : false) { + // Youtube link + + $item['content'] = $this->template( + $data->url, + $data->media->oembed->thumbnail_url, + 'YouTube' + ); + } elseif (explode('.', $data->domain)[0] == 'self') { + // Crossposted text post + // TODO (optionally?) Fetch content of the original post. + + $item['content'] = $this->link( + $this->encodePermalink($data->permalink), + 'Crossposted from r/' + . explode('.', $data->domain)[1] + ); + } else { + // Link WITHOUT preview + + $item['content'] = $this->link($data->url, $data->domain); + } + + $this->items[] = $item; + } + } + // Sort the order to put the latest posts first, even for mixed subreddits + usort($this->items, function ($a, $b) { + return $a['timestamp'] < $b['timestamp']; + }); + } + + private function encodePermalink($link) + { + return self::URI . implode( + '/', + array_map('urlencode', explode('/', $link)) + ); + } + + private function template($href, $src, $caption) + { + return '<a href="' . $href . '"><figure><figcaption>' + . $caption . '</figcaption><img src="' + . $src . '"/></figure></a>'; + } + + private function link($href, $text) + { + return '<a href="' . $href . '">' . $text . '</a>'; + } } diff --git a/bridges/Releases3DSBridge.php b/bridges/Releases3DSBridge.php index 620340ce..56946a47 100644 --- a/bridges/Releases3DSBridge.php +++ b/bridges/Releases3DSBridge.php @@ -1,107 +1,117 @@ <?php -class Releases3DSBridge extends BridgeAbstract { - const MAINTAINER = 'ORelio'; - const NAME = '3DS Scene Releases'; - const URI = 'http://www.3dsdb.com/'; - const CACHE_TIMEOUT = 10800; // 3h - const DESCRIPTION = 'Returns the newest scene releases for Nintendo 3DS.'; +class Releases3DSBridge extends BridgeAbstract +{ + const MAINTAINER = 'ORelio'; + const NAME = '3DS Scene Releases'; + const URI = 'http://www.3dsdb.com/'; + const CACHE_TIMEOUT = 10800; // 3h + const DESCRIPTION = 'Returns the newest scene releases for Nintendo 3DS.'; - public function collectData(){ - $this->collectDataUrl(self::URI . 'xml.php'); - } + public function collectData() + { + $this->collectDataUrl(self::URI . 'xml.php'); + } - protected function collectDataUrl($dataUrl){ + protected function collectDataUrl($dataUrl) + { + $xml = getContents($dataUrl); + $limit = 0; - $xml = getContents($dataUrl); - $limit = 0; + foreach (array_reverse(explode('<release>', $xml)) as $element) { + if ($limit >= 5) { + break; + } - foreach(array_reverse(explode('<release>', $xml)) as $element) { - if($limit >= 5) { - break; - } + if (strpos($element, '</release>') === false) { + continue; + } - if(strpos($element, '</release>') === false) { - continue; - } + $releasename = extractFromDelimiters($element, '<releasename>', '</releasename>'); + if (empty($releasename)) { + continue; + } - $releasename = extractFromDelimiters($element, '<releasename>', '</releasename>'); - if(empty($releasename)) { - continue; - } + $id = extractFromDelimiters($element, '<id>', '</id>'); + $name = extractFromDelimiters($element, '<name>', '</name>'); + $publisher = extractFromDelimiters($element, '<publisher>', '</publisher>'); + $region = extractFromDelimiters($element, '<region>', '</region>'); + $group = extractFromDelimiters($element, '<group>', '</group>'); + $imagesize = extractFromDelimiters($element, '<imagesize>', '</imagesize>'); + $serial = extractFromDelimiters($element, '<serial>', '</serial>'); + $titleid = extractFromDelimiters($element, '<titleid>', '</titleid>'); + $imgcrc = extractFromDelimiters($element, '<imgcrc>', '</imgcrc>'); + $filename = extractFromDelimiters($element, '<filename>', '</filename>'); + $trimmedsize = extractFromDelimiters($element, '<trimmedsize>', '</trimmedsize>'); + $firmware = extractFromDelimiters($element, '<firmware>', '</firmware>'); + $type = extractFromDelimiters($element, '<type>', '</type>'); + $card = extractFromDelimiters($element, '<card>', '</card>'); - $id = extractFromDelimiters($element, '<id>', '</id>'); - $name = extractFromDelimiters($element, '<name>', '</name>'); - $publisher = extractFromDelimiters($element, '<publisher>', '</publisher>'); - $region = extractFromDelimiters($element, '<region>', '</region>'); - $group = extractFromDelimiters($element, '<group>', '</group>'); - $imagesize = extractFromDelimiters($element, '<imagesize>', '</imagesize>'); - $serial = extractFromDelimiters($element, '<serial>', '</serial>'); - $titleid = extractFromDelimiters($element, '<titleid>', '</titleid>'); - $imgcrc = extractFromDelimiters($element, '<imgcrc>', '</imgcrc>'); - $filename = extractFromDelimiters($element, '<filename>', '</filename>'); - $trimmedsize = extractFromDelimiters($element, '<trimmedsize>', '</trimmedsize>'); - $firmware = extractFromDelimiters($element, '<firmware>', '</firmware>'); - $type = extractFromDelimiters($element, '<type>', '</type>'); - $card = extractFromDelimiters($element, '<card>', '</card>'); + //Main section : Release description from 3DS database + $releaseDescription = '<h3>Release Details</h3><b>Release ID: </b>' . $id + . '<br /><b>Game Name: </b>' . $name + . '<br /><b>Publisher: </b>' . $publisher + . '<br /><b>Region: </b>' . $region + . '<br /><b>Group: </b>' . $group + . '<br /><b>Image size: </b>' . (intval($imagesize) / 8) + . 'MB<br /><b>Serial: </b>' . $serial + . '<br /><b>Title ID: </b>' . $titleid + . '<br /><b>Image CRC: </b>' . $imgcrc + . '<br /><b>File Name: </b>' . $filename + . '<br /><b>Release Name: </b>' . $releasename + . '<br /><b>Trimmed size: </b>' . intval(intval($trimmedsize) / 1048576) + . 'MB<br /><b>Firmware: </b>' . $firmware + . '<br /><b>Type: </b>' . $this->typeToString($type) + . '<br /><b>Card: </b>' . $this->cardToString($card) + . '<br />'; - //Main section : Release description from 3DS database - $releaseDescription = '<h3>Release Details</h3><b>Release ID: </b>' . $id - . '<br /><b>Game Name: </b>' . $name - . '<br /><b>Publisher: </b>' . $publisher - . '<br /><b>Region: </b>' . $region - . '<br /><b>Group: </b>' . $group - . '<br /><b>Image size: </b>' . (intval($imagesize) / 8) - . 'MB<br /><b>Serial: </b>' . $serial - . '<br /><b>Title ID: </b>' . $titleid - . '<br /><b>Image CRC: </b>' . $imgcrc - . '<br /><b>File Name: </b>' . $filename - . '<br /><b>Release Name: </b>' . $releasename - . '<br /><b>Trimmed size: </b>' . intval(intval($trimmedsize) / 1048576) - . 'MB<br /><b>Firmware: </b>' . $firmware - . '<br /><b>Type: </b>' . $this->typeToString($type) - . '<br /><b>Card: </b>' . $this->cardToString($card) - . '<br />'; + //Build search links section to facilitate release search using search engines + $releaseNameEncoded = urlencode(str_replace(' ', '+', $releasename)); + $searchLinkGoogle = 'https://google.com/?q=' . $releaseNameEncoded; + $searchLinkDuckDuckGo = 'https://duckduckgo.com/?q=' . $releaseNameEncoded; + $searchLinkQwant = 'https://lite.qwant.com/?q=' . $releaseNameEncoded . '&t=web'; + $releaseSearchLinks = '<h3>Search this release</h3><ul><li><a href="' + . $searchLinkGoogle + . '">Search using Google</a></li><li><a href="' + . $searchLinkDuckDuckGo + . '">Search using DuckDuckGo</a></li><li><a href="' + . $searchLinkQwant + . '">Search using Qwant</a></li></ul>'; - //Build search links section to facilitate release search using search engines - $releaseNameEncoded = urlencode(str_replace(' ', '+', $releasename)); - $searchLinkGoogle = 'https://google.com/?q=' . $releaseNameEncoded; - $searchLinkDuckDuckGo = 'https://duckduckgo.com/?q=' . $releaseNameEncoded; - $searchLinkQwant = 'https://lite.qwant.com/?q=' . $releaseNameEncoded . '&t=web'; - $releaseSearchLinks = '<h3>Search this release</h3><ul><li><a href="' - . $searchLinkGoogle - . '">Search using Google</a></li><li><a href="' - . $searchLinkDuckDuckGo - . '">Search using DuckDuckGo</a></li><li><a href="' - . $searchLinkQwant - . '">Search using Qwant</a></li></ul>'; + //Build and add final item with the above three sections + $item = []; + $item['title'] = $name; + $item['author'] = $publisher; + $item['timestamp'] = $ignDate; + $item['enclosures'] = [$ignCoverArt]; + $item['uri'] = empty($ignLink) ? $searchLinkDuckDuckGo : $ignLink; + $item['content'] = $ignDescription . $releaseDescription . $releaseSearchLinks; + $this->items[] = $item; + $limit++; + } + } - //Build and add final item with the above three sections - $item = array(); - $item['title'] = $name; - $item['author'] = $publisher; - $item['timestamp'] = $ignDate; - $item['enclosures'] = array($ignCoverArt); - $item['uri'] = empty($ignLink) ? $searchLinkDuckDuckGo : $ignLink; - $item['content'] = $ignDescription . $releaseDescription . $releaseSearchLinks; - $this->items[] = $item; - $limit++; - } - } + private function typeToString($type) + { + switch ($type) { + case 1: + return 'Card Game'; + case 4: + return 'eShop'; + default: + return '??? (' . $type . ')'; + } + } - private function typeToString($type){ - switch($type) { - case 1: return 'Card Game'; - case 4: return 'eShop'; - default: return '??? (' . $type . ')'; - } - } - - private function cardToString($card){ - switch($card) { - case 1: return 'Regular (CARD1)'; - case 2: return 'NAND (CARD2)'; - default: return '??? (' . $card . ')'; - } - } + private function cardToString($card) + { + switch ($card) { + case 1: + return 'Regular (CARD1)'; + case 2: + return 'NAND (CARD2)'; + default: + return '??? (' . $card . ')'; + } + } } diff --git a/bridges/ReleasesSwitchBridge.php b/bridges/ReleasesSwitchBridge.php index 89ca76d5..7544278f 100644 --- a/bridges/ReleasesSwitchBridge.php +++ b/bridges/ReleasesSwitchBridge.php @@ -2,16 +2,17 @@ // This bridge depends on Releases3DSBridge if (!class_exists('Releases3DSBridge')) { - include('Releases3DSBridge.php'); + include('Releases3DSBridge.php'); } -class ReleasesSwitchBridge extends Releases3DSBridge { +class ReleasesSwitchBridge extends Releases3DSBridge +{ + const NAME = 'Switch Scene Releases'; + const URI = 'http://www.nswdb.com/'; + const DESCRIPTION = 'Returns the newest scene releases for Nintendo Switch.'; - const NAME = 'Switch Scene Releases'; - const URI = 'http://www.nswdb.com/'; - const DESCRIPTION = 'Returns the newest scene releases for Nintendo Switch.'; - - public function collectData(){ - $this->collectDataUrl(self::URI . 'xml.php'); - } + public function collectData() + { + $this->collectDataUrl(self::URI . 'xml.php'); + } } diff --git a/bridges/ReporterreBridge.php b/bridges/ReporterreBridge.php index 3b8e2dbe..c441d876 100644 --- a/bridges/ReporterreBridge.php +++ b/bridges/ReporterreBridge.php @@ -1,40 +1,43 @@ <?php -class ReporterreBridge extends BridgeAbstract { - const MAINTAINER = 'nyutag'; - const NAME = 'Reporterre Bridge'; - const URI = 'https://www.reporterre.net/'; - const DESCRIPTION = 'Returns the newest articles.'; +class ReporterreBridge extends BridgeAbstract +{ + const MAINTAINER = 'nyutag'; + const NAME = 'Reporterre Bridge'; + const URI = 'https://www.reporterre.net/'; + const DESCRIPTION = 'Returns the newest articles.'; - private function extractContent($url){ - $html2 = getSimpleHTMLDOM($url); - $html2 = defaultLinkTo($html2, self::URI); + private function extractContent($url) + { + $html2 = getSimpleHTMLDOM($url); + $html2 = defaultLinkTo($html2, self::URI); - foreach($html2->find('div[style=text-align:justify]') as $e) { - $text = $e->outertext; - } + foreach ($html2->find('div[style=text-align:justify]') as $e) { + $text = $e->outertext; + } - $html2->clear(); - unset($html2); + $html2->clear(); + unset($html2); - $text = strip_tags($text, '<p><br><a><img>'); - return $text; - } + $text = strip_tags($text, '<p><br><a><img>'); + return $text; + } - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend'); - $limit = 0; + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend'); + $limit = 0; - foreach($html->find('item') as $element) { - if($limit < 5) { - $item = array(); - $item['title'] = html_entity_decode($element->find('title', 0)->plaintext); - $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext); - $item['uri'] = $element->find('guid', 0)->innertext; - $item['content'] = html_entity_decode($this->extractContent($item['uri'])); - $this->items[] = $item; - $limit++; - } - } - } + foreach ($html->find('item') as $element) { + if ($limit < 5) { + $item = []; + $item['title'] = html_entity_decode($element->find('title', 0)->plaintext); + $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext); + $item['uri'] = $element->find('guid', 0)->innertext; + $item['content'] = html_entity_decode($this->extractContent($item['uri'])); + $this->items[] = $item; + $limit++; + } + } + } } diff --git a/bridges/ReutersBridge.php b/bridges/ReutersBridge.php index 196139b3..853b134b 100644 --- a/bridges/ReutersBridge.php +++ b/bridges/ReutersBridge.php @@ -1,473 +1,483 @@ <?php + class ReutersBridge extends BridgeAbstract { - const MAINTAINER = 'hollowleviathan, spraynard, csisoap'; - const NAME = 'Reuters Bridge'; - const URI = 'https://www.reuters.com'; - const CACHE_TIMEOUT = 1800; // 30min - const DESCRIPTION = 'Returns news from Reuters'; - - private $feedName = self::NAME; - private $useWireAPI = false; - - /** - * Wireitem types allowed in the final story output - */ - const ALLOWED_WIREITEM_TYPES = array( - 'story', - 'headlines' - ); - - /** - * Wireitem template types allowed in the final story output - */ - const ALLOWED_TEMPLATE_TYPES = array( - 'story', - 'headlines' - ); - - const PARAMETERS = array( - array( - 'feed' => array( - 'name' => 'News Feed', - 'type' => 'list', - 'title' => 'Feeds from Reuters U.S/International edition', - 'values' => array( - 'Top News' => 'home/topnews', - 'Fact Check' => 'chan:abtpk0vm', - 'Entertainment' => 'chan:8ym8q8dl', - 'Politics' => 'politics', - 'Wire' => 'wire', - 'Breakingviews' => '/breakingviews', - 'World' => array( - 'World' => 'world', - 'Africa' => '/world/africa', - 'Americas' => '/world/americas', - 'Asia-Pacific' => '/world/asia-pacific', - 'China' => 'china', - 'europe' => '/world/europe', - 'India' => '/world/india', - 'Middle East' => '/world/middle-east', - 'UK' => 'chan:61leiu7j', - 'USA News' => 'us', - 'The Great Reboot' => '/world/the-great-reboot', - 'Reuters Next' => '/world/reuters-next' - ), - 'Business' => array( - 'Business' => 'business', - 'Aerospace and Defense' => 'aerospace', - 'Autos Transportation' => '/business/autos-transportation', - 'Energy' => 'energy', - 'Finance' => '/business/finance', - 'Health' => 'chan:8hw7807a', - 'Media Telecom' => '/business/media-telecom', - 'Retail Consumer' => '/business/retail-consumer', - 'Sustainable Business' => '/business/sustainable-business', - 'Change Suite' => '/business/change-suite', - 'Future of Health' => '/business/future-of-health', - 'Future of Money' => '/business/future-of-money', - 'Take Five' => '/business/take-five', - 'Reuters Impact' => '/business/reuters-impact', - ), - 'Legal' => array( - 'Legal' => '/legal', - 'Government' => '/legal/government', - 'Legal Industry' => '/legal/legalindustry', - 'Litigation' => '/legal/litigation', - 'Transactional' => '/legal/transactional', - ), - 'Markets' => array( - 'Markets' => 'markets', - 'Asian Markets' => '/markets/asia', - 'Commodities' => '/markets/commodities', - 'Currencies' => '/markets/currencies', - 'Deals' => '/markets/deals', - 'European Markets' => '/markets/europe', - 'Funds' => '/markets/fund', - 'Global Market Data' => '/markets/global-market-data', - 'Rates & Bonds' => '/markets/rates-bonds', - 'Stocks' => '/markets/stocks', - 'U.S Markets' => '/markets/us', - 'Wealth' => '/markets/wealth', - 'Macro Matters' => '/markets/macromatters', - ), - 'Technology' => array( - 'Technology' => 'tech', - 'Disrupted' => '/technology/disrupted', - 'Reuters Momentum' => '/technology/reuters-momentum', - ), - 'Sports' => array( - 'Sports' => 'sports', - 'Athletics' => '/lifestyle/sports/athletics', - 'Cricket' => '/lifestyle/sports/cricket', - 'Cycling' => '/lifestyle/sports/cycling', - 'Golf' => '/lifestyle/sports/golf', - 'Motor Sports' => '/lifestyle/sports/motor-sports', - 'Soccer' => '/lifestyle/sports/soccer', - 'Tennis' => '/lifestyle/sports/tennis', - ), - 'Lifestyle' => array( - 'Lifestyle' => 'life', - 'Oddly Enough' => '/lifestyle/oddly-enough', - 'Science' => 'science', - ) - ) - ) - ) - ); - - const BACKWARD_COMPATIBILITY = array( - 'world' => '/world', - 'china' => '/world/china', - 'chan:61leiu7j' => '/world/uk', - 'us' => '/world/us', - 'business' => '/business', - 'aerospace' => '/business/aerospace-defense', - 'energy' => '/business/energy', - 'environment' => '/business/environment', - 'chan:8hw7807a' => '/business/healthcare-pharmaceuticals', - 'markets' => '/markets', - 'tech' => '/technology', - 'sports' => '/lifestyle/sports', - 'life' => '/lifestyle', - 'science' => '/lifestyle/science', - 'home/topnews' => '/home', - ); - - const OLD_WIRE_SECTION = array( - 'home/topnews', - 'chan:abtpk0vm', - 'chan:8ym8q8dl', - 'politics', - 'wire' - ); - - /** - * Performs an HTTP request to the Reuters API and returns decoded JSON - * in the form of an associative array - * @param string $feed_uri Full API URL to fetch data - * @return array - */ - private function getJson($uri) - { - $returned_data = getContents($uri); - return json_decode($returned_data, true); - } - - /** - * Takes in data from Reuters Wire API and - * creates structured data in the form of a list - * of story information. - * @param array $data JSON collected from the Reuters Wire API - */ - private function processData($data) - { - /** - * Gets a list of wire items which are groups of templates - */ - $reuters_allowed_wireitems = array_filter( - $data, function ($wireitem) { - return in_array( - $wireitem['wireitem_type'], - self::ALLOWED_WIREITEM_TYPES - ); - } - ); - - /* - * Gets a list of "Templates", which is data containing a story - */ - $reuters_wireitem_templates = array_reduce( - $reuters_allowed_wireitems, - function (array $carry, array $wireitem) { - $wireitem_templates = $wireitem['templates']; - return array_merge( - $carry, - array_filter( - $wireitem_templates, function ( - array $template_data - ) { - return in_array( - $template_data['type'], - self::ALLOWED_TEMPLATE_TYPES - ); - } - ) - ); - }, - array() - ); - - return $reuters_wireitem_templates; - } - - private function getSectionEndpoint() { - $endpoint = $this->getInput('feed'); - if(isset(self::BACKWARD_COMPATIBILITY[$endpoint])) { - $endpoint = self::BACKWARD_COMPATIBILITY[$endpoint]; - } elseif (in_array($endpoint, self::OLD_WIRE_SECTION)) { - $this->useWireAPI = true; - } - return $endpoint; - } - - /** - * @param string $endpoint - A endpoint is provided could be article URI or ID. - * @param string $fetch_type - Provide what kind of fetch do you want? Article or Section. - * @param boolean $is_article_uid {true|false} - A boolean flag to determined if using UID instead of url to fetch. - * @return string A completed API URL to fetch data - */ - private function getAPIURL($endpoint, $fetch_type, $is_article_uid = false) { - $base_url = self::URI . '/pf/api/v3/content/fetch/'; - $wire_url = 'https://wireapi.reuters.com/v8'; - switch($fetch_type) { - case 'article': - if($this->useWireAPI) { - return $wire_url . $endpoint; - } - - $base_query = array( - 'website' => 'reuters', - ); - $query = array(); - - if ($is_article_uid) { - $query = array( - 'id' => $endpoint - ); - } else { - $query = array( - 'website_url' => $endpoint, - ); - } - - $query = array_merge($base_query, $query); - $json_query = json_encode($query); - return $base_url . 'article-by-id-or-url-v1?query=' . $json_query; - break; - case 'section': - if($this->useWireAPI) { - if(strpos($endpoint, 'chan:') !== false) { - // Now checking whether that feed has unique ID or not. - $feed_uri = "/feed/rapp/us/wirefeed/$endpoint"; - } else { - $feed_uri = "/feed/rapp/us/tabbar/feeds/$endpoint"; - } - return $wire_url . $feed_uri; - } - $query = array( - 'section_id' => $endpoint, - 'size' => 30, - 'website' => 'reuters' - ); - - if ($endpoint != '/home') { - $query = array_merge($query, array( - 'fetch_type' => 'section', - )); - } - - $json_query = json_encode($query); - return $base_url . 'articles-by-section-alias-or-id-v1?query=' . $json_query; - break; - } - returnServerError('unsupported endpoint'); - } - - private function addStories($title, $content, $timestamp, $author, $url, $category) { - $item = array(); - $item['categories'] = $category; - $item['author'] = $author; - $item['content'] = $content; - $item['title'] = $title; - $item['timestamp'] = $timestamp; - $item['uri'] = $url; - $this->items[] = $item; - } - - private function getArticle($feed_uri, $is_article_uid = false) - { - // This will make another request to API to get full detail of article and author's name. - $url = $this->getAPIURL($feed_uri, 'article', $is_article_uid); - $rawData = $this->getJson($url); - - if(json_last_error() != JSON_ERROR_NONE) { // Checking whether a valid JSON or not - return $this->handleRedirectedArticle($url); - } - - $article_content = ''; - $authorlist = ''; - $category = array(); - $image_list = array(); - $published_at = ''; - if($this->useWireAPI) { - $reuters_wireitems = $rawData['wireitems']; - $processedData = $this->processData($reuters_wireitems); - - $first = reset($processedData); - $article_content = $first['story']['body_items']; - $authorlist = $first['story']['authors']; - $category = array($first['story']['channel']['name']); - $image_list = $first['story']['images']; - $published_at = $first['story']['published_at']; - } else { - $article_content = $rawData['result']['content_elements']; - $authorlist = $rawData['result']['authors']; - $category = array($rawData['result']['taxonomy']['ads_primary_section']['name']); - $image_list = array(); - if(!empty($rawData['result']['related_content']['galleries'])) { - $galleries = $rawData['result']['related_content']['galleries']; - foreach($galleries as $gallery) { - $image_list = array_merge($image_list, $gallery['content_elements']); - } - } else if(!empty($rawData['result']['related_content']['images'])) { - $image_list = $rawData['result']['related_content']['images']; - } - $published_at = $rawData['result']['published_time']; - } - - $content_detail = array( - 'content' => $this->handleArticleContent($article_content), - 'author' => $this->handleAuthorName($authorlist), - 'category' => $category, - 'images' => $this->handleImage($image_list), - 'published_at' => $published_at - ); - return $content_detail; - } - - private function handleRedirectedArticle($url) { - $html = getSimpleHTMLDOMCached($url, 86400); // Duration 24h - - $description = ''; - $author = ''; - $images = ''; - $meta_items = $html->find('meta'); - foreach($meta_items as $meta) { - switch ($meta->name) { - case 'description': - $description = $meta->content; - break; - case 'author': - case 'twitter:creator': - $author = $meta->content; - break; - case 'twitter:image:src': - case 'twitter:image': - $url = $meta->content; - $images = "<img src=$url" . '>'; - break; - } - } - - return array( - 'content' => $description, - 'author' => $author, - 'category' => '', - 'images' => $images, - 'published_at' => '', - 'status' => 'redirected' - ); - } - - private function handleImage($images) { - $img_placeholder = ''; - - foreach($images as $image) { // Add more image to article. - $image_url = $image['url']; - $image_caption = $image['caption']; - $image_alt_text = ''; - if(isset($image['alt_text'])) { - $image_alt_text = $image['alt_text']; - } else { - $image_alt_text = $image_caption; - } - $img = "<img src=\"$image_url\" alt=\"$image_alt_text\">"; - $img_caption = "<figcaption style=\"text-align: center;\"><i>$image_caption</i></figcaption>"; - $figure = "<figure>$img \t $img_caption</figure>"; - $img_placeholder = $img_placeholder . $figure; - } - - return $img_placeholder; - } - - private function handleAuthorName($authors) { - $author = ''; - $counter = 0; - foreach ($authors as $data) { - //Formatting author's name. - $name = $data['name']; - $counter++; - if($counter == count($authors)) { - $author .= $name; - } else { - $author .= $name . ', '; - } - } - return $author; - } - - private function handleArticleContent($contents) { - $description = ''; - foreach ($contents as $content) { - $data; - if(isset($content['content'])) { - $data = $content['content']; - } - switch($content['type']) { - case 'paragraph': - $description = $description . "<p>$data</p>"; - break; - case 'heading': - $description = $description . "<h3>$data</h3>"; - break; - case 'infographics': - $description = $description . "<img src=\"$data\">"; - break; - case 'inline_items': - $item_list = $content['items']; - $description = $description . '<p>'; - foreach ($item_list as $item) { - if($item['type'] == 'text') { - $description = $description . $item['content']; - } else { - $description = $description . $item['symbol']; - } - } - $description = $description . '</p>'; - break; - case 'p_table': - $description = $description . $content['content']; - break; - case 'upstream_embed': - $media_type = $content['media_type']; - $cid = $content['cid']; - $embed = ''; - switch ($media_type) { - case 'tweet': - try { - $tweet_url = "https://twitter.com/dummyname/statuses/$cid"; - $get_embed_url = 'https://publish.twitter.com/oembed?url=' - . urlencode($tweet_url) . - '&partner=&hide_thread=false'; - $oembed_json = json_decode(getContents($get_embed_url), true); - $embed .= $oembed_json['html']; - } catch (Exception $e) { // In case not found any tweet. - $embed .= ''; - } - break; - case 'instagram': - $url = "https://instagram.com/p/$cid/media/?size=l"; - $embed .= <<<EOD + const MAINTAINER = 'hollowleviathan, spraynard, csisoap'; + const NAME = 'Reuters Bridge'; + const URI = 'https://www.reuters.com'; + const CACHE_TIMEOUT = 1800; // 30min + const DESCRIPTION = 'Returns news from Reuters'; + + private $feedName = self::NAME; + private $useWireAPI = false; + + /** + * Wireitem types allowed in the final story output + */ + const ALLOWED_WIREITEM_TYPES = [ + 'story', + 'headlines' + ]; + + /** + * Wireitem template types allowed in the final story output + */ + const ALLOWED_TEMPLATE_TYPES = [ + 'story', + 'headlines' + ]; + + const PARAMETERS = [ + [ + 'feed' => [ + 'name' => 'News Feed', + 'type' => 'list', + 'title' => 'Feeds from Reuters U.S/International edition', + 'values' => [ + 'Top News' => 'home/topnews', + 'Fact Check' => 'chan:abtpk0vm', + 'Entertainment' => 'chan:8ym8q8dl', + 'Politics' => 'politics', + 'Wire' => 'wire', + 'Breakingviews' => '/breakingviews', + 'World' => [ + 'World' => 'world', + 'Africa' => '/world/africa', + 'Americas' => '/world/americas', + 'Asia-Pacific' => '/world/asia-pacific', + 'China' => 'china', + 'europe' => '/world/europe', + 'India' => '/world/india', + 'Middle East' => '/world/middle-east', + 'UK' => 'chan:61leiu7j', + 'USA News' => 'us', + 'The Great Reboot' => '/world/the-great-reboot', + 'Reuters Next' => '/world/reuters-next' + ], + 'Business' => [ + 'Business' => 'business', + 'Aerospace and Defense' => 'aerospace', + 'Autos Transportation' => '/business/autos-transportation', + 'Energy' => 'energy', + 'Finance' => '/business/finance', + 'Health' => 'chan:8hw7807a', + 'Media Telecom' => '/business/media-telecom', + 'Retail Consumer' => '/business/retail-consumer', + 'Sustainable Business' => '/business/sustainable-business', + 'Change Suite' => '/business/change-suite', + 'Future of Health' => '/business/future-of-health', + 'Future of Money' => '/business/future-of-money', + 'Take Five' => '/business/take-five', + 'Reuters Impact' => '/business/reuters-impact', + ], + 'Legal' => [ + 'Legal' => '/legal', + 'Government' => '/legal/government', + 'Legal Industry' => '/legal/legalindustry', + 'Litigation' => '/legal/litigation', + 'Transactional' => '/legal/transactional', + ], + 'Markets' => [ + 'Markets' => 'markets', + 'Asian Markets' => '/markets/asia', + 'Commodities' => '/markets/commodities', + 'Currencies' => '/markets/currencies', + 'Deals' => '/markets/deals', + 'European Markets' => '/markets/europe', + 'Funds' => '/markets/fund', + 'Global Market Data' => '/markets/global-market-data', + 'Rates & Bonds' => '/markets/rates-bonds', + 'Stocks' => '/markets/stocks', + 'U.S Markets' => '/markets/us', + 'Wealth' => '/markets/wealth', + 'Macro Matters' => '/markets/macromatters', + ], + 'Technology' => [ + 'Technology' => 'tech', + 'Disrupted' => '/technology/disrupted', + 'Reuters Momentum' => '/technology/reuters-momentum', + ], + 'Sports' => [ + 'Sports' => 'sports', + 'Athletics' => '/lifestyle/sports/athletics', + 'Cricket' => '/lifestyle/sports/cricket', + 'Cycling' => '/lifestyle/sports/cycling', + 'Golf' => '/lifestyle/sports/golf', + 'Motor Sports' => '/lifestyle/sports/motor-sports', + 'Soccer' => '/lifestyle/sports/soccer', + 'Tennis' => '/lifestyle/sports/tennis', + ], + 'Lifestyle' => [ + 'Lifestyle' => 'life', + 'Oddly Enough' => '/lifestyle/oddly-enough', + 'Science' => 'science', + ] + ] + ] + ] + ]; + + const BACKWARD_COMPATIBILITY = [ + 'world' => '/world', + 'china' => '/world/china', + 'chan:61leiu7j' => '/world/uk', + 'us' => '/world/us', + 'business' => '/business', + 'aerospace' => '/business/aerospace-defense', + 'energy' => '/business/energy', + 'environment' => '/business/environment', + 'chan:8hw7807a' => '/business/healthcare-pharmaceuticals', + 'markets' => '/markets', + 'tech' => '/technology', + 'sports' => '/lifestyle/sports', + 'life' => '/lifestyle', + 'science' => '/lifestyle/science', + 'home/topnews' => '/home', + ]; + + const OLD_WIRE_SECTION = [ + 'home/topnews', + 'chan:abtpk0vm', + 'chan:8ym8q8dl', + 'politics', + 'wire' + ]; + + /** + * Performs an HTTP request to the Reuters API and returns decoded JSON + * in the form of an associative array + * @param string $feed_uri Full API URL to fetch data + * @return array + */ + private function getJson($uri) + { + $returned_data = getContents($uri); + return json_decode($returned_data, true); + } + + /** + * Takes in data from Reuters Wire API and + * creates structured data in the form of a list + * of story information. + * @param array $data JSON collected from the Reuters Wire API + */ + private function processData($data) + { + /** + * Gets a list of wire items which are groups of templates + */ + $reuters_allowed_wireitems = array_filter( + $data, + function ($wireitem) { + return in_array( + $wireitem['wireitem_type'], + self::ALLOWED_WIREITEM_TYPES + ); + } + ); + + /* + * Gets a list of "Templates", which is data containing a story + */ + $reuters_wireitem_templates = array_reduce( + $reuters_allowed_wireitems, + function (array $carry, array $wireitem) { + $wireitem_templates = $wireitem['templates']; + return array_merge( + $carry, + array_filter( + $wireitem_templates, + function ( + array $template_data + ) { + return in_array( + $template_data['type'], + self::ALLOWED_TEMPLATE_TYPES + ); + } + ) + ); + }, + [] + ); + + return $reuters_wireitem_templates; + } + + private function getSectionEndpoint() + { + $endpoint = $this->getInput('feed'); + if (isset(self::BACKWARD_COMPATIBILITY[$endpoint])) { + $endpoint = self::BACKWARD_COMPATIBILITY[$endpoint]; + } elseif (in_array($endpoint, self::OLD_WIRE_SECTION)) { + $this->useWireAPI = true; + } + return $endpoint; + } + + /** + * @param string $endpoint - A endpoint is provided could be article URI or ID. + * @param string $fetch_type - Provide what kind of fetch do you want? Article or Section. + * @param boolean $is_article_uid {true|false} - A boolean flag to determined if using UID instead of url to fetch. + * @return string A completed API URL to fetch data + */ + private function getAPIURL($endpoint, $fetch_type, $is_article_uid = false) + { + $base_url = self::URI . '/pf/api/v3/content/fetch/'; + $wire_url = 'https://wireapi.reuters.com/v8'; + switch ($fetch_type) { + case 'article': + if ($this->useWireAPI) { + return $wire_url . $endpoint; + } + + $base_query = [ + 'website' => 'reuters', + ]; + $query = []; + + if ($is_article_uid) { + $query = [ + 'id' => $endpoint + ]; + } else { + $query = [ + 'website_url' => $endpoint, + ]; + } + + $query = array_merge($base_query, $query); + $json_query = json_encode($query); + return $base_url . 'article-by-id-or-url-v1?query=' . $json_query; + break; + case 'section': + if ($this->useWireAPI) { + if (strpos($endpoint, 'chan:') !== false) { + // Now checking whether that feed has unique ID or not. + $feed_uri = "/feed/rapp/us/wirefeed/$endpoint"; + } else { + $feed_uri = "/feed/rapp/us/tabbar/feeds/$endpoint"; + } + return $wire_url . $feed_uri; + } + $query = [ + 'section_id' => $endpoint, + 'size' => 30, + 'website' => 'reuters' + ]; + + if ($endpoint != '/home') { + $query = array_merge($query, [ + 'fetch_type' => 'section', + ]); + } + + $json_query = json_encode($query); + return $base_url . 'articles-by-section-alias-or-id-v1?query=' . $json_query; + break; + } + returnServerError('unsupported endpoint'); + } + + private function addStories($title, $content, $timestamp, $author, $url, $category) + { + $item = []; + $item['categories'] = $category; + $item['author'] = $author; + $item['content'] = $content; + $item['title'] = $title; + $item['timestamp'] = $timestamp; + $item['uri'] = $url; + $this->items[] = $item; + } + + private function getArticle($feed_uri, $is_article_uid = false) + { + // This will make another request to API to get full detail of article and author's name. + $url = $this->getAPIURL($feed_uri, 'article', $is_article_uid); + $rawData = $this->getJson($url); + + if (json_last_error() != JSON_ERROR_NONE) { // Checking whether a valid JSON or not + return $this->handleRedirectedArticle($url); + } + + $article_content = ''; + $authorlist = ''; + $category = []; + $image_list = []; + $published_at = ''; + if ($this->useWireAPI) { + $reuters_wireitems = $rawData['wireitems']; + $processedData = $this->processData($reuters_wireitems); + + $first = reset($processedData); + $article_content = $first['story']['body_items']; + $authorlist = $first['story']['authors']; + $category = [$first['story']['channel']['name']]; + $image_list = $first['story']['images']; + $published_at = $first['story']['published_at']; + } else { + $article_content = $rawData['result']['content_elements']; + $authorlist = $rawData['result']['authors']; + $category = [$rawData['result']['taxonomy']['ads_primary_section']['name']]; + $image_list = []; + if (!empty($rawData['result']['related_content']['galleries'])) { + $galleries = $rawData['result']['related_content']['galleries']; + foreach ($galleries as $gallery) { + $image_list = array_merge($image_list, $gallery['content_elements']); + } + } elseif (!empty($rawData['result']['related_content']['images'])) { + $image_list = $rawData['result']['related_content']['images']; + } + $published_at = $rawData['result']['published_time']; + } + + $content_detail = [ + 'content' => $this->handleArticleContent($article_content), + 'author' => $this->handleAuthorName($authorlist), + 'category' => $category, + 'images' => $this->handleImage($image_list), + 'published_at' => $published_at + ]; + return $content_detail; + } + + private function handleRedirectedArticle($url) + { + $html = getSimpleHTMLDOMCached($url, 86400); // Duration 24h + + $description = ''; + $author = ''; + $images = ''; + $meta_items = $html->find('meta'); + foreach ($meta_items as $meta) { + switch ($meta->name) { + case 'description': + $description = $meta->content; + break; + case 'author': + case 'twitter:creator': + $author = $meta->content; + break; + case 'twitter:image:src': + case 'twitter:image': + $url = $meta->content; + $images = "<img src=$url" . '>'; + break; + } + } + + return [ + 'content' => $description, + 'author' => $author, + 'category' => '', + 'images' => $images, + 'published_at' => '', + 'status' => 'redirected' + ]; + } + + private function handleImage($images) + { + $img_placeholder = ''; + + foreach ($images as $image) { // Add more image to article. + $image_url = $image['url']; + $image_caption = $image['caption']; + $image_alt_text = ''; + if (isset($image['alt_text'])) { + $image_alt_text = $image['alt_text']; + } else { + $image_alt_text = $image_caption; + } + $img = "<img src=\"$image_url\" alt=\"$image_alt_text\">"; + $img_caption = "<figcaption style=\"text-align: center;\"><i>$image_caption</i></figcaption>"; + $figure = "<figure>$img \t $img_caption</figure>"; + $img_placeholder = $img_placeholder . $figure; + } + + return $img_placeholder; + } + + private function handleAuthorName($authors) + { + $author = ''; + $counter = 0; + foreach ($authors as $data) { + //Formatting author's name. + $name = $data['name']; + $counter++; + if ($counter == count($authors)) { + $author .= $name; + } else { + $author .= $name . ', '; + } + } + return $author; + } + + private function handleArticleContent($contents) + { + $description = ''; + foreach ($contents as $content) { + $data; + if (isset($content['content'])) { + $data = $content['content']; + } + switch ($content['type']) { + case 'paragraph': + $description = $description . "<p>$data</p>"; + break; + case 'heading': + $description = $description . "<h3>$data</h3>"; + break; + case 'infographics': + $description = $description . "<img src=\"$data\">"; + break; + case 'inline_items': + $item_list = $content['items']; + $description = $description . '<p>'; + foreach ($item_list as $item) { + if ($item['type'] == 'text') { + $description = $description . $item['content']; + } else { + $description = $description . $item['symbol']; + } + } + $description = $description . '</p>'; + break; + case 'p_table': + $description = $description . $content['content']; + break; + case 'upstream_embed': + $media_type = $content['media_type']; + $cid = $content['cid']; + $embed = ''; + switch ($media_type) { + case 'tweet': + try { + $tweet_url = "https://twitter.com/dummyname/statuses/$cid"; + $get_embed_url = 'https://publish.twitter.com/oembed?url=' + . urlencode($tweet_url) . + '&partner=&hide_thread=false'; + $oembed_json = json_decode(getContents($get_embed_url), true); + $embed .= $oembed_json['html']; + } catch (Exception $e) { // In case not found any tweet. + $embed .= ''; + } + break; + case 'instagram': + $url = "https://instagram.com/p/$cid/media/?size=l"; + $embed .= <<<EOD <img src="{$url}" alt="instagram-image-$cid" > EOD; - break; - case 'youtube': - $url = "https://www.youtube.com/embed/$cid"; - $embed .= <<<EOD + break; + case 'youtube': + $url = "https://www.youtube.com/embed/$cid"; + $embed .= <<<EOD <iframe width="560" height="315" @@ -477,151 +487,152 @@ EOD; > </iframe> EOD; - break; - } - $description .= $embed; - break; - case 'social_media': - if ($content['sub_type'] == 'twitter') { - $description .= $content['html']; - } - break; - case 'table': - $table = '<table>'; - $theaders = $content['header']; - $tr = '<tr>'; - foreach($theaders as $header) { - $tr .= '<th>' . $header . '</th>'; - } - $tr .= '</tr>'; - $table .= $tr; - $rows = $content['rows']; - foreach($rows as $row) { - $tr = '<tr>'; - foreach($row as $data) { - $tr .= '<td>' . $data . '</td>'; - } - $tr .= '</tr>'; - $table .= $tr; - } - $table .= '</table>'; - $description .= $table; - break; - case 'image': - $description .= $this->handleImage(array($content)); - } - } - - return $description; - } - - /** - * @param array $stories - */ - private function addRelatedStories($stories) { - foreach($stories as $story) { - $story_data = $this->getArticle($story['url']); - $title = $story['caption']; - $url = self::URI . $story['url']; - if(isset($story_data['status']) && $story_data['status'] != 'redirected') { - $article_body = defaultLinkTo($story_data['content'], $this->getURI()); - } else { - $article_body = $story_data['content']; - } - $content = $article_body . $story_data['images']; - $timestamp = $story_data['published_at']; - $category = $story_data['category']; - $author = $story_data['author']; - $this->addStories($title, $content, $timestamp, $author, $url, $category); - } - } - - public function getName() { - return $this->feedName; - } - - public function collectData() - { - $endpoint = $this->getSectionEndpoint(); - $url = $this->getAPIURL($endpoint, 'section'); - $data = $this->getJson($url); - - $stories = array(); - $section_name = ''; - if($this->useWireAPI) { - $reuters_wireitems = $data['wireitems']; - $section_name = $data['wire_name']; - $processedData = $this->processData($reuters_wireitems); - - // Merge all articles from Editor's Highlight section into existing array of templates. - $top_section = reset($processedData); - if ($top_section['type'] == 'headlines') { - $top_section = array_shift($processedData); - $articles = $top_section['headlines']; - $processedData = array_merge($articles, $processedData); - } - $stories = $processedData; - } else { - $section_name = $data['result']['section']['name']; - if(isset($data['arcResult']['articles'])) { - $stories = $data['arcResult']['articles']; - } else { - $stories = $data['result']['articles']; - } - } - $this->feedName = $section_name . ' | Reuters'; - - foreach ($stories as $story) { - $uid = ''; - $author = ''; - $category = array(); - $content = ''; - $title = ''; - $timestamp = ''; - $url = ''; - $article_uri = ''; - $source_type = ''; - if($this->useWireAPI) { - $uid = $story['story']['usn']; - $article_uri = $story['template_action']['api_path']; - $title = $story['story']['hed']; - $url = $story['template_action']['url']; - } else { - $uid = $story['id']; - $url = self::URI . $story['canonical_url']; - $title = $story['title']; - $article_uri = $story['canonical_url']; - $source_type = $story['source']['name']; - if (isset($story['related_stories'])) { - $this->addRelatedStories($story['related_stories']); - } - } - - // Some article cause unexpected behaviour like redirect to another site not API. - // Attempt to check article source type to avoid this. - if(!$this->useWireAPI && $source_type != 'Package') { // Only Reuters PF api have this, Wire don't. - $author = $this->handleAuthorName($story['authors']); - $timestamp = $story['published_time']; - $image_placeholder = ''; - if (isset($story['thumbnail'])) { - $image_placeholder = $this->handleImage(array($story['thumbnail'])); - } - $content = $story['description'] . $image_placeholder; - $category = array($story['primary_section']['name']); - } else { - $content_detail = $this->getArticle($article_uri); - $description = $content_detail['content']; - $description = defaultLinkTo($description, $this->getURI()); - - $author = $content_detail['author']; - $images = $content_detail['images']; - $category = $content_detail['category']; - $content = "$description $images"; - $timestamp = $content_detail['published_at']; - } - - $this->addStories($title, $content, $timestamp, $author, $url, $category); - - } - } + break; + } + $description .= $embed; + break; + case 'social_media': + if ($content['sub_type'] == 'twitter') { + $description .= $content['html']; + } + break; + case 'table': + $table = '<table>'; + $theaders = $content['header']; + $tr = '<tr>'; + foreach ($theaders as $header) { + $tr .= '<th>' . $header . '</th>'; + } + $tr .= '</tr>'; + $table .= $tr; + $rows = $content['rows']; + foreach ($rows as $row) { + $tr = '<tr>'; + foreach ($row as $data) { + $tr .= '<td>' . $data . '</td>'; + } + $tr .= '</tr>'; + $table .= $tr; + } + $table .= '</table>'; + $description .= $table; + break; + case 'image': + $description .= $this->handleImage([$content]); + } + } + + return $description; + } + + /** + * @param array $stories + */ + private function addRelatedStories($stories) + { + foreach ($stories as $story) { + $story_data = $this->getArticle($story['url']); + $title = $story['caption']; + $url = self::URI . $story['url']; + if (isset($story_data['status']) && $story_data['status'] != 'redirected') { + $article_body = defaultLinkTo($story_data['content'], $this->getURI()); + } else { + $article_body = $story_data['content']; + } + $content = $article_body . $story_data['images']; + $timestamp = $story_data['published_at']; + $category = $story_data['category']; + $author = $story_data['author']; + $this->addStories($title, $content, $timestamp, $author, $url, $category); + } + } + + public function getName() + { + return $this->feedName; + } + + public function collectData() + { + $endpoint = $this->getSectionEndpoint(); + $url = $this->getAPIURL($endpoint, 'section'); + $data = $this->getJson($url); + + $stories = []; + $section_name = ''; + if ($this->useWireAPI) { + $reuters_wireitems = $data['wireitems']; + $section_name = $data['wire_name']; + $processedData = $this->processData($reuters_wireitems); + + // Merge all articles from Editor's Highlight section into existing array of templates. + $top_section = reset($processedData); + if ($top_section['type'] == 'headlines') { + $top_section = array_shift($processedData); + $articles = $top_section['headlines']; + $processedData = array_merge($articles, $processedData); + } + $stories = $processedData; + } else { + $section_name = $data['result']['section']['name']; + if (isset($data['arcResult']['articles'])) { + $stories = $data['arcResult']['articles']; + } else { + $stories = $data['result']['articles']; + } + } + $this->feedName = $section_name . ' | Reuters'; + + foreach ($stories as $story) { + $uid = ''; + $author = ''; + $category = []; + $content = ''; + $title = ''; + $timestamp = ''; + $url = ''; + $article_uri = ''; + $source_type = ''; + if ($this->useWireAPI) { + $uid = $story['story']['usn']; + $article_uri = $story['template_action']['api_path']; + $title = $story['story']['hed']; + $url = $story['template_action']['url']; + } else { + $uid = $story['id']; + $url = self::URI . $story['canonical_url']; + $title = $story['title']; + $article_uri = $story['canonical_url']; + $source_type = $story['source']['name']; + if (isset($story['related_stories'])) { + $this->addRelatedStories($story['related_stories']); + } + } + + // Some article cause unexpected behaviour like redirect to another site not API. + // Attempt to check article source type to avoid this. + if (!$this->useWireAPI && $source_type != 'Package') { // Only Reuters PF api have this, Wire don't. + $author = $this->handleAuthorName($story['authors']); + $timestamp = $story['published_time']; + $image_placeholder = ''; + if (isset($story['thumbnail'])) { + $image_placeholder = $this->handleImage([$story['thumbnail']]); + } + $content = $story['description'] . $image_placeholder; + $category = [$story['primary_section']['name']]; + } else { + $content_detail = $this->getArticle($article_uri); + $description = $content_detail['content']; + $description = defaultLinkTo($description, $this->getURI()); + + $author = $content_detail['author']; + $images = $content_detail['images']; + $category = $content_detail['category']; + $content = "$description $images"; + $timestamp = $content_detail['published_at']; + } + + $this->addStories($title, $content, $timestamp, $author, $url, $category); + } + } } diff --git a/bridges/RoadAndTrackBridge.php b/bridges/RoadAndTrackBridge.php index b81b45c2..d666b6bd 100644 --- a/bridges/RoadAndTrackBridge.php +++ b/bridges/RoadAndTrackBridge.php @@ -1,72 +1,71 @@ <?php -class RoadAndTrackBridge extends BridgeAbstract { - const MAINTAINER = 'teromene'; - const NAME = 'Road And Track Bridge'; - const URI = 'https://www.roadandtrack.com/'; - const CACHE_TIMEOUT = 86400; // 24h - const DESCRIPTION = 'Returns the latest news from Road & Track.'; - - public function collectData() { - - $page = getSimpleHTMLDOM(self::URI); - - $limit = 5; - - foreach($page->find('a.enk2x9t2') as $article) { - $this->items[] = $this->fetchArticle($article->href); - - if (count($this->items) >= $limit) { - break; - } - } - } - - private function fixImages($content) { - - $enclosures = array(); - foreach($content->find('img') as $image) { - $image->src = explode('?', $image->getAttribute('data-src'))[0]; - $enclosures[] = $image->src; - } - - foreach($content->find('.embed-image-wrap, .content-lede-image-wrap') as $imgContainer) { - $imgContainer->style = ''; - } - - return $enclosures; - } - - private function fetchArticle($articleLink) { - - $articleLink = self::URI . $articleLink; - $article = getSimpleHTMLDOM($articleLink); - $item = array(); - - $title = $article->find('.content-hed', 0); - if ($title) { - $item['title'] = $title->innertext; - } - - $item['author'] = $article->find('.byline-name', 0)->innertext; - $item['timestamp'] = strtotime($article->find('.content-info-date', 0)->getAttribute('datetime')); - - $content = $article->find('.content-container', 0); - if($content->find('.content-rail', 0) !== null) { - $content->find('.content-rail', 0)->innertext = ''; - } - - $enclosures = $this->fixImages($content); - - $item['enclosures'] = $enclosures; - $item['content'] = $content; - return $item; - - } - - private function getArticleContent($article) { - - return getContents($article->contentUrl); - - } +class RoadAndTrackBridge extends BridgeAbstract +{ + const MAINTAINER = 'teromene'; + const NAME = 'Road And Track Bridge'; + const URI = 'https://www.roadandtrack.com/'; + const CACHE_TIMEOUT = 86400; // 24h + const DESCRIPTION = 'Returns the latest news from Road & Track.'; + + public function collectData() + { + $page = getSimpleHTMLDOM(self::URI); + + $limit = 5; + + foreach ($page->find('a.enk2x9t2') as $article) { + $this->items[] = $this->fetchArticle($article->href); + + if (count($this->items) >= $limit) { + break; + } + } + } + + private function fixImages($content) + { + $enclosures = []; + foreach ($content->find('img') as $image) { + $image->src = explode('?', $image->getAttribute('data-src'))[0]; + $enclosures[] = $image->src; + } + + foreach ($content->find('.embed-image-wrap, .content-lede-image-wrap') as $imgContainer) { + $imgContainer->style = ''; + } + + return $enclosures; + } + + private function fetchArticle($articleLink) + { + $articleLink = self::URI . $articleLink; + $article = getSimpleHTMLDOM($articleLink); + $item = []; + + $title = $article->find('.content-hed', 0); + if ($title) { + $item['title'] = $title->innertext; + } + + $item['author'] = $article->find('.byline-name', 0)->innertext; + $item['timestamp'] = strtotime($article->find('.content-info-date', 0)->getAttribute('datetime')); + + $content = $article->find('.content-container', 0); + if ($content->find('.content-rail', 0) !== null) { + $content->find('.content-rail', 0)->innertext = ''; + } + + $enclosures = $this->fixImages($content); + + $item['enclosures'] = $enclosures; + $item['content'] = $content; + return $item; + } + + private function getArticleContent($article) + { + return getContents($article->contentUrl); + } } diff --git a/bridges/RobinhoodSnacksBridge.php b/bridges/RobinhoodSnacksBridge.php index 0f2eac83..aecc0265 100644 --- a/bridges/RobinhoodSnacksBridge.php +++ b/bridges/RobinhoodSnacksBridge.php @@ -1,113 +1,114 @@ <?php -class RobinhoodSnacksBridge extends BridgeAbstract { - const MAINTAINER = 'johnpc'; - const NAME = 'Robinhood Snacks Newsletter'; - const URI = 'https://snacks.robinhood.com/newsletters/'; - const CACHE_TIMEOUT = 86400; // 24h - const DESCRIPTION = 'Returns newsletters from Robinhood Snacks'; - - // Work around 403 by pretending to be a legit browser - const FAKE_HEADERS = array( - 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0', - 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', - 'Accept-Language: es-ES,en-US;q=0.7,en;q=0.3', - 'Accept-Encoding: gzip, deflate, br', - 'Connection: keep-alive', - 'Upgrade-Insecure-Requests: 1', - 'Sec-Fetch-Dest: document', - 'Sec-Fetch-Mode: navigate', - 'Sec-Fetch-Site: none', - 'Sec-Fetch-User: ?1', - 'Pragma: no-cache', - 'Cache-Control: no-cache', - 'TE: trailers' - ); - - public function collectData() - { - $html = getSimpleHTMLDOM(self::URI, self::FAKE_HEADERS); - $html = defaultLinkTo($html, $this->getURI()); - - $elements = $html->find('#__next > div > div > div > div > a'); - - foreach ($elements as $element) { - if ($element->href === 'https://snacks.robinhood.com/newsletters/page/2/') { - continue; - } - - $content = $element->find('div > div', 2); - - // Remove element that is not parsed (span with weekly tag) - $unwanted_selector = 'span'; - foreach($content->find($unwanted_selector) as $found) { - $found->outertext = ''; - } - - $title = $content->find('div', 0)->innertext; - $timestamp = strtotime($content->find('div', 1)->innertext); - $uri = $element->href; - - $this->items[] = array( - 'uri' => $uri, - 'title' => $title, - 'timestamp' => $timestamp, - 'content' => self::getArticleContent($uri) - ); - } - } - - private function getArticleContent($uri) - { - $article_html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT, self::FAKE_HEADERS); - if(!$article_html) { - return ''; - } - - $content = $article_html->find('#__next > div > div > div > span', 0); - $content->removeChild($content->find('div', 0)); - $content->removeChild($content->find('h1', 0)); - $content->removeChild($content->find('img', 1)); - - // Remove elements that are not part of article content - $unwanted_selector = 'style'; - foreach($content->find($unwanted_selector) as $found) { - $found->outertext = ''; - } - - // Images cleanup - $already_displayed_pictures = array(); - foreach($content->find('img') as $found) { - // Skip loader images - if (str_contains($found->src, 'data:image/gif;base64')) { - $found->outertext = ''; - continue; - } - - // Skip multiple images with same src - // and remove duplicated image description - if (in_array($found->src, $already_displayed_pictures)) { - $found->parent->parent->parent->outertext = ''; - $found->parent->parent->parent->nextSibling()->nextSibling()->outertext = ''; - continue; - } - - // Remove srcset attribute - $found->removeAttribute('srcset'); - - // If relative img, fix path - if (str_starts_with($found->src, '/_next')) { - $found->setAttribute('src', 'https://snacks.robinhood.com' . $found->getAttribute('src')); - } - - $already_displayed_pictures[] = $found->src; - } - - $content_text = $content->innertext; - - // Remove noscript tag to display images - $content_text = str_replace('<noscript>', '', $content_text); - - return $content_text; - } +class RobinhoodSnacksBridge extends BridgeAbstract +{ + const MAINTAINER = 'johnpc'; + const NAME = 'Robinhood Snacks Newsletter'; + const URI = 'https://snacks.robinhood.com/newsletters/'; + const CACHE_TIMEOUT = 86400; // 24h + const DESCRIPTION = 'Returns newsletters from Robinhood Snacks'; + + // Work around 403 by pretending to be a legit browser + const FAKE_HEADERS = [ + 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0', + 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language: es-ES,en-US;q=0.7,en;q=0.3', + 'Accept-Encoding: gzip, deflate, br', + 'Connection: keep-alive', + 'Upgrade-Insecure-Requests: 1', + 'Sec-Fetch-Dest: document', + 'Sec-Fetch-Mode: navigate', + 'Sec-Fetch-Site: none', + 'Sec-Fetch-User: ?1', + 'Pragma: no-cache', + 'Cache-Control: no-cache', + 'TE: trailers' + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI, self::FAKE_HEADERS); + $html = defaultLinkTo($html, $this->getURI()); + + $elements = $html->find('#__next > div > div > div > div > a'); + + foreach ($elements as $element) { + if ($element->href === 'https://snacks.robinhood.com/newsletters/page/2/') { + continue; + } + + $content = $element->find('div > div', 2); + + // Remove element that is not parsed (span with weekly tag) + $unwanted_selector = 'span'; + foreach ($content->find($unwanted_selector) as $found) { + $found->outertext = ''; + } + + $title = $content->find('div', 0)->innertext; + $timestamp = strtotime($content->find('div', 1)->innertext); + $uri = $element->href; + + $this->items[] = [ + 'uri' => $uri, + 'title' => $title, + 'timestamp' => $timestamp, + 'content' => self::getArticleContent($uri) + ]; + } + } + + private function getArticleContent($uri) + { + $article_html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT, self::FAKE_HEADERS); + if (!$article_html) { + return ''; + } + + $content = $article_html->find('#__next > div > div > div > span', 0); + $content->removeChild($content->find('div', 0)); + $content->removeChild($content->find('h1', 0)); + $content->removeChild($content->find('img', 1)); + + // Remove elements that are not part of article content + $unwanted_selector = 'style'; + foreach ($content->find($unwanted_selector) as $found) { + $found->outertext = ''; + } + + // Images cleanup + $already_displayed_pictures = []; + foreach ($content->find('img') as $found) { + // Skip loader images + if (str_contains($found->src, 'data:image/gif;base64')) { + $found->outertext = ''; + continue; + } + + // Skip multiple images with same src + // and remove duplicated image description + if (in_array($found->src, $already_displayed_pictures)) { + $found->parent->parent->parent->outertext = ''; + $found->parent->parent->parent->nextSibling()->nextSibling()->outertext = ''; + continue; + } + + // Remove srcset attribute + $found->removeAttribute('srcset'); + + // If relative img, fix path + if (str_starts_with($found->src, '/_next')) { + $found->setAttribute('src', 'https://snacks.robinhood.com' . $found->getAttribute('src')); + } + + $already_displayed_pictures[] = $found->src; + } + + $content_text = $content->innertext; + + // Remove noscript tag to display images + $content_text = str_replace('<noscript>', '', $content_text); + + return $content_text; + } } diff --git a/bridges/RoosterTeethBridge.php b/bridges/RoosterTeethBridge.php index 9b85de53..ab4d12ce 100644 --- a/bridges/RoosterTeethBridge.php +++ b/bridges/RoosterTeethBridge.php @@ -1,105 +1,106 @@ <?php -class RoosterTeethBridge extends BridgeAbstract { +class RoosterTeethBridge extends BridgeAbstract +{ + const MAINTAINER = 'tgkenney'; + const NAME = 'Rooster Teeth'; + const URI = 'https://roosterteeth.com'; + const DESCRIPTION = 'Gets the latest channel videos from the Rooster Teeth website'; + const API = 'https://svod-be.roosterteeth.com/'; - const MAINTAINER = 'tgkenney'; - const NAME = 'Rooster Teeth'; - const URI = 'https://roosterteeth.com'; - const DESCRIPTION = 'Gets the latest channel videos from the Rooster Teeth website'; - const API = 'https://svod-be.roosterteeth.com/'; + const PARAMETERS = [ + 'Options' => [ + 'channel' => [ + 'type' => 'list', + 'name' => 'Channel', + 'title' => 'Select a channel to filter by', + 'values' => [ + 'All channels' => 'all', + 'Achievement Hunter' => 'achievement-hunter', + 'Cow Chop' => 'cow-chop', + 'Death Battle' => 'death-battle', + 'Funhaus' => 'funhaus', + 'Inside Gaming' => 'inside-gaming', + 'JT Music' => 'jt-music', + 'Kinda Funny' => 'kinda-funny', + 'Rooster Teeth' => 'rooster-teeth', + 'Sugar Pine 7' => 'sugar-pine-7' + ] + ], + 'sort' => [ + 'type' => 'list', + 'name' => 'Sort', + 'title' => 'Select a sort order', + 'values' => [ + 'Newest -> Oldest' => 'desc', + 'Oldest -> Newest' => 'asc' + ], + 'defaultValue' => 'desc' + ], + 'first' => [ + 'type' => 'list', + 'name' => 'RoosterTeeth First', + 'title' => 'Select whether to include "First" videos before they are public', + 'values' => [ + 'True' => true, + 'False' => false + ] + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Maximum number of items to return', + 'defaultValue' => 10 + ] + ] + ]; - const PARAMETERS = array( - 'Options' => array( - 'channel' => array( - 'type' => 'list', - 'name' => 'Channel', - 'title' => 'Select a channel to filter by', - 'values' => array( - 'All channels' => 'all', - 'Achievement Hunter' => 'achievement-hunter', - 'Cow Chop' => 'cow-chop', - 'Death Battle' => 'death-battle', - 'Funhaus' => 'funhaus', - 'Inside Gaming' => 'inside-gaming', - 'JT Music' => 'jt-music', - 'Kinda Funny' => 'kinda-funny', - 'Rooster Teeth' => 'rooster-teeth', - 'Sugar Pine 7' => 'sugar-pine-7' - ) - ), - 'sort' => array( - 'type' => 'list', - 'name' => 'Sort', - 'title' => 'Select a sort order', - 'values' => array( - 'Newest -> Oldest' => 'desc', - 'Oldest -> Newest' => 'asc' - ), - 'defaultValue' => 'desc' - ), - 'first' => array( - 'type' => 'list', - 'name' => 'RoosterTeeth First', - 'title' => 'Select whether to include "First" videos before they are public', - 'values' => array( - 'True' => true, - 'False' => false - ) - ), - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Maximum number of items to return', - 'defaultValue' => 10 - ) - ) - ); + public function collectData() + { + if ($this->getInput('channel') !== 'all') { + $uri = self::API + . 'api/v1/episodes?per_page=' + . $this->getInput('limit') + . '&channel_id=' + . $this->getInput('channel') + . '&order=' . $this->getInput('sort') + . '&page=1'; - public function collectData() { - if ($this->getInput('channel') !== 'all') { - $uri = self::API - . 'api/v1/episodes?per_page=' - . $this->getInput('limit') - . '&channel_id=' - . $this->getInput('channel') - . '&order=' . $this->getInput('sort') - . '&page=1'; + $htmlJSON = getSimpleHTMLDOM($uri); + } else { + $uri = self::API + . '/api/v1/episodes?per_page=' + . $this->getInput('limit') + . '&filter=all&order=' + . $this->getInput('sort') + . '&page=1'; - $htmlJSON = getSimpleHTMLDOM($uri); - } else { - $uri = self::API - . '/api/v1/episodes?per_page=' - . $this->getInput('limit') - . '&filter=all&order=' - . $this->getInput('sort') - . '&page=1'; + $htmlJSON = getSimpleHTMLDOM($uri); + } - $htmlJSON = getSimpleHTMLDOM($uri); - } + $htmlArray = json_decode($htmlJSON, true); - $htmlArray = json_decode($htmlJSON, true); + foreach ($htmlArray['data'] as $key => $value) { + $item = []; - foreach($htmlArray['data'] as $key => $value) { - $item = array(); + if (!$this->getInput('first') && $value['attributes']['is_sponsors_only']) { + continue; + } - if (!$this->getInput('first') && $value['attributes']['is_sponsors_only']) { - continue; - } + $publicDate = date_create($value['attributes']['member_golive_at']); + $dateDiff = date_diff($publicDate, date_create(), false); - $publicDate = date_create($value['attributes']['member_golive_at']); - $dateDiff = date_diff($publicDate, date_create(), false); + if (!$this->getInput('first') && $dateDiff->invert == 1) { + continue; + } - if (!$this->getInput('first') && $dateDiff->invert == 1) { - continue; - } + $item['uri'] = self::URI . $value['canonical_links']['self']; + $item['title'] = $value['attributes']['title']; + $item['timestamp'] = $value['attributes']['member_golive_at']; + $item['author'] = $value['attributes']['show_title']; - $item['uri'] = self::URI . $value['canonical_links']['self']; - $item['title'] = $value['attributes']['title']; - $item['timestamp'] = $value['attributes']['member_golive_at']; - $item['author'] = $value['attributes']['show_title']; - - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/RtsBridge.php b/bridges/RtsBridge.php index 002e4d6a..1253ea3c 100644 --- a/bridges/RtsBridge.php +++ b/bridges/RtsBridge.php @@ -1,76 +1,77 @@ <?php -class RtsBridge extends BridgeAbstract { - const NAME = 'Radio Télévision Suisse'; - const URI = 'https://www.rts.ch/'; - const MAINTAINER = 'imagoiq'; - const DESCRIPTION = 'Returns newest videos from RTS'; +class RtsBridge extends BridgeAbstract +{ + const NAME = 'Radio Télévision Suisse'; + const URI = 'https://www.rts.ch/'; + const MAINTAINER = 'imagoiq'; + const DESCRIPTION = 'Returns newest videos from RTS'; - const PARAMETERS = array( - 'ID de l\'émission' => array( - 'idShow' => array( - 'name' => 'Show id', - 'required' => true, - 'exampleValue' => 385418, - 'title' => 'ex. 385418 pour + const PARAMETERS = [ + 'ID de l\'émission' => [ + 'idShow' => [ + 'name' => 'Show id', + 'required' => true, + 'exampleValue' => 385418, + 'title' => 'ex. 385418 pour https://www.rts.ch/play/tv/emission/a-bon-entendeur?id=385418' - ) - ), - 'ID de la section' => array( - 'idSection' => array( - 'name' => 'Section id', - 'required' => true, - 'exampleValue' => 'ce802a54-8877-49cc-acd6-8d244762829b', - 'title' => 'ex. ce802a54-8877-49cc-acd6-8d244762829b pour + ] + ], + 'ID de la section' => [ + 'idSection' => [ + 'name' => 'Section id', + 'required' => true, + 'exampleValue' => 'ce802a54-8877-49cc-acd6-8d244762829b', + 'title' => 'ex. ce802a54-8877-49cc-acd6-8d244762829b pour https://www.rts.ch/play/tv/detail/humour?id=ce802a54-8877-49cc-acd6-8d244762829b' - ) - ) - ); + ] + ] + ]; - public function collectData(){ - switch($this->queriedContext) { - case 'ID de l\'émission': - $showId = $this->getInput('idShow'); + public function collectData() + { + switch ($this->queriedContext) { + case 'ID de l\'émission': + $showId = $this->getInput('idShow'); - $url = 'https://www.rts.ch/play/v3/api/rts/production/videos-by-show-id?showId=' - . $showId; - break; - case 'ID de la section': - $sectionId = $this->getInput('idSection'); + $url = 'https://www.rts.ch/play/v3/api/rts/production/videos-by-show-id?showId=' + . $showId; + break; + case 'ID de la section': + $sectionId = $this->getInput('idSection'); - $url = 'https://www.rts.ch/play/v3/api/rts/production/media-section?sectionId=' - . $sectionId; - break; - } + $url = 'https://www.rts.ch/play/v3/api/rts/production/media-section?sectionId=' + . $sectionId; + break; + } - $header = array(); - $input = getContents($url, $header); - $input_json = json_decode($input, true); + $header = []; + $input = getContents($url, $header); + $input_json = json_decode($input, true); - foreach($input_json['data']['data'] as $element) { + foreach ($input_json['data']['data'] as $element) { + $item = []; + $item['uri'] = 'https://www.rts.ch/play/tv/-/video/-?urn=' . $element['urn']; + $item['uid'] = $element['id']; - $item = array(); - $item['uri'] = 'https://www.rts.ch/play/tv/-/video/-?urn=' . $element['urn']; - $item['uid'] = $element['id']; + $item['timestamp'] = strtotime($element['date']); + $item['title'] = $element['show']['title'] . ' - ' . $element['title']; - $item['timestamp'] = strtotime($element['date']); - $item['title'] = $element['show']['title'] . ' - ' . $element['title']; + $item['duration'] = round((int)$element['duration'] / 60000); + $durationInHour = date('g\hi', mktime(0, $item['duration'])); + $durationInMin = date('i\m\i\n', mktime(0, $item['duration'])); + $durationText = $item['duration'] > 60 ? $durationInHour : $durationInMin; - $item['duration'] = round((int)$element['duration'] / 60000); - $durationInHour = date('g\hi', mktime(0, $item['duration'])); - $durationInMin = date('i\m\i\n', mktime(0, $item['duration'])); - $durationText = $item['duration'] > 60 ? $durationInHour : $durationInMin; + $item['content'] = $element['description'] + . '<br/><br/>' + . $durationText + . '<br><a href="' + . $item['uri'] + . '"><img src="' + . $element['imageUrl'] + . '/scale/width/700" alt=""/></a>'; - $item['content'] = $element['description'] - . '<br/><br/>' - . $durationText - . '<br><a href="' - . $item['uri'] - . '"><img src="' - . $element['imageUrl'] - . '/scale/width/700" alt=""/></a>'; - - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/Rue89Bridge.php b/bridges/Rue89Bridge.php index c6038448..9b446e44 100644 --- a/bridges/Rue89Bridge.php +++ b/bridges/Rue89Bridge.php @@ -1,47 +1,46 @@ <?php -class Rue89Bridge extends BridgeAbstract { - const MAINTAINER = 'teromene'; - const NAME = 'Rue89'; - const URI = 'https://www.nouvelobs.com/rue89/'; - const DESCRIPTION = 'Returns the newest posts from Rue89'; - - public function collectData() { - - $jsonArticles = getContents('https://appdata.nouvelobs.com/rue89/feed.json'); - $articles = json_decode($jsonArticles)->items; - foreach($articles as $article) { - $this->items[] = $this->getArticle($article); - } - - } - - private function getArticle($articleInfo) { - - $articleJson = getContents($articleInfo->json_url); - $article = json_decode($articleJson); - $item = array(); - $item['title'] = $article->title; - $item['uri'] = $article->url; - if($article->content_premium !== null) { - $item['content'] = $article->content_premium; - } else { - $item['content'] = $article->content; - } - $item['timestamp'] = $article->date_publi; - $item['author'] = $article->author->show_name; - - $item['enclosures'] = array(); - foreach($article->images as $image) { - $item['enclosures'][] = $image->url; - } - - $item['categories'] = array(); - foreach($article->categories as $category) { - $item['categories'][] = $category->title; - } - - return $item; - - } +class Rue89Bridge extends BridgeAbstract +{ + const MAINTAINER = 'teromene'; + const NAME = 'Rue89'; + const URI = 'https://www.nouvelobs.com/rue89/'; + const DESCRIPTION = 'Returns the newest posts from Rue89'; + + public function collectData() + { + $jsonArticles = getContents('https://appdata.nouvelobs.com/rue89/feed.json'); + $articles = json_decode($jsonArticles)->items; + foreach ($articles as $article) { + $this->items[] = $this->getArticle($article); + } + } + + private function getArticle($articleInfo) + { + $articleJson = getContents($articleInfo->json_url); + $article = json_decode($articleJson); + $item = []; + $item['title'] = $article->title; + $item['uri'] = $article->url; + if ($article->content_premium !== null) { + $item['content'] = $article->content_premium; + } else { + $item['content'] = $article->content; + } + $item['timestamp'] = $article->date_publi; + $item['author'] = $article->author->show_name; + + $item['enclosures'] = []; + foreach ($article->images as $image) { + $item['enclosures'][] = $image->url; + } + + $item['categories'] = []; + foreach ($article->categories as $category) { + $item['categories'][] = $category->title; + } + + return $item; + } } diff --git a/bridges/Rule34Bridge.php b/bridges/Rule34Bridge.php index 5c8ddc93..05241fb8 100644 --- a/bridges/Rule34Bridge.php +++ b/bridges/Rule34Bridge.php @@ -1,10 +1,9 @@ <?php -class Rule34Bridge extends GelbooruBridge { - - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Rule34'; - const URI = 'https://rule34.xxx/'; - const DESCRIPTION = 'Returns images from given page'; - +class Rule34Bridge extends GelbooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Rule34'; + const URI = 'https://rule34.xxx/'; + const DESCRIPTION = 'Returns images from given page'; } diff --git a/bridges/Rule34pahealBridge.php b/bridges/Rule34pahealBridge.php index 37e216df..e738ed69 100644 --- a/bridges/Rule34pahealBridge.php +++ b/bridges/Rule34pahealBridge.php @@ -1,28 +1,29 @@ <?php -class Rule34pahealBridge extends Shimmie2Bridge { +class Rule34pahealBridge extends Shimmie2Bridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Rule34paheal'; + const URI = 'https://rule34.paheal.net/'; + const DESCRIPTION = 'Returns images from given page'; - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Rule34paheal'; - const URI = 'https://rule34.paheal.net/'; - const DESCRIPTION = 'Returns images from given page'; + const PATHTODATA = '.shm-thumb'; - const PATHTODATA = '.shm-thumb'; - - protected function getItemFromElement($element){ - $item = array(); - $item['uri'] = rtrim($this->getURI(), '/') . $element->find('.shm-thumb-link', 0)->href; - $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); - $item['timestamp'] = time(); - $thumbnailUri = $element->find('a', 1)->href; - $item['categories'] = explode(' ', $element->getAttribute('data-tags')); - $item['title'] = $this->getName() . ' | ' . $item['id']; - $item['content'] = '<a href="' - . $item['uri'] - . '"><img src="' - . $thumbnailUri - . '" /></a><br>Tags: ' - . $element->getAttribute('data-tags'); - return $item; - } + protected function getItemFromElement($element) + { + $item = []; + $item['uri'] = rtrim($this->getURI(), '/') . $element->find('.shm-thumb-link', 0)->href; + $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); + $item['timestamp'] = time(); + $thumbnailUri = $element->find('a', 1)->href; + $item['categories'] = explode(' ', $element->getAttribute('data-tags')); + $item['title'] = $this->getName() . ' | ' . $item['id']; + $item['content'] = '<a href="' + . $item['uri'] + . '"><img src="' + . $thumbnailUri + . '" /></a><br>Tags: ' + . $element->getAttribute('data-tags'); + return $item; + } } diff --git a/bridges/RutubeBridge.php b/bridges/RutubeBridge.php index f4bfcdb4..6e2559d2 100644 --- a/bridges/RutubeBridge.php +++ b/bridges/RutubeBridge.php @@ -1,91 +1,98 @@ <?php -class RutubeBridge extends BridgeAbstract { - const NAME = 'Rutube'; - const URI = 'https://rutube.ru'; - const MAINTAINER = 'em92'; - const DESCRIPTION = 'Выводит ленту видео'; +class RutubeBridge extends BridgeAbstract +{ + const NAME = 'Rutube'; + const URI = 'https://rutube.ru'; + const MAINTAINER = 'em92'; + const DESCRIPTION = 'Выводит ленту видео'; - const PARAMETERS = array( - 'По каналу' => array( - 'c' => array( - 'name' => 'ИД канала', - 'exampleValue' => 1342940, // Мятежник Джек - 'type' => 'number', - 'required' => true - ), - ), - 'По плейлисту' => array( - 'p' => array( - 'name' => 'ИД плейлиста', - 'exampleValue' => 83641, // QRUSH - 'type' => 'number', - 'required' => true - ), - ), - ); + const PARAMETERS = [ + 'По каналу' => [ + 'c' => [ + 'name' => 'ИД канала', + 'exampleValue' => 1342940, // Мятежник Джек + 'type' => 'number', + 'required' => true + ], + ], + 'По плейлисту' => [ + 'p' => [ + 'name' => 'ИД плейлиста', + 'exampleValue' => 83641, // QRUSH + 'type' => 'number', + 'required' => true + ], + ], + ]; - protected $title; + protected $title; - public function getURI() { - if ($this->getInput('c')) { - return self::URI . '/channel/' . strval($this->getInput('c')) . '/videos/'; - } else if ($this->getInput('p')) { - return self::URI . '/plst/' . strval($this->getInput('p')) . '/'; - } else { - return parent::getURI(); - } - } + public function getURI() + { + if ($this->getInput('c')) { + return self::URI . '/channel/' . strval($this->getInput('c')) . '/videos/'; + } elseif ($this->getInput('p')) { + return self::URI . '/plst/' . strval($this->getInput('p')) . '/'; + } else { + return parent::getURI(); + } + } - public function getIcon() { - return 'https://static.rutube.ru/static/favicon.ico'; - } + public function getIcon() + { + return 'https://static.rutube.ru/static/favicon.ico'; + } - public function getName() { - if (is_null($this->title)) { - return parent::getName(); - } else { - return $this->title . ' - ' . parent::getName(); - } - } + public function getName() + { + if (is_null($this->title)) { + return parent::getName(); + } else { + return $this->title . ' - ' . parent::getName(); + } + } - private function getJSONData($html) { - $jsonDataRegex = '/window.reduxState = (.*?);/'; - preg_match($jsonDataRegex, $html, $matches) or returnServerError('Could not find reduxState'); - return json_decode(str_replace('\x', '\\\x', $matches[1])); - } + private function getJSONData($html) + { + $jsonDataRegex = '/window.reduxState = (.*?);/'; + preg_match($jsonDataRegex, $html, $matches) or returnServerError('Could not find reduxState'); + return json_decode(str_replace('\x', '\\\x', $matches[1])); + } - public function collectData(){ - $link = $this->getURI(); + public function collectData() + { + $link = $this->getURI(); - $html = getContents($link); - $reduxState = $this->getJSONData($html); - $videos = []; - if ($this->getInput('c')) { - $videos = $reduxState->userChannel->videos->results; - $this->title = $reduxState->userChannel->info->name; - } else if ($this->getInput('p')) { - $videos = $reduxState->playlist->data->results; - $this->title = $reduxState->playlist->title; - } + $html = getContents($link); + $reduxState = $this->getJSONData($html); + $videos = []; + if ($this->getInput('c')) { + $videos = $reduxState->userChannel->videos->results; + $this->title = $reduxState->userChannel->info->name; + } elseif ($this->getInput('p')) { + $videos = $reduxState->playlist->data->results; + $this->title = $reduxState->playlist->title; + } - foreach($videos as $video) { - $item = new FeedItem(); - $item->setTitle($video->title); - $item->setURI($video->video_url); - $content = '<a href="' . $item->getURI() . '">'; - $content .= '<img src="' . $video->thumbnail_url . '" />'; - $content .= '</a><br/>'; - $content .= nl2br( - // Converting links in plaintext - // Copied from https://stackoverflow.com/a/12590772 - preg_replace( - '$(https?://[a-z0-9_./?=&#-]+)(?![^<>]*>)$i', ' <a href="$1" target="_blank">$1</a> ', - $video->description . ' ' - ) - ); - $item->setContent($content); - $this->items[] = $item; - } - } + foreach ($videos as $video) { + $item = new FeedItem(); + $item->setTitle($video->title); + $item->setURI($video->video_url); + $content = '<a href="' . $item->getURI() . '">'; + $content .= '<img src="' . $video->thumbnail_url . '" />'; + $content .= '</a><br/>'; + $content .= nl2br( + // Converting links in plaintext + // Copied from https://stackoverflow.com/a/12590772 + preg_replace( + '$(https?://[a-z0-9_./?=&#-]+)(?![^<>]*>)$i', + ' <a href="$1" target="_blank">$1</a> ', + $video->description . ' ' + ) + ); + $item->setContent($content); + $this->items[] = $item; + } + } } diff --git a/bridges/SIMARBridge.php b/bridges/SIMARBridge.php index df79b1d9..8b9cd31a 100644 --- a/bridges/SIMARBridge.php +++ b/bridges/SIMARBridge.php @@ -1,62 +1,65 @@ <?php -class SIMARBridge extends BridgeAbstract { - const NAME = 'SIMAR'; - const URI = 'http://www.simar-louresodivelas.pt/'; - const DESCRIPTION = 'Verificar estado da rede SIMAR'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - 'Público' => array( - 'interventions' => array( - 'type' => 'checkbox', - 'name' => 'Incluir Intervenções?', - 'defaultValue' => 'checked', - ) - ) - ); - - public function collectData() { - $html = getSimpleHTMLDOM(self::getURI()); - $e_home = $html->find('#home', 0) - or returnServerError('Invalid site structure'); - - foreach($e_home->find('span') as $element) { - $item = array(); - - $item['title'] = 'Rotura: ' . $element->plaintext; - $item['content'] = $element->innertext; - $item['uid'] = 'urn:sha1:' . hash('sha1', $item['content']); - - $this->items[] = $item; - } - - if ($this->getInput('interventions')) { - $e_main1 = $html->find('#menu1', 0) - or returnServerError('Invalid site structure'); - - foreach ($e_main1->find('a') as $element) { - $item = array(); - - $item['title'] = 'Intervenção: ' . $element->plaintext; - $item['uri'] = self::getURI() . $element->href; - $item['content'] = $element->innertext; - - /* Try to get the actual contents for this kind of item */ - $item_html = getSimpleHTMLDOMCached($item['uri']); - if ($item_html) { - $e_item = $item_html->find('.auto-style59', 0); - foreach($e_item->find('p') as $paragraph) { - /* Remove empty paragraphs */ - if (preg_match('/^(\W| )+$/', $paragraph->innertext) == 1) { - $paragraph->outertext = ''; - } - } - if ($e_item) { - $item['content'] = $e_item->innertext; - } - } - - $this->items[] = $item; - } - } - } + +class SIMARBridge extends BridgeAbstract +{ + const NAME = 'SIMAR'; + const URI = 'http://www.simar-louresodivelas.pt/'; + const DESCRIPTION = 'Verificar estado da rede SIMAR'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + 'Público' => [ + 'interventions' => [ + 'type' => 'checkbox', + 'name' => 'Incluir Intervenções?', + 'defaultValue' => 'checked', + ] + ] + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM(self::getURI()); + $e_home = $html->find('#home', 0) + or returnServerError('Invalid site structure'); + + foreach ($e_home->find('span') as $element) { + $item = []; + + $item['title'] = 'Rotura: ' . $element->plaintext; + $item['content'] = $element->innertext; + $item['uid'] = 'urn:sha1:' . hash('sha1', $item['content']); + + $this->items[] = $item; + } + + if ($this->getInput('interventions')) { + $e_main1 = $html->find('#menu1', 0) + or returnServerError('Invalid site structure'); + + foreach ($e_main1->find('a') as $element) { + $item = []; + + $item['title'] = 'Intervenção: ' . $element->plaintext; + $item['uri'] = self::getURI() . $element->href; + $item['content'] = $element->innertext; + + /* Try to get the actual contents for this kind of item */ + $item_html = getSimpleHTMLDOMCached($item['uri']); + if ($item_html) { + $e_item = $item_html->find('.auto-style59', 0); + foreach ($e_item->find('p') as $paragraph) { + /* Remove empty paragraphs */ + if (preg_match('/^(\W| )+$/', $paragraph->innertext) == 1) { + $paragraph->outertext = ''; + } + } + if ($e_item) { + $item['content'] = $e_item->innertext; + } + } + + $this->items[] = $item; + } + } + } } diff --git a/bridges/SafebooruBridge.php b/bridges/SafebooruBridge.php index 0ca7a45d..b0ebea19 100644 --- a/bridges/SafebooruBridge.php +++ b/bridges/SafebooruBridge.php @@ -1,15 +1,16 @@ <?php -class SafebooruBridge extends GelbooruBridge { +class SafebooruBridge extends GelbooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Safebooru'; + const URI = 'https://safebooru.org/'; + const DESCRIPTION = 'Returns images from given page'; - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Safebooru'; - const URI = 'https://safebooru.org/'; - const DESCRIPTION = 'Returns images from given page'; - - protected function buildThumbnailURI($element){ - $regex = '/\.\w+$/'; - return $this->getURI() . 'thumbnails/' . $element->directory - . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image); - } + protected function buildThumbnailURI($element) + { + $regex = '/\.\w+$/'; + return $this->getURI() . 'thumbnails/' . $element->directory + . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image); + } } diff --git a/bridges/SchweinfurtBuergerinformationenBridge.php b/bridges/SchweinfurtBuergerinformationenBridge.php index 1cee949a..c7c935fd 100644 --- a/bridges/SchweinfurtBuergerinformationenBridge.php +++ b/bridges/SchweinfurtBuergerinformationenBridge.php @@ -1,121 +1,132 @@ <?php -class SchweinfurtBuergerinformationenBridge extends BridgeAbstract { - const MAINTAINER = 'mibe'; - const NAME = 'Schweinfurt Bürgerinformationen'; - const URI = 'https://www.schweinfurt.de/rathaus-politik/pressestelle/buergerinformationen/index.html'; - const ARTICLE_URI = 'https://www.schweinfurt.de/rathaus-politik/pressestelle/buergerinformationen/%d.html'; - const INDEX_CACHE_TIMEOUT = 10800; // 3h - const ARTICLE_CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Returns the latest news for citizens of Schweinfurt'; - const PARAMETERS = array( - array( - 'pages' => array( - 'name' => 'Number of pages', - 'type' => 'number', - 'title' => 'Specifies the number of pages to fetch. Usually one or two are enough.', - 'exampleValue' => '1', - 'defaultValue' => '1', - ) - ) - ); - - public function getIcon() - { - return 'https://www.schweinfurt.de/__/images/favicon.ico'; - } - - public function collectData() - { - // Get number of pages to retrieve. One page is the minimum. - $pages = $this->getInput('pages'); - if (!is_int($pages) || $pages < 1) - $pages = 1; - - $articleIDs = array(); - - for($page = 0; $page < $pages; $page++) { - $newIDs = $this->getArticleIDsFromPage($page); - $articleIDs = array_merge($articleIDs, $newIDs); - } - - foreach($articleIDs as $articleID) { - $this->items[] = $this->generateItemFromArticle($articleID); - - if (Debug::isEnabled()) - break; - } - } - - private function getArticleIDsFromPage($page) - { - $url = sprintf(self::URI . '?art_pager=%d', $page); - $html = getSimpleHTMLDOMCached($url, self::INDEX_CACHE_TIMEOUT) - or returnServerError('Could not retrieve ' . $url); - - $articles = $html->find('div.artikel-uebersicht'); - $articleIDs = array(); - - foreach($articles as $article) { - // The article ID is in the 'id' attribute of the div element, prefixed with 'artikel_id_' - if (preg_match('/artikel_id_(\d+)/', $article->id, $match)) { - $articleIDs[] = $match[1]; - } else - returnServerError('Couldn\'t determine article ID from index page.'); - } - - return $articleIDs; - } - - private function generateItemFromArticle($id) - { - $url = sprintf(self::ARTICLE_URI, $id); - $html = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT) - or returnServerError('Could not retrieve ' . $url); - - $div = $html->find('div#artikel-detail', 0); - $divContent = $div->find('.c-content', 0); - $images = $divContent->find('img'); - - // Every external link has a little arrow symbol image attached to it. - // Remove this image. This has to be done before building $content. - foreach($images as $image) - if ($image->class == 'imgextlink') - $image->outertext = ''; - - $title = $div->find('.c-title', 0)->innertext; - $teaser = $div->find('.c-teaser', 0)->innertext; - $content = $divContent->innertext; - - // The title can contain HTML entities. These can be converted back - // to regular UTF-8 characters. - $title = html_entity_decode($title, ENT_HTML5, 'UTF-8'); - - // If there's a teaser, make it more eye-catching, - // so that it is clear, that this is not part of the actual content. - if (strlen(trim($teaser)) > 0) - $content = '<i><strong>' . $teaser . '</strong></i>' . $content; - - $item = array( - 'uri' => $url, - 'title' => $title, - 'content' => $content, - 'uid' => $id, - ); - - // Let's see if there are images in the content, and if yes, attach - // them as enclosures, but not images which are used for linking to an external site. - foreach($images as $image) - if ($image->class != 'imgextlink') - $item['enclosures'][] = $image->src; - - // Get the date of the article. Example: "zuletzt geändert: 26.05.2020" - $editDate = $div->find('div#edit', 0)->plaintext; - $editDate = substr($editDate, strrpos($editDate, ' ') + 1); - $editDate = DateTime::createFromFormat('d.m.Y', $editDate); - - if ($editDate !== false) - $item['timestamp'] = $editDate->getTimestamp(); - - return $item; - } + +class SchweinfurtBuergerinformationenBridge extends BridgeAbstract +{ + const MAINTAINER = 'mibe'; + const NAME = 'Schweinfurt Bürgerinformationen'; + const URI = 'https://www.schweinfurt.de/rathaus-politik/pressestelle/buergerinformationen/index.html'; + const ARTICLE_URI = 'https://www.schweinfurt.de/rathaus-politik/pressestelle/buergerinformationen/%d.html'; + const INDEX_CACHE_TIMEOUT = 10800; // 3h + const ARTICLE_CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Returns the latest news for citizens of Schweinfurt'; + const PARAMETERS = [ + [ + 'pages' => [ + 'name' => 'Number of pages', + 'type' => 'number', + 'title' => 'Specifies the number of pages to fetch. Usually one or two are enough.', + 'exampleValue' => '1', + 'defaultValue' => '1', + ] + ] + ]; + + public function getIcon() + { + return 'https://www.schweinfurt.de/__/images/favicon.ico'; + } + + public function collectData() + { + // Get number of pages to retrieve. One page is the minimum. + $pages = $this->getInput('pages'); + if (!is_int($pages) || $pages < 1) { + $pages = 1; + } + + $articleIDs = []; + + for ($page = 0; $page < $pages; $page++) { + $newIDs = $this->getArticleIDsFromPage($page); + $articleIDs = array_merge($articleIDs, $newIDs); + } + + foreach ($articleIDs as $articleID) { + $this->items[] = $this->generateItemFromArticle($articleID); + + if (Debug::isEnabled()) { + break; + } + } + } + + private function getArticleIDsFromPage($page) + { + $url = sprintf(self::URI . '?art_pager=%d', $page); + $html = getSimpleHTMLDOMCached($url, self::INDEX_CACHE_TIMEOUT) + or returnServerError('Could not retrieve ' . $url); + + $articles = $html->find('div.artikel-uebersicht'); + $articleIDs = []; + + foreach ($articles as $article) { + // The article ID is in the 'id' attribute of the div element, prefixed with 'artikel_id_' + if (preg_match('/artikel_id_(\d+)/', $article->id, $match)) { + $articleIDs[] = $match[1]; + } else { + returnServerError('Couldn\'t determine article ID from index page.'); + } + } + + return $articleIDs; + } + + private function generateItemFromArticle($id) + { + $url = sprintf(self::ARTICLE_URI, $id); + $html = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT) + or returnServerError('Could not retrieve ' . $url); + + $div = $html->find('div#artikel-detail', 0); + $divContent = $div->find('.c-content', 0); + $images = $divContent->find('img'); + + // Every external link has a little arrow symbol image attached to it. + // Remove this image. This has to be done before building $content. + foreach ($images as $image) { + if ($image->class == 'imgextlink') { + $image->outertext = ''; + } + } + + $title = $div->find('.c-title', 0)->innertext; + $teaser = $div->find('.c-teaser', 0)->innertext; + $content = $divContent->innertext; + + // The title can contain HTML entities. These can be converted back + // to regular UTF-8 characters. + $title = html_entity_decode($title, ENT_HTML5, 'UTF-8'); + + // If there's a teaser, make it more eye-catching, + // so that it is clear, that this is not part of the actual content. + if (strlen(trim($teaser)) > 0) { + $content = '<i><strong>' . $teaser . '</strong></i>' . $content; + } + + $item = [ + 'uri' => $url, + 'title' => $title, + 'content' => $content, + 'uid' => $id, + ]; + + // Let's see if there are images in the content, and if yes, attach + // them as enclosures, but not images which are used for linking to an external site. + foreach ($images as $image) { + if ($image->class != 'imgextlink') { + $item['enclosures'][] = $image->src; + } + } + + // Get the date of the article. Example: "zuletzt geändert: 26.05.2020" + $editDate = $div->find('div#edit', 0)->plaintext; + $editDate = substr($editDate, strrpos($editDate, ' ') + 1); + $editDate = DateTime::createFromFormat('d.m.Y', $editDate); + + if ($editDate !== false) { + $item['timestamp'] = $editDate->getTimestamp(); + } + + return $item; + } } diff --git a/bridges/ScmbBridge.php b/bridges/ScmbBridge.php index 646d7c1c..d2fd0b50 100644 --- a/bridges/ScmbBridge.php +++ b/bridges/ScmbBridge.php @@ -1,41 +1,43 @@ <?php -class ScmbBridge extends BridgeAbstract { - const MAINTAINER = 'Astalaseven'; - const NAME = 'Se Coucher Moins Bête Bridge'; - const URI = 'https://secouchermoinsbete.fr'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Returns the newest anecdotes.'; +class ScmbBridge extends BridgeAbstract +{ + const MAINTAINER = 'Astalaseven'; + const NAME = 'Se Coucher Moins Bête Bridge'; + const URI = 'https://secouchermoinsbete.fr'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Returns the newest anecdotes.'; - public function collectData(){ - $html = ''; - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $html = ''; + $html = getSimpleHTMLDOM(self::URI); - foreach($html->find('article') as $article) { - $item = array(); - $item['uri'] = self::URI . $article->find('p.summary a', 0)->href; - $item['title'] = $article->find('header h1 a', 0)->innertext; + foreach ($html->find('article') as $article) { + $item = []; + $item['uri'] = self::URI . $article->find('p.summary a', 0)->href; + $item['title'] = $article->find('header h1 a', 0)->innertext; - // remove text "En savoir plus" from anecdote content - $readMoreButton = $article->find('span.read-more', 0); - if ($readMoreButton) { - $readMoreButton->outertext = ''; - } - $content = $article->find('p.summary a', 0)->innertext; + // remove text "En savoir plus" from anecdote content + $readMoreButton = $article->find('span.read-more', 0); + if ($readMoreButton) { + $readMoreButton->outertext = ''; + } + $content = $article->find('p.summary a', 0)->innertext; - // remove superfluous spaces at the end - $content = substr($content, 0, strlen($content) - 17); + // remove superfluous spaces at the end + $content = substr($content, 0, strlen($content) - 17); - // get publication date - $str_date = $article->find('time', 0)->datetime; - list($date, $time) = explode(' ', $str_date); - list($y, $m, $d) = explode('-', $date); - list($h, $i) = explode(':', $time); - $timestamp = mktime($h, $i, 0, $m, $d, $y); - $item['timestamp'] = $timestamp; + // get publication date + $str_date = $article->find('time', 0)->datetime; + list($date, $time) = explode(' ', $str_date); + list($y, $m, $d) = explode('-', $date); + list($h, $i) = explode(':', $time); + $timestamp = mktime($h, $i, 0, $m, $d, $y); + $item['timestamp'] = $timestamp; - $item['content'] = $content; - $this->items[] = $item; - } - } + $item['content'] = $content; + $this->items[] = $item; + } + } } diff --git a/bridges/ScoopItBridge.php b/bridges/ScoopItBridge.php index 6cf70ce5..a3de71f1 100644 --- a/bridges/ScoopItBridge.php +++ b/bridges/ScoopItBridge.php @@ -1,42 +1,44 @@ <?php -class ScoopItBridge extends BridgeAbstract { - const MAINTAINER = 'Pitchoule'; - const NAME = 'ScoopIt'; - const URI = 'https://www.scoop.it/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Returns most recent results from ScoopIt.'; +class ScoopItBridge extends BridgeAbstract +{ + const MAINTAINER = 'Pitchoule'; + const NAME = 'ScoopIt'; + const URI = 'https://www.scoop.it/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Returns most recent results from ScoopIt.'; - const PARAMETERS = array( array( - 'u' => array( - 'name' => 'keyword', - 'exampleValue' => 'docker', - 'required' => true - ) - )); + const PARAMETERS = [ [ + 'u' => [ + 'name' => 'keyword', + 'exampleValue' => 'docker', + 'required' => true + ] + ]]; - public function collectData(){ - $this->request = $this->getInput('u'); - $link = self::URI . 'search?q=' . urlencode($this->getInput('u')); + public function collectData() + { + $this->request = $this->getInput('u'); + $link = self::URI . 'search?q=' . urlencode($this->getInput('u')); - $html = getSimpleHTMLDOM($link); + $html = getSimpleHTMLDOM($link); - foreach($html->find('div.post-view') as $element) { - $item = array(); - $item['uri'] = $element->find('a', 0)->href; - $item['title'] = preg_replace( - '~[[:cntrl:]]~', - '', - $element->find('div.tCustomization_post_title', 0)->plaintext - ); + foreach ($html->find('div.post-view') as $element) { + $item = []; + $item['uri'] = $element->find('a', 0)->href; + $item['title'] = preg_replace( + '~[[:cntrl:]]~', + '', + $element->find('div.tCustomization_post_title', 0)->plaintext + ); - $item['content'] = preg_replace( - '~[[:cntrl:]]~', - '', - $element->find('div.tCustomization_post_description', 0)->plaintext - ); + $item['content'] = preg_replace( + '~[[:cntrl:]]~', + '', + $element->find('div.tCustomization_post_description', 0)->plaintext + ); - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } } diff --git a/bridges/ScribdBridge.php b/bridges/ScribdBridge.php index 17f4740d..9c93b156 100644 --- a/bridges/ScribdBridge.php +++ b/bridges/ScribdBridge.php @@ -1,78 +1,81 @@ <?php -class ScribdBridge extends BridgeAbstract { - const NAME = 'Scribd Bridge'; - const URI = 'https://www.scribd.com'; - const DESCRIPTION = 'Returns documents uploaded by a user.'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(array( - 'profile' => array( - 'name' => 'Profile URL', - 'type' => 'text', - 'required' => true, - 'title' => 'Profile URL. Example: https://www.scribd.com/user/164147088/Ars-Technica', - 'exampleValue' => 'https://www.scribd.com/user/164147088/Ars-Technica' - ), - )); - const CACHE_TIMEOUT = 3600; +class ScribdBridge extends BridgeAbstract +{ + const NAME = 'Scribd Bridge'; + const URI = 'https://www.scribd.com'; + const DESCRIPTION = 'Returns documents uploaded by a user.'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [[ + 'profile' => [ + 'name' => 'Profile URL', + 'type' => 'text', + 'required' => true, + 'title' => 'Profile URL. Example: https://www.scribd.com/user/164147088/Ars-Technica', + 'exampleValue' => 'https://www.scribd.com/user/164147088/Ars-Technica' + ], + ]]; - private $profileUrlRegex = '/scribd\.com\/(user\/[0-9]+\/[\w-]+)\/?/'; - private $feedName = ''; + const CACHE_TIMEOUT = 3600; - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); + private $profileUrlRegex = '/scribd\.com\/(user\/[0-9]+\/[\w-]+)\/?/'; + private $feedName = ''; - $this->feedName = $html->find('div.header', 0)->plaintext; + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); - foreach($html->find('ul.document_cells > li') as $index => $li) { - $item = array(); + $this->feedName = $html->find('div.header', 0)->plaintext; - $item['title'] = $li->find('div.under_title', 0)->plaintext; - $item['uri'] = $li->find('a', 0)->href; - $item['author'] = $li->find('span.uploader', 0)->plaintext; - $item['uid'] = $li->find('a', 0)->href; + foreach ($html->find('ul.document_cells > li') as $index => $li) { + $item = []; - $pageHtml = getSimpleHTMLDOMCached($item['uri'], 3600); + $item['title'] = $li->find('div.under_title', 0)->plaintext; + $item['uri'] = $li->find('a', 0)->href; + $item['author'] = $li->find('span.uploader', 0)->plaintext; + $item['uid'] = $li->find('a', 0)->href; - $image = $pageHtml->find('meta[property="og:image"]', 0)->content; - $description = $pageHtml->find('meta[property="og:description"]', 0)->content; + $pageHtml = getSimpleHTMLDOMCached($item['uri'], 3600); - foreach ($pageHtml->find('ul.interest_pills li') as $pills) { - $item['categories'][] = $pills->plaintext; - } + $image = $pageHtml->find('meta[property="og:image"]', 0)->content; + $description = $pageHtml->find('meta[property="og:description"]', 0)->content; - $item['content'] = <<<EOD + foreach ($pageHtml->find('ul.interest_pills li') as $pills) { + $item['categories'][] = $pills->plaintext; + } + + $item['content'] = <<<EOD <p>{$description}<p><p><img src="{$image}"></p> EOD; - $item['enclosures'][] = $image; - - $this->items[] = $item; - - if (count($this->items) >= 15) { - break; - } - } - } + $item['enclosures'][] = $image; - public function getName() { + $this->items[] = $item; - if ($this->feedName) { - return $this->feedName . ' - Scribd'; - } + if (count($this->items) >= 15) { + break; + } + } + } - return parent::getName(); - } + public function getName() + { + if ($this->feedName) { + return $this->feedName . ' - Scribd'; + } - public function getURI() { + return parent::getName(); + } - if (!is_null($this->getInput('profile'))) { - preg_match($this->profileUrlRegex, $this->getInput('profile'), $user) - or returnServerError('Could not extract user ID and name from given profile URL.'); + public function getURI() + { + if (!is_null($this->getInput('profile'))) { + preg_match($this->profileUrlRegex, $this->getInput('profile'), $user) + or returnServerError('Could not extract user ID and name from given profile URL.'); - return self::URI . '/' . $user[1] . '/uploads'; - } + return self::URI . '/' . $user[1] . '/uploads'; + } - return parent::getURI(); - } + return parent::getURI(); + } } diff --git a/bridges/SensCritiqueBridge.php b/bridges/SensCritiqueBridge.php index e34beea7..9e42d6a6 100644 --- a/bridges/SensCritiqueBridge.php +++ b/bridges/SensCritiqueBridge.php @@ -1,96 +1,104 @@ <?php -class SensCritiqueBridge extends BridgeAbstract { - const MAINTAINER = 'kranack'; - const NAME = 'Sens Critique'; - const URI = 'https://www.senscritique.com/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Sens Critique news'; +class SensCritiqueBridge extends BridgeAbstract +{ + const MAINTAINER = 'kranack'; + const NAME = 'Sens Critique'; + const URI = 'https://www.senscritique.com/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Sens Critique news'; - const PARAMETERS = array( array( - 's' => array( - 'name' => 'Series', - 'type' => 'checkbox', - 'defaultValue' => 'checked' - ), - 'g' => array( - 'name' => 'Video Games', - 'type' => 'checkbox' - ), - 'b' => array( - 'name' => 'Books', - 'type' => 'checkbox' - ), - 'bd' => array( - 'name' => 'BD', - 'type' => 'checkbox' - ), - 'mu' => array( - 'name' => 'Music', - 'type' => 'checkbox' - ) - )); + const PARAMETERS = [ [ + 's' => [ + 'name' => 'Series', + 'type' => 'checkbox', + 'defaultValue' => 'checked' + ], + 'g' => [ + 'name' => 'Video Games', + 'type' => 'checkbox' + ], + 'b' => [ + 'name' => 'Books', + 'type' => 'checkbox' + ], + 'bd' => [ + 'name' => 'BD', + 'type' => 'checkbox' + ], + 'mu' => [ + 'name' => 'Music', + 'type' => 'checkbox' + ] + ]]; - public function collectData(){ - $categories = array(); - foreach(self::PARAMETERS[$this->queriedContext] as $category => $properties) { - if($this->getInput($category)) { - $uri = self::URI; - switch($category) { - case 's': $uri .= 'series/actualite'; - break; - case 'g': $uri .= 'jeuxvideo/actualite'; - break; - case 'b': $uri .= 'livres/actualite'; - break; - case 'bd': $uri .= 'bd/actualite'; - break; - case 'mu': $uri .= 'musique/actualite'; - break; - } - $html = getSimpleHTMLDOM($uri); - $list = $html->find('ul.elpr-list', 0); + public function collectData() + { + $categories = []; + foreach (self::PARAMETERS[$this->queriedContext] as $category => $properties) { + if ($this->getInput($category)) { + $uri = self::URI; + switch ($category) { + case 's': + $uri .= 'series/actualite'; + break; + case 'g': + $uri .= 'jeuxvideo/actualite'; + break; + case 'b': + $uri .= 'livres/actualite'; + break; + case 'bd': + $uri .= 'bd/actualite'; + break; + case 'mu': + $uri .= 'musique/actualite'; + break; + } + $html = getSimpleHTMLDOM($uri); + $list = $html->find('ul.elpr-list', 0); - $this->extractDataFromList($list); - } - } - } + $this->extractDataFromList($list); + } + } + } - private function extractDataFromList($list){ - if($list === null) { - returnClientError('Cannot extract data from list'); - } + private function extractDataFromList($list) + { + if ($list === null) { + returnClientError('Cannot extract data from list'); + } - foreach($list->find('li') as $movie) { - $item = array(); - $item['author'] = htmlspecialchars_decode($movie->find('.elco-title a', 0)->plaintext, ENT_QUOTES) - . ' ' - . $movie->find('.elco-date', 0)->plaintext; + foreach ($list->find('li') as $movie) { + $item = []; + $item['author'] = htmlspecialchars_decode($movie->find('.elco-title a', 0)->plaintext, ENT_QUOTES) + . ' ' + . $movie->find('.elco-date', 0)->plaintext; - $item['title'] = $movie->find('.elco-title a', 0)->plaintext - . ' ' - . $movie->find('.elco-date', 0)->plaintext; + $item['title'] = $movie->find('.elco-title a', 0)->plaintext + . ' ' + . $movie->find('.elco-date', 0)->plaintext; - $item['content'] = ''; - $originalTitle = $movie->find('.elco-original-title', 0); - $description = $movie->find('.elco-description', 0); + $item['content'] = ''; + $originalTitle = $movie->find('.elco-original-title', 0); + $description = $movie->find('.elco-description', 0); - if ($originalTitle) { - $item['content'] = '<em>' . $originalTitle->plaintext . '</em><br><br>'; - } + if ($originalTitle) { + $item['content'] = '<em>' . $originalTitle->plaintext . '</em><br><br>'; + } - $item['content'] .= $movie->find('.elco-baseline', 0)->plaintext - . '<br>' - . $movie->find('.elco-baseline', 1)->plaintext - . '<br><br>' - . ($description ? $description->plaintext : '') - . '<br><br>' - . trim($movie->find('.erra-ratings .erra-global', 0)->plaintext) - . ' / 10'; + $item['content'] .= $movie->find('.elco-baseline', 0)->plaintext + . '<br>' + . $movie->find('.elco-baseline', 1)->plaintext + . '<br><br>' + . ($description ? $description->plaintext : '') + . '<br><br>' + . trim($movie->find('.erra-ratings .erra-global', 0)->plaintext) + . ' / 10'; - $item['id'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/'); - $item['uri'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/'); - $this->items[] = $item; - } - } + $item['id'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/'); + $item['uri'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/'); + $this->items[] = $item; + } + } } diff --git a/bridges/SeznamZpravyBridge.php b/bridges/SeznamZpravyBridge.php index 06072cdd..f052ed1c 100644 --- a/bridges/SeznamZpravyBridge.php +++ b/bridges/SeznamZpravyBridge.php @@ -1,125 +1,129 @@ <?php -class SeznamZpravyBridge extends BridgeAbstract { - const NAME = 'Seznam Zprávy Bridge'; - const URI = 'https://seznamzpravy.cz'; - const DESCRIPTION = 'Returns newest stories from Seznam Zprávy'; - const MAINTAINER = 'thezeroalpha'; - const PARAMETERS = array( - 'By Author' => array( - 'author' => array( - 'name' => 'Author String', - 'type' => 'text', - 'required' => true, - 'title' => 'The dash-separated author string, as shown in the URL bar.', - 'pattern' => '[a-z]+-[a-z]+-[0-9]+', - 'exampleValue' => 'radek-nohl-1' - ), - ) - ); - - private $feedName; - - public function getName() { - if (isset($this->feedName)) { - return $this->feedName; - } - return parent::getName(); - } - - public function collectData() { - $ONE_DAY = 86500; - switch($this->queriedContext) { - case 'By Author': - $url = 'https://www.seznamzpravy.cz/autor/'; - $selectors = array( - 'breadcrumbs' => 'div[data-dot=ogm-breadcrumb-navigation]', - 'articleList' => 'ul.ogm-document-timeline-page li article[data-dot=mol-timeline-item]', - 'articleTitle' => 'a[data-dot=mol-article-card-title]', - 'articleDM' => 'span.mol-formatted-date__date', - 'articleTime' => 'span.mol-formatted-date__time', - 'articleContent' => 'div[data-dot=ogm-article-content]', - 'articleImage' => 'div[data-dot=ogm-main-media] img', - 'articleParagraphs' => 'div[data-dot=mol-paragraph]' - ); - - $html = getSimpleHTMLDOMCached($url . $this->getInput('author'), $ONE_DAY); - $mainBreadcrumbs = $html->find($selectors['breadcrumbs'], 0) - or returnServerError('Could not get breadcrumbs for: ' . $this->getURI()); - - $author = $mainBreadcrumbs->last_child()->plaintext - or returnServerError('Could not get author for: ' . $this->getURI()); - - $this->feedName = $author . ' - Seznam Zprávy'; - - $articles = $html->find($selectors['articleList']) - or returnServerError('Could not find articles for: ' . $this->getURI()); - - foreach ($articles as $article) { - // Get article URL - $titleLink = $article->find($selectors['articleTitle'], 0) - or returnServerError('Could not find title for: ' . $this->getURI()); - $articleURL = $titleLink->href; - - $articleContentHTML = getSimpleHTMLDOMCached($articleURL, $ONE_DAY); - - // Article header image - $articleImageElem = $articleContentHTML->find($selectors['articleImage'], 0); - - // Article text content - $contentElem = $articleContentHTML->find($selectors['articleContent'], 0) - or returnServerError('Could not get article content for: ' . $articleURL); - $contentParagraphs = $contentElem->find($selectors['articleParagraphs']) - or returnServerError('Could not find paragraphs for: ' . $articleURL); - - // If the article has an image, put that image at the start - $contentInitialValue = isset($articleImageElem) ? $articleImageElem->outertext : ''; - $contentText = array_reduce($contentParagraphs, function($s, $elem) { - return $s . $elem->innertext; - }, $contentInitialValue); - - // Article categories - $breadcrumbsElem = $articleContentHTML->find($selectors['breadcrumbs'], 0) - or returnServerError('Could not find breadcrumbs for: ' . $articleURL); - $breadcrumbs = $breadcrumbsElem->children(); - $numBreadcrumbs = count($breadcrumbs); - $categories = array(); - foreach ($breadcrumbs as $cat) { - if (--$numBreadcrumbs <= 0) { - break; - } - $categories[] = trim($cat->plaintext); - } - - // Article date & time - $articleTimeElem = $article->find($selectors['articleTime'], 0) - or returnServerError('Could not find article time for: ' . $articleURL); - $articleTime = $articleTimeElem->plaintext; - - $articleDMElem = $article->find($selectors['articleDM'], 0); - if (isset($articleDMElem)) { - $articleDMText = $articleDMElem->plaintext; - } else { - // If there is no date but only a time, the article was published today - $articleDMText = date('d.m.'); - } - $articleDMY = preg_replace('/[^0-9\.]/', '', $articleDMText) . date('Y'); - - // Add article to items, potentially with header image as enclosure - $item = array( - 'title' => $titleLink->plaintext, - 'uri' => $titleLink->href, - 'timestamp' => strtotime($articleDMY . ' ' . $articleTime), - 'author' => $author, - 'content' => $contentText, - 'categories' => $categories - ); - if (isset($articleImageElem)) { - $item['enclosures'] = array('https:' . $articleImageElem->src); - } - $this->items[] = $item; - } - break; - } - $this->items[] = $item; - } + +class SeznamZpravyBridge extends BridgeAbstract +{ + const NAME = 'Seznam Zprávy Bridge'; + const URI = 'https://seznamzpravy.cz'; + const DESCRIPTION = 'Returns newest stories from Seznam Zprávy'; + const MAINTAINER = 'thezeroalpha'; + const PARAMETERS = [ + 'By Author' => [ + 'author' => [ + 'name' => 'Author String', + 'type' => 'text', + 'required' => true, + 'title' => 'The dash-separated author string, as shown in the URL bar.', + 'pattern' => '[a-z]+-[a-z]+-[0-9]+', + 'exampleValue' => 'radek-nohl-1' + ], + ] + ]; + + private $feedName; + + public function getName() + { + if (isset($this->feedName)) { + return $this->feedName; + } + return parent::getName(); + } + + public function collectData() + { + $ONE_DAY = 86500; + switch ($this->queriedContext) { + case 'By Author': + $url = 'https://www.seznamzpravy.cz/autor/'; + $selectors = [ + 'breadcrumbs' => 'div[data-dot=ogm-breadcrumb-navigation]', + 'articleList' => 'ul.ogm-document-timeline-page li article[data-dot=mol-timeline-item]', + 'articleTitle' => 'a[data-dot=mol-article-card-title]', + 'articleDM' => 'span.mol-formatted-date__date', + 'articleTime' => 'span.mol-formatted-date__time', + 'articleContent' => 'div[data-dot=ogm-article-content]', + 'articleImage' => 'div[data-dot=ogm-main-media] img', + 'articleParagraphs' => 'div[data-dot=mol-paragraph]' + ]; + + $html = getSimpleHTMLDOMCached($url . $this->getInput('author'), $ONE_DAY); + $mainBreadcrumbs = $html->find($selectors['breadcrumbs'], 0) + or returnServerError('Could not get breadcrumbs for: ' . $this->getURI()); + + $author = $mainBreadcrumbs->last_child()->plaintext + or returnServerError('Could not get author for: ' . $this->getURI()); + + $this->feedName = $author . ' - Seznam Zprávy'; + + $articles = $html->find($selectors['articleList']) + or returnServerError('Could not find articles for: ' . $this->getURI()); + + foreach ($articles as $article) { + // Get article URL + $titleLink = $article->find($selectors['articleTitle'], 0) + or returnServerError('Could not find title for: ' . $this->getURI()); + $articleURL = $titleLink->href; + + $articleContentHTML = getSimpleHTMLDOMCached($articleURL, $ONE_DAY); + + // Article header image + $articleImageElem = $articleContentHTML->find($selectors['articleImage'], 0); + + // Article text content + $contentElem = $articleContentHTML->find($selectors['articleContent'], 0) + or returnServerError('Could not get article content for: ' . $articleURL); + $contentParagraphs = $contentElem->find($selectors['articleParagraphs']) + or returnServerError('Could not find paragraphs for: ' . $articleURL); + + // If the article has an image, put that image at the start + $contentInitialValue = isset($articleImageElem) ? $articleImageElem->outertext : ''; + $contentText = array_reduce($contentParagraphs, function ($s, $elem) { + return $s . $elem->innertext; + }, $contentInitialValue); + + // Article categories + $breadcrumbsElem = $articleContentHTML->find($selectors['breadcrumbs'], 0) + or returnServerError('Could not find breadcrumbs for: ' . $articleURL); + $breadcrumbs = $breadcrumbsElem->children(); + $numBreadcrumbs = count($breadcrumbs); + $categories = []; + foreach ($breadcrumbs as $cat) { + if (--$numBreadcrumbs <= 0) { + break; + } + $categories[] = trim($cat->plaintext); + } + + // Article date & time + $articleTimeElem = $article->find($selectors['articleTime'], 0) + or returnServerError('Could not find article time for: ' . $articleURL); + $articleTime = $articleTimeElem->plaintext; + + $articleDMElem = $article->find($selectors['articleDM'], 0); + if (isset($articleDMElem)) { + $articleDMText = $articleDMElem->plaintext; + } else { + // If there is no date but only a time, the article was published today + $articleDMText = date('d.m.'); + } + $articleDMY = preg_replace('/[^0-9\.]/', '', $articleDMText) . date('Y'); + + // Add article to items, potentially with header image as enclosure + $item = [ + 'title' => $titleLink->plaintext, + 'uri' => $titleLink->href, + 'timestamp' => strtotime($articleDMY . ' ' . $articleTime), + 'author' => $author, + 'content' => $contentText, + 'categories' => $categories + ]; + if (isset($articleImageElem)) { + $item['enclosures'] = ['https:' . $articleImageElem->src]; + } + $this->items[] = $item; + } + break; + } + $this->items[] = $item; + } } diff --git a/bridges/ShanaprojectBridge.php b/bridges/ShanaprojectBridge.php index 2ae793b0..ee9fac7c 100644 --- a/bridges/ShanaprojectBridge.php +++ b/bridges/ShanaprojectBridge.php @@ -1,182 +1,193 @@ <?php -class ShanaprojectBridge extends BridgeAbstract { - const MAINTAINER = 'logmanoriginal'; - const NAME = 'Shanaproject Bridge'; - const URI = 'https://www.shanaproject.com'; - const DESCRIPTION = 'Returns a list of anime from the current Season Anime List'; - const PARAMETERS = array( - array( - 'min_episodes' => array( - 'name' => 'Minimum Episodes', - 'type' => 'number', - 'title' => 'Minimum number of episodes before including in feed', - 'defaultValue' => 0, - ), - 'min_total_episodes' => array( - 'name' => 'Minimum Total Episodes', - 'type' => 'number', - 'title' => 'Minimum total number of episodes before including in feed', - 'defaultValue' => 0, - ), - 'require_banner' => array( - 'name' => 'Require Banner', - 'type' => 'checkbox', - 'title' => 'Only include anime with custom banner image', - 'defaultValue' => false, - ), - ), - ); - - private $uri; - - public function getURI() { - return isset($this->uri) ? $this->uri : parent::getURI(); - } - - public function collectData(){ - $html = $this->loadSeasonAnimeList(); - - $animes = $html->find('div.header_display_box_info') - or returnServerError('Could not find anime headers!'); - - $min_episodes = $this->getInput('min_episodes') ?: 0; - $min_total_episodes = $this->getInput('min_total_episodes') ?: 0; - - foreach($animes as $anime) { - - list( - $episodes_released, - /* of */, - $episodes_total - ) = explode(' ', $this->extractAnimeEpisodeInformation($anime)); - - // Skip if not enough episodes yet - if ($episodes_released < $min_episodes) { - continue; - } - - // Skip if too many episodes in total - if ($episodes_total !== '?' && $episodes_total < $min_total_episodes) { - continue; - } - - // Skip if https://static.shanaproject.com/no-art.jpg - if ($this->getInput('require_banner') - && strpos($this->extractAnimeBackgroundImage($anime), 'no-art') !== false) { - continue; - } - - $this->items[] = array( - 'title' => $this->extractAnimeTitle($anime), - 'author' => $this->extractAnimeAuthor($anime), - 'uri' => $this->extractAnimeUri($anime), - 'timestamp' => $this->extractAnimeTimestamp($anime), - 'content' => $this->buildAnimeContent($anime), - ); - - } - } - - // Returns an html object for the Season Anime List (latest season) - private function loadSeasonAnimeList(){ - - $html = getSimpleHTMLDOM(self::URI . '/seasons'); - - $html = defaultLinkTo($html, self::URI . '/seasons'); - - $season = $html->find('div.follows_menu > a', 1) - or returnServerError('Could not find \'Season Anime List\'!'); - - $html = getSimpleHTMLDOM($season->href); - - $this->uri = $season->href; - - $html = defaultLinkTo($html, $season->href); - - return $html; - - } - - // Extracts the anime title - private function extractAnimeTitle($anime){ - $title = $anime->find('a', 0) - or returnServerError('Could not find anime title!'); - return trim($title->innertext); - } - - // Extracts the anime URI - private function extractAnimeUri($anime){ - $uri = $anime->find('a', 0) - or returnServerError('Could not find anime URI!'); - return $uri->href; - } - - // Extracts the anime release date (timestamp) - private function extractAnimeTimestamp($anime){ - $timestamp = $anime->find('span.header_info_block', 1); - - if(!$timestamp) { - return null; - } - - return strtotime($timestamp->innertext); - } - - // Extracts the anime studio name (author) - private function extractAnimeAuthor($anime){ - $author = $anime->find('span.header_info_block', 2); - - if(!$author) { - return null; // Sometimes the studio is unknown, so leave empty - } - - return trim($author->innertext); - } - - // Extracts the episode information (x of y released) - private function extractAnimeEpisodeInformation($anime){ - $episode = $anime->find('div.header_info_episode', 0) - or returnServerError('Could not find anime episode information!'); - - $retVal = preg_replace('/\r|\n/', ' ', $episode->plaintext); - $retVal = preg_replace('/\s+/', ' ', $retVal); - - return $retVal; - } - - // Extracts the background image - private function extractAnimeBackgroundImage($anime){ - // Getting the picture is a little bit tricky as it is part of the style. - // Luckily the style is part of the parent div :) - - if(preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches)) { - return $matches[1]; - } - - returnServerError('Could not extract background image!'); - } - - // Builds an URI to search for a specific anime (subber is left empty) - private function buildAnimeSearchUri($anime){ - return self::URI - . '/search/?title=' - . urlencode($this->extractAnimeTitle($anime)) - . '&subber='; - } - - // Builds the content string for a given anime - private function buildAnimeContent($anime){ - // We'll use a template string to place our contents - return '<a href="' - . $this->extractAnimeUri($anime) - . '"><img src="http://' - . $this->extractAnimeBackgroundImage($anime) - . '" alt="' - . htmlspecialchars($this->extractAnimeTitle($anime)) - . '" style="border: 1px solid black"></a><br><p>' - . $this->extractAnimeEpisodeInformation($anime) - . '</p><br><p><a href="' - . $this->buildAnimeSearchUri($anime) - . '">Search episodes</a></p>'; - } + +class ShanaprojectBridge extends BridgeAbstract +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'Shanaproject Bridge'; + const URI = 'https://www.shanaproject.com'; + const DESCRIPTION = 'Returns a list of anime from the current Season Anime List'; + const PARAMETERS = [ + [ + 'min_episodes' => [ + 'name' => 'Minimum Episodes', + 'type' => 'number', + 'title' => 'Minimum number of episodes before including in feed', + 'defaultValue' => 0, + ], + 'min_total_episodes' => [ + 'name' => 'Minimum Total Episodes', + 'type' => 'number', + 'title' => 'Minimum total number of episodes before including in feed', + 'defaultValue' => 0, + ], + 'require_banner' => [ + 'name' => 'Require Banner', + 'type' => 'checkbox', + 'title' => 'Only include anime with custom banner image', + 'defaultValue' => false, + ], + ], + ]; + + private $uri; + + public function getURI() + { + return isset($this->uri) ? $this->uri : parent::getURI(); + } + + public function collectData() + { + $html = $this->loadSeasonAnimeList(); + + $animes = $html->find('div.header_display_box_info') + or returnServerError('Could not find anime headers!'); + + $min_episodes = $this->getInput('min_episodes') ?: 0; + $min_total_episodes = $this->getInput('min_total_episodes') ?: 0; + + foreach ($animes as $anime) { + list( + $episodes_released, + /* of */, + $episodes_total + ) = explode(' ', $this->extractAnimeEpisodeInformation($anime)); + + // Skip if not enough episodes yet + if ($episodes_released < $min_episodes) { + continue; + } + + // Skip if too many episodes in total + if ($episodes_total !== '?' && $episodes_total < $min_total_episodes) { + continue; + } + + // Skip if https://static.shanaproject.com/no-art.jpg + if ( + $this->getInput('require_banner') + && strpos($this->extractAnimeBackgroundImage($anime), 'no-art') !== false + ) { + continue; + } + + $this->items[] = [ + 'title' => $this->extractAnimeTitle($anime), + 'author' => $this->extractAnimeAuthor($anime), + 'uri' => $this->extractAnimeUri($anime), + 'timestamp' => $this->extractAnimeTimestamp($anime), + 'content' => $this->buildAnimeContent($anime), + ]; + } + } + + // Returns an html object for the Season Anime List (latest season) + private function loadSeasonAnimeList() + { + $html = getSimpleHTMLDOM(self::URI . '/seasons'); + + $html = defaultLinkTo($html, self::URI . '/seasons'); + + $season = $html->find('div.follows_menu > a', 1) + or returnServerError('Could not find \'Season Anime List\'!'); + + $html = getSimpleHTMLDOM($season->href); + + $this->uri = $season->href; + + $html = defaultLinkTo($html, $season->href); + + return $html; + } + + // Extracts the anime title + private function extractAnimeTitle($anime) + { + $title = $anime->find('a', 0) + or returnServerError('Could not find anime title!'); + return trim($title->innertext); + } + + // Extracts the anime URI + private function extractAnimeUri($anime) + { + $uri = $anime->find('a', 0) + or returnServerError('Could not find anime URI!'); + return $uri->href; + } + + // Extracts the anime release date (timestamp) + private function extractAnimeTimestamp($anime) + { + $timestamp = $anime->find('span.header_info_block', 1); + + if (!$timestamp) { + return null; + } + + return strtotime($timestamp->innertext); + } + + // Extracts the anime studio name (author) + private function extractAnimeAuthor($anime) + { + $author = $anime->find('span.header_info_block', 2); + + if (!$author) { + return null; // Sometimes the studio is unknown, so leave empty + } + + return trim($author->innertext); + } + + // Extracts the episode information (x of y released) + private function extractAnimeEpisodeInformation($anime) + { + $episode = $anime->find('div.header_info_episode', 0) + or returnServerError('Could not find anime episode information!'); + + $retVal = preg_replace('/\r|\n/', ' ', $episode->plaintext); + $retVal = preg_replace('/\s+/', ' ', $retVal); + + return $retVal; + } + + // Extracts the background image + private function extractAnimeBackgroundImage($anime) + { + // Getting the picture is a little bit tricky as it is part of the style. + // Luckily the style is part of the parent div :) + + if (preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches)) { + return $matches[1]; + } + + returnServerError('Could not extract background image!'); + } + + // Builds an URI to search for a specific anime (subber is left empty) + private function buildAnimeSearchUri($anime) + { + return self::URI + . '/search/?title=' + . urlencode($this->extractAnimeTitle($anime)) + . '&subber='; + } + + // Builds the content string for a given anime + private function buildAnimeContent($anime) + { + // We'll use a template string to place our contents + return '<a href="' + . $this->extractAnimeUri($anime) + . '"><img src="http://' + . $this->extractAnimeBackgroundImage($anime) + . '" alt="' + . htmlspecialchars($this->extractAnimeTitle($anime)) + . '" style="border: 1px solid black"></a><br><p>' + . $this->extractAnimeEpisodeInformation($anime) + . '</p><br><p><a href="' + . $this->buildAnimeSearchUri($anime) + . '">Search episodes</a></p>'; + } } diff --git a/bridges/Shimmie2Bridge.php b/bridges/Shimmie2Bridge.php index a279c77d..0a87d65e 100644 --- a/bridges/Shimmie2Bridge.php +++ b/bridges/Shimmie2Bridge.php @@ -1,37 +1,39 @@ <?php -class Shimmie2Bridge extends DanbooruBridge { +class Shimmie2Bridge extends DanbooruBridge +{ + const NAME = 'Shimmie v2'; + const URI = 'https://shimmie.shishnet.org/'; + const DESCRIPTION = 'Returns images from given page'; - const NAME = 'Shimmie v2'; - const URI = 'https://shimmie.shishnet.org/'; - const DESCRIPTION = 'Returns images from given page'; + const PATHTODATA = '.shm-thumb-link'; + const IDATTRIBUTE = 'data-post-id'; - const PATHTODATA = '.shm-thumb-link'; - const IDATTRIBUTE = 'data-post-id'; + protected function getFullURI() + { + return $this->getURI() + . 'post/list/' + . $this->getInput('t') + . '/' + . $this->getInput('p'); + } - protected function getFullURI(){ - return $this->getURI() - . 'post/list/' - . $this->getInput('t') - . '/' - . $this->getInput('p'); - } + protected function getItemFromElement($element) + { + $item = []; + $item['uri'] = $this->getURI() . $element->href; + $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); + $item['timestamp'] = time(); + $thumbnailUri = $this->getURI() . $element->find('img', 0)->src; + $item['categories'] = explode(' ', $element->getAttribute('data-tags')); + $item['title'] = $this->getName() . ' | ' . $item['id']; + $item['content'] = '<a href="' + . $item['uri'] + . '"><img src="' + . $thumbnailUri + . '" /></a><br>Tags: ' + . $element->getAttribute('data-tags'); - protected function getItemFromElement($element){ - $item = array(); - $item['uri'] = $this->getURI() . $element->href; - $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); - $item['timestamp'] = time(); - $thumbnailUri = $this->getURI() . $element->find('img', 0)->src; - $item['categories'] = explode(' ', $element->getAttribute('data-tags')); - $item['title'] = $this->getName() . ' | ' . $item['id']; - $item['content'] = '<a href="' - . $item['uri'] - . '"><img src="' - . $thumbnailUri - . '" /></a><br>Tags: ' - . $element->getAttribute('data-tags'); - - return $item; - } + return $item; + } } diff --git a/bridges/SkimfeedBridge.php b/bridges/SkimfeedBridge.php index d4475c77..0555af0f 100644 --- a/bridges/SkimfeedBridge.php +++ b/bridges/SkimfeedBridge.php @@ -1,713 +1,681 @@ <?php -class SkimfeedBridge extends BridgeAbstract { - - const CONTEXT_NEWS_BOX = 'News box'; - const CONTEXT_HOT_TOPICS = 'Hot topics'; - const CONTEXT_TECH_NEWS = 'Tech news'; - const CONTEXT_CUSTOM = 'Custom feed'; - - const NAME = 'Skimfeed Bridge'; - const URI = 'https://skimfeed.com'; - const DESCRIPTION = 'Returns feeds from Skimfeed, also supports custom feeds!'; - const MAINTAINER = 'logmanoriginal'; - const CACHE_TIMEOUT = 3600; - - const PARAMETERS = array( - self::CONTEXT_NEWS_BOX => array( // auto-generated (see below) - 'box_channel' => array( - 'name' => 'Channel', - 'type' => 'list', - 'title' => 'Select your channel', - 'values' => array( - 'Hacker News' => '/news/hacker-news.html', - 'QZ' => '/news/qz.html', - 'The Verge' => '/news/the-verge.html', - 'Slashdot' => '/news/slashdot.html', - 'Lifehacker' => '/news/lifehacker.html', - 'Gizmag' => '/news/gizmag.html', - 'Fast Company' => '/news/fast-company.html', - 'Engadget' => '/news/engadget.html', - 'Wired' => '/news/wired.html', - 'MakeUseOf' => '/news/makeuseof.html', - 'Techcrunch' => '/news/techcrunch.html', - 'Apple Insider' => '/news/apple-insider.html', - 'ArsTechnica' => '/news/arstechnica.html', - 'Tech in Asia' => '/news/tech-in-asia.html', - 'FastCoExist' => '/news/fastcoexist.html', - 'Digital Trends' => '/news/digital-trends.html', - 'AnandTech' => '/news/anandtech.html', - 'How to Geek' => '/news/how-to-geek.html', - 'Geek' => '/news/geek.html', - 'BBC Technology' => '/news/bbc-technology.html', - 'Extreme Tech' => '/news/extreme-tech.html', - 'Packet Storm Sec' => '/news/packet-storm-sec.html', - 'MedGadget' => '/news/medgadget.html', - 'Design' => '/news/design.html', - 'The Next Web' => '/news/the-next-web.html', - 'Bit-Tech' => '/news/bit-tech.html', - 'Next Big Future' => '/news/next-big-future.html', - 'A VC' => '/news/a-vc.html', - 'Copyblogger' => '/news/copyblogger.html', - 'Smashing Mag' => '/news/smashing-mag.html', - 'Continuations' => '/news/continuations.html', - 'Cult of Mac' => '/news/cult-of-mac.html', - 'SecuriTeam' => '/news/securiteam.html', - 'The Tech Block' => '/news/the-tech-block.html', - 'BetaBeat' => '/news/betabeat.html', - 'PC Mag' => '/news/pc-mag.html', - 'Venture Beat' => '/news/venture-beat.html', - 'ReadWriteWeb' => '/news/readwriteweb.html', - 'High Scalability' => '/news/high-scalability.html', - ) - ) - ), - self::CONTEXT_HOT_TOPICS => array(), - self::CONTEXT_TECH_NEWS => array( // auto-generated (see below) - 'tech_channel' => array( - 'name' => 'Tech channel', - 'type' => 'list', - 'title' => 'Select your tech channel', - 'values' => array( - 'Agg' => array( - 'Reddit' => '/news/reddit.html', - 'Tech Insider' => '/news/tech-insider.html', - 'Digg' => '/news/digg.html', - 'Meta Filter' => '/news/meta-filter.html', - 'Fark' => '/news/fark.html', - 'Mashable' => '/news/mashable.html', - 'Ad Week' => '/news/ad-week.html', - 'The Chive' => '/news/the-chive.html', - 'BoingBoing' => '/news/boingboing.html', - 'Vice' => '/news/vice.html', - 'ClientsFromHell' => '/news/clientsfromhell.html', - 'How Stuff Works' => '/news/how-stuff-works.html', - 'Buzzfeed' => '/news/buzzfeed.html', - 'BoingBoing' => '/news/boingboing.html', - 'Cracked' => '/news/cracked.html', - 'Weird News' => '/news/weird-news.html', - 'ITOTD' => '/news/itotd.html', - 'Metafilter' => '/news/metafilter.html', - 'TheOnion' => '/news/theonion.html', - ), - 'Cars' => array( - 'Reddit Cars' => '/news/reddit-cars.html', - 'NYT Auto' => '/news/nyt-auto.html', - 'Truth About Cars' => '/news/truth-about-cars.html', - 'AutoBlog' => '/news/autoblog.html', - 'AutoSpies' => '/news/autospies.html', - 'Autoweek' => '/news/autoweek.html', - 'The Garage' => '/news/the-garage.html', - 'Car and Driver' => '/news/car-and-driver.html', - 'EGM Car Tech' => '/news/egm-car-tech.html', - 'Top Gear' => '/news/top-gear.html', - 'eGarage' => '/news/egarage.html', - ), - 'Comics' => array( - 'Penny Arcade' => '/news/penny-arcade.html', - 'XKCD' => '/news/xkcd.html', - 'Channelate' => '/news/channelate.html', - 'Savage Chicken' => '/news/savage-chicken.html', - 'Dinosaur Comics' => '/news/dinosaur-comics.html', - 'Explosm' => '/news/explosm.html', - 'PoorlyDLines' => '/news/poorlydlines.html', - 'Moonbeard' => '/news/moonbeard.html', - 'Nedroid' => '/news/nedroid.html', - ), - 'Design' => array( - 'FastCoCreate' => '/news/fastcocreate.html', - 'Dezeen' => '/news/dezeen.html', - 'Design Boom' => '/news/design-boom.html', - 'Mmminimal' => '/news/mmminimal.html', - 'We Heart' => '/news/we-heart.html', - 'CreativeBloq' => '/news/creativebloq.html', - 'TheDSGNblog' => '/news/thedsgnblog.html', - 'Grainedit' => '/news/grainedit.html', - ), - 'Football' => array( - 'Mail Football' => '/news/mail-football.html', - 'Yahoo Football' => '/news/yahoo-football.html', - 'FourFourTwo' => '/news/fourfourtwo.html', - 'Goal' => '/news/goal.html', - 'BBC Football' => '/news/bbc-football.html', - 'TalkSport' => '/news/talksport.html', - '101 Great Goals' => '/news/101-great-goals.html', - 'Who Scored' => '/news/who-scored.html', - 'Football365 Champ' => '/news/football365-champ.html', - 'Football365 Premier' => '/news/football365-premier.html', - 'BleacherReport' => '/news/bleacherreport.html', - ), - 'Gaming' => array( - 'Polygon' => '/news/polygon.html', - 'Gamespot' => '/news/gamespot.html', - 'RockPaperShotgun' => '/news/rockpapershotgun.html', - 'VG247' => '/news/vg247.html', - 'IGN' => '/news/ign.html', - 'Reddit Games' => '/news/reddit-games.html', - 'TouchArcade' => '/news/toucharcade.html', - 'GamesRadar' => '/news/gamesradar.html', - 'Siliconera' => '/news/siliconera.html', - 'Reddit GameDeals' => '/news/reddit-gamedeals.html', - 'Joystiq' => '/news/joystiq.html', - 'GameInformer' => '/news/gameinformer.html', - 'PSN Blog' => '/news/psn-blog.html', - 'Reddit GamerNews' => '/news/reddit-gamernews.html', - 'Steam' => '/news/steam.html', - 'DualShockers' => '/news/dualshockers.html', - 'ShackNews' => '/news/shacknews.html', - 'CheapAssGamer' => '/news/cheapassgamer.html', - 'Eurogamer' => '/news/eurogamer.html', - 'Major Nelson' => '/news/major-nelson.html', - 'Reddit Truegaming' => '/news/reddit-truegaming.html', - 'GameTrailers' => '/news/gametrailers.html', - 'GamaSutra' => '/news/gamasutra.html', - 'USGamer' => '/news/usgamer.html', - 'Shoryuken' => '/news/shoryuken.html', - 'Destructoid' => '/news/destructoid.html', - 'ArsGaming' => '/news/arsgaming.html', - 'XBOX Blog' => '/news/xbox-blog.html', - 'GiantBomb' => '/news/giantbomb.html', - 'VideoGamer' => '/news/videogamer.html', - 'Pocket Tactics' => '/news/pocket-tactics.html', - 'WiredGaming' => '/news/wiredgaming.html', - 'AllGamesBeta' => '/news/allgamesbeta.html', - 'OnGamers' => '/news/ongamers.html', - 'Reddit GameBundles' => '/news/reddit-gamebundles.html', - 'Kotaku' => '/news/kotaku.html', - 'PCGamer' => '/news/pcgamer.html', - ), - 'Investing' => array( - 'Seeking Alpha' => '/news/seeking-alpha.html', - 'BBC Business' => '/news/bbc-business.html', - 'Harvard Biz' => '/news/harvard-biz.html', - 'Market Watch' => '/news/market-watch.html', - 'Investor Place' => '/news/investor-place.html', - 'Money Week' => '/news/money-week.html', - 'Moneybeat' => '/news/moneybeat.html', - 'Dealbook' => '/news/dealbook.html', - 'Economist Business' => '/news/economist-business.html', - 'Economist' => '/news/economist.html', - 'Economist CN' => '/news/economist-cn.html', - ), - 'Long' => array( - 'The Atlantic' => '/news/the-atlantic.html', - 'Reddit Long' => '/news/reddit-long.html', - 'Paris Review' => '/news/paris-review.html', - 'New Yorker' => '/news/new-yorker.html', - 'LongForm' => '/news/longform.html', - 'LongReads' => '/news/longreads.html', - 'The Browser' => '/news/the-browser.html', - 'The Feature' => '/news/the-feature.html', - ), - 'MMA' => array( - 'MMA Weekly' => '/news/mma-weekly.html', - 'MMAFighting' => '/news/mmafighting.html', - 'Reddit MMA' => '/news/reddit-mma.html', - 'Sherdog Articles' => '/news/sherdog-articles.html', - 'FightLand Vice' => '/news/fightland-vice.html', - 'Sherdog Forum' => '/news/sherdog-forum.html', - 'MMA Junkie' => '/news/mma-junkie.html', - 'Sherdog MMA Video' => '/news/sherdog-mma-video.html', - 'BloodyElbow' => '/news/bloodyelbow.html', - 'CageWriter' => '/news/cagewriter.html', - 'Sherdog News' => '/news/sherdog-news.html', - 'MMAForum' => '/news/mmaforum.html', - 'MMA Junkie Radio' => '/news/mma-junkie-radio.html', - 'UFC News' => '/news/ufc-news.html', - 'FightLinker' => '/news/fightlinker.html', - 'Bodybuilding MMA' => '/news/bodybuilding-mma.html', - 'BleacherReport MMA' => '/news/bleacherreport-mma.html', - 'FiveOuncesofPain' => '/news/fiveouncesofpain.html', - 'Sherdog Pictures' => '/news/sherdog-pictures.html', - 'CagePotato' => '/news/cagepotato.html', - 'Sherdog Radio' => '/news/sherdog-radio.html', - 'ProMMARadio' => '/news/prommaradio.html', - ), - 'Mobile' => array( - 'Macrumors' => '/news/macrumors.html', - 'Android Police' => '/news/android-police.html', - 'GSM Arena' => '/news/gsm-arena.html', - 'DigiTrend Mobile' => '/news/digitrend-mobile.html', - 'Mobile Nation' => '/news/mobile-nation.html', - 'TechRadar' => '/news/techradar.html', - 'ZDNET Mobile' => '/news/zdnet-mobile.html', - 'MacWorld' => '/news/macworld.html', - 'Android Dev Blog' => '/news/android-dev-blog.html', - ), - 'News' => array( - 'Daily Mail' => '/news/daily-mail.html', - 'Business Insider' => '/news/business-insider.html', - 'The Guardian' => '/news/the-guardian.html', - 'Fox' => '/news/fox.html', - 'BBC World' => '/news/bbc-world.html', - 'MSNBC' => '/news/msnbc.html', - 'ABC News' => '/news/abc-news.html', - 'Al Jazeera' => '/news/al-jazeera.html', - 'Business Insider India' => '/news/business-insider-india.html', - 'Observer' => '/news/observer.html', - 'NYT Tech' => '/news/nyt-tech.html', - 'NYT World' => '/news/nyt-world.html', - 'CNN' => '/news/cnn.html', - 'Japan Times' => '/news/japan-times.html', - 'WorldCrunch' => '/news/worldcrunch.html', - 'Pro publica' => '/news/pro-publica.html', - 'OZY' => '/news/ozy.html', - 'Times of India' => '/news/times-of-india.html', - 'The Australian' => '/news/the-australian.html', - 'Harpers' => '/news/harpers.html', - 'Moscow Times' => '/news/moscow-times.html', - 'The Times' => '/news/the-times.html', - 'Reuters Tech' => '/news/reuters-tech.html', - ), - 'Politics' => array( - 'FreeRepublic' => '/news/freerepublic.html', - 'Salon' => '/news/salon.html', - 'DrudgeReport' => '/news/drudgereport.html', - 'TheHill' => '/news/thehill.html', - 'TheBlaze' => '/news/theblaze.html', - 'InfoWars' => '/news/infowars.html', - 'New Republic' => '/news/new-republic.html', - 'WashTimes' => '/news/washtimes.html', - 'RealCleanPol' => '/news/realcleanpol.html', - 'Fact Check' => '/news/fact-check.html', - 'DailyKos' => '/news/dailykos.html', - 'NewsMax' => '/news/newsmax.html', - 'Politico' => '/news/politico.html', - 'Michelle Malkin' => '/news/michelle-malkin.html', - ), - 'Reddit' => array( - 'R Movies' => '/news/r-movies.html', - 'R News' => '/news/r-news.html', - 'Futurology' => '/news/futurology.html', - 'R All' => '/news/r-all.html', - 'R Music' => '/news/r-music.html', - 'R Askscience' => '/news/r-askscience.html', - 'R Technology' => '/news/r-technology.html', - 'R Bestof' => '/news/r-bestof.html', - 'R Askreddit' => '/news/r-askreddit.html', - 'R Worldnews' => '/news/r-worldnews.html', - 'R Explainlikeimfive' => '/news/r-explainlikeimfive.html', - 'R Iama' => '/news/r-iama.html', - ), - 'Science' => array( - 'PhysOrg' => '/news/physorg.html', - 'Hack-a-day' => '/news/hack-a-day.html', - 'Reddit Science' => '/news/reddit-science.html', - 'Stats Blog' => '/news/stats-blog.html', - 'Flowing Data' => '/news/flowing-data.html', - 'Eureka Alert' => '/news/eureka-alert.html', - 'Robotics BizRev' => '/news/robotics-bizrev.html', - 'Planet big Data' => '/news/planet-big-data.html', - 'Makezine' => '/news/makezine.html', - 'MIT Tech' => '/news/mit-tech.html', - 'R Bloggers' => '/news/r-bloggers.html', - 'DataIsBeautiful' => '/news/dataisbeautiful.html', - 'Ted Videos' => '/news/ted-videos.html', - 'Advanced Science' => '/news/advanced-science.html', - 'Robotiq' => '/news/robotiq.html', - 'Science Daily' => '/news/science-daily.html', - 'IEEE Robotics' => '/news/ieee-robotics.html', - 'PSFK' => '/news/psfk.html', - 'Discover Magazine' => '/news/discover-magazine.html', - 'DataTau' => '/news/datatau.html', - 'RoboHub' => '/news/robohub.html', - 'Discovery' => '/news/discovery.html', - 'Smart Data' => '/news/smart-data.html', - 'Whats Big Data' => '/news/whats-big-data.html', - ), - 'Tech' => array( - 'Hacker News' => '/news/hacker-news.html', - 'The Verge' => '/news/the-verge.html', - 'Lifehacker' => '/news/lifehacker.html', - 'Fast Company' => '/news/fast-company.html', - 'ArsTechnica' => '/news/arstechnica.html', - 'MakeUseOf' => '/news/makeuseof.html', - 'FastCoExist' => '/news/fastcoexist.html', - 'How to Geek' => '/news/how-to-geek.html', - 'The Next Web' => '/news/the-next-web.html', - 'Engadget' => '/news/engadget.html', - 'Gizmag' => '/news/gizmag.html', - 'QZ' => '/news/qz.html', - 'Wired' => '/news/wired.html', - 'Techcrunch' => '/news/techcrunch.html', - 'Slashdot' => '/news/slashdot.html', - 'Extreme Tech' => '/news/extreme-tech.html', - 'AnandTech' => '/news/anandtech.html', - 'Digital Trends' => '/news/digital-trends.html', - 'Next Big Future' => '/news/next-big-future.html', - 'Apple Insider' => '/news/apple-insider.html', - 'Geek' => '/news/geek.html', - 'BBC Technology' => '/news/bbc-technology.html', - 'Bit-Tech' => '/news/bit-tech.html', - 'Packet Storm Sec' => '/news/packet-storm-sec.html', - 'Design' => '/news/design.html', - 'High Scalability' => '/news/high-scalability.html', - 'Smashing Mag' => '/news/smashing-mag.html', - 'The Tech Block' => '/news/the-tech-block.html', - 'A VC' => '/news/a-vc.html', - 'Tech in Asia' => '/news/tech-in-asia.html', - 'ReadWriteWeb' => '/news/readwriteweb.html', - 'PC Mag' => '/news/pc-mag.html', - 'Continuations' => '/news/continuations.html', - 'Copyblogger' => '/news/copyblogger.html', - 'Cult of Mac' => '/news/cult-of-mac.html', - 'BetaBeat' => '/news/betabeat.html', - 'MedGadget' => '/news/medgadget.html', - 'SecuriTeam' => '/news/securiteam.html', - 'Venture Beat' => '/news/venture-beat.html', - ), - 'Trend' => array( - 'Trend Hunter' => '/news/trend-hunter.html', - 'ApartmentT' => '/news/apartmentt.html', - 'GQ' => '/news/gq.html', - 'Digital Trends' => '/news/digital-trends.html', - 'Cool Hunting' => '/news/cool-hunting.html', - 'FastCoDesign' => '/news/fastcodesign.html', - 'TC Startups' => '/news/tc-startups.html', - 'Killer Startups' => '/news/killer-startups.html', - 'DigiInfo' => '/news/digiinfo.html', - 'New Startups' => '/news/new-startups.html', - 'DigiTrends' => '/news/digitrends.html', - ), - 'Watches' => array( - 'Hodinkee' => '/news/hodinkee.html', - 'Quill and Pad' => '/news/quill-and-pad.html', - 'Monochrome' => '/news/monochrome.html', - 'Deployant' => '/news/deployant.html', - 'Watches by SJX' => '/news/watches-by-sjx.html', - 'Fratello Watches' => '/news/fratello-watches.html', - 'A Blog to Watch' => '/news/a-blog-to-watch.html', - 'Wound for Life' => '/news/wound-for-life.html', - 'Watch Paper' => '/news/watch-paper.html', - 'Watch Report' => '/news/watch-report.html', - 'Perpetuelle' => '/news/perpetuelle.html', - ), - 'Youtube' => array( - 'LinusTechTips' => '/news/linustechtips.html', - 'MetalJesusRocks' => '/news/metaljesusrocks.html', - 'TotalBiscuit' => '/news/totalbiscuit.html', - 'DexBonus' => '/news/dexbonus.html', - 'Lon Siedman' => '/news/lon-siedman.html', - 'MKBHD' => '/news/mkbhd.html', - 'Terry A Davis' => '/news/terry-a-davis.html', - 'HappyConsole' => '/news/happyconsole.html', - 'Austin Evans' => '/news/austin-evans.html', - 'NCIX' => '/news/ncix.html', - ), - ) - ), - ), - self::CONTEXT_CUSTOM => array( - 'config' => array( - 'name' => 'Configuration', - 'type' => 'text', - 'required' => true, - 'title' => 'Enter feed numbers from Skimfeed! e.g: 5,8,2,l,p,9,23', - 'exampleValue' => '5' - ) - ), - 'global' => array( - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'title' => 'Limits the number of returned items in the feed', - 'exampleValue' => 10 - ) - ) - ); - - public function getURI() { - - switch($this->queriedContext) { - - case self::CONTEXT_NEWS_BOX: - - $channel = $this->getInput('box_channel'); - - if($channel) { - return static::URI . $channel; - } - - break; - - case self::CONTEXT_HOT_TOPICS: - return static::URI; +class SkimfeedBridge extends BridgeAbstract +{ + const CONTEXT_NEWS_BOX = 'News box'; + const CONTEXT_HOT_TOPICS = 'Hot topics'; + const CONTEXT_TECH_NEWS = 'Tech news'; + const CONTEXT_CUSTOM = 'Custom feed'; + + const NAME = 'Skimfeed Bridge'; + const URI = 'https://skimfeed.com'; + const DESCRIPTION = 'Returns feeds from Skimfeed, also supports custom feeds!'; + const MAINTAINER = 'logmanoriginal'; + const CACHE_TIMEOUT = 3600; + + const PARAMETERS = [ + self::CONTEXT_NEWS_BOX => [ // auto-generated (see below) + 'box_channel' => [ + 'name' => 'Channel', + 'type' => 'list', + 'title' => 'Select your channel', + 'values' => [ + 'Hacker News' => '/news/hacker-news.html', + 'QZ' => '/news/qz.html', + 'The Verge' => '/news/the-verge.html', + 'Slashdot' => '/news/slashdot.html', + 'Lifehacker' => '/news/lifehacker.html', + 'Gizmag' => '/news/gizmag.html', + 'Fast Company' => '/news/fast-company.html', + 'Engadget' => '/news/engadget.html', + 'Wired' => '/news/wired.html', + 'MakeUseOf' => '/news/makeuseof.html', + 'Techcrunch' => '/news/techcrunch.html', + 'Apple Insider' => '/news/apple-insider.html', + 'ArsTechnica' => '/news/arstechnica.html', + 'Tech in Asia' => '/news/tech-in-asia.html', + 'FastCoExist' => '/news/fastcoexist.html', + 'Digital Trends' => '/news/digital-trends.html', + 'AnandTech' => '/news/anandtech.html', + 'How to Geek' => '/news/how-to-geek.html', + 'Geek' => '/news/geek.html', + 'BBC Technology' => '/news/bbc-technology.html', + 'Extreme Tech' => '/news/extreme-tech.html', + 'Packet Storm Sec' => '/news/packet-storm-sec.html', + 'MedGadget' => '/news/medgadget.html', + 'Design' => '/news/design.html', + 'The Next Web' => '/news/the-next-web.html', + 'Bit-Tech' => '/news/bit-tech.html', + 'Next Big Future' => '/news/next-big-future.html', + 'A VC' => '/news/a-vc.html', + 'Copyblogger' => '/news/copyblogger.html', + 'Smashing Mag' => '/news/smashing-mag.html', + 'Continuations' => '/news/continuations.html', + 'Cult of Mac' => '/news/cult-of-mac.html', + 'SecuriTeam' => '/news/securiteam.html', + 'The Tech Block' => '/news/the-tech-block.html', + 'BetaBeat' => '/news/betabeat.html', + 'PC Mag' => '/news/pc-mag.html', + 'Venture Beat' => '/news/venture-beat.html', + 'ReadWriteWeb' => '/news/readwriteweb.html', + 'High Scalability' => '/news/high-scalability.html', + ] + ] + ], + self::CONTEXT_HOT_TOPICS => [], + self::CONTEXT_TECH_NEWS => [ // auto-generated (see below) + 'tech_channel' => [ + 'name' => 'Tech channel', + 'type' => 'list', + 'title' => 'Select your tech channel', + 'values' => [ + 'Agg' => [ + 'Reddit' => '/news/reddit.html', + 'Tech Insider' => '/news/tech-insider.html', + 'Digg' => '/news/digg.html', + 'Meta Filter' => '/news/meta-filter.html', + 'Fark' => '/news/fark.html', + 'Mashable' => '/news/mashable.html', + 'Ad Week' => '/news/ad-week.html', + 'The Chive' => '/news/the-chive.html', + 'BoingBoing' => '/news/boingboing.html', + 'Vice' => '/news/vice.html', + 'ClientsFromHell' => '/news/clientsfromhell.html', + 'How Stuff Works' => '/news/how-stuff-works.html', + 'Buzzfeed' => '/news/buzzfeed.html', + 'BoingBoing' => '/news/boingboing.html', + 'Cracked' => '/news/cracked.html', + 'Weird News' => '/news/weird-news.html', + 'ITOTD' => '/news/itotd.html', + 'Metafilter' => '/news/metafilter.html', + 'TheOnion' => '/news/theonion.html', + ], + 'Cars' => [ + 'Reddit Cars' => '/news/reddit-cars.html', + 'NYT Auto' => '/news/nyt-auto.html', + 'Truth About Cars' => '/news/truth-about-cars.html', + 'AutoBlog' => '/news/autoblog.html', + 'AutoSpies' => '/news/autospies.html', + 'Autoweek' => '/news/autoweek.html', + 'The Garage' => '/news/the-garage.html', + 'Car and Driver' => '/news/car-and-driver.html', + 'EGM Car Tech' => '/news/egm-car-tech.html', + 'Top Gear' => '/news/top-gear.html', + 'eGarage' => '/news/egarage.html', + ], + 'Comics' => [ + 'Penny Arcade' => '/news/penny-arcade.html', + 'XKCD' => '/news/xkcd.html', + 'Channelate' => '/news/channelate.html', + 'Savage Chicken' => '/news/savage-chicken.html', + 'Dinosaur Comics' => '/news/dinosaur-comics.html', + 'Explosm' => '/news/explosm.html', + 'PoorlyDLines' => '/news/poorlydlines.html', + 'Moonbeard' => '/news/moonbeard.html', + 'Nedroid' => '/news/nedroid.html', + ], + 'Design' => [ + 'FastCoCreate' => '/news/fastcocreate.html', + 'Dezeen' => '/news/dezeen.html', + 'Design Boom' => '/news/design-boom.html', + 'Mmminimal' => '/news/mmminimal.html', + 'We Heart' => '/news/we-heart.html', + 'CreativeBloq' => '/news/creativebloq.html', + 'TheDSGNblog' => '/news/thedsgnblog.html', + 'Grainedit' => '/news/grainedit.html', + ], + 'Football' => [ + 'Mail Football' => '/news/mail-football.html', + 'Yahoo Football' => '/news/yahoo-football.html', + 'FourFourTwo' => '/news/fourfourtwo.html', + 'Goal' => '/news/goal.html', + 'BBC Football' => '/news/bbc-football.html', + 'TalkSport' => '/news/talksport.html', + '101 Great Goals' => '/news/101-great-goals.html', + 'Who Scored' => '/news/who-scored.html', + 'Football365 Champ' => '/news/football365-champ.html', + 'Football365 Premier' => '/news/football365-premier.html', + 'BleacherReport' => '/news/bleacherreport.html', + ], + 'Gaming' => [ + 'Polygon' => '/news/polygon.html', + 'Gamespot' => '/news/gamespot.html', + 'RockPaperShotgun' => '/news/rockpapershotgun.html', + 'VG247' => '/news/vg247.html', + 'IGN' => '/news/ign.html', + 'Reddit Games' => '/news/reddit-games.html', + 'TouchArcade' => '/news/toucharcade.html', + 'GamesRadar' => '/news/gamesradar.html', + 'Siliconera' => '/news/siliconera.html', + 'Reddit GameDeals' => '/news/reddit-gamedeals.html', + 'Joystiq' => '/news/joystiq.html', + 'GameInformer' => '/news/gameinformer.html', + 'PSN Blog' => '/news/psn-blog.html', + 'Reddit GamerNews' => '/news/reddit-gamernews.html', + 'Steam' => '/news/steam.html', + 'DualShockers' => '/news/dualshockers.html', + 'ShackNews' => '/news/shacknews.html', + 'CheapAssGamer' => '/news/cheapassgamer.html', + 'Eurogamer' => '/news/eurogamer.html', + 'Major Nelson' => '/news/major-nelson.html', + 'Reddit Truegaming' => '/news/reddit-truegaming.html', + 'GameTrailers' => '/news/gametrailers.html', + 'GamaSutra' => '/news/gamasutra.html', + 'USGamer' => '/news/usgamer.html', + 'Shoryuken' => '/news/shoryuken.html', + 'Destructoid' => '/news/destructoid.html', + 'ArsGaming' => '/news/arsgaming.html', + 'XBOX Blog' => '/news/xbox-blog.html', + 'GiantBomb' => '/news/giantbomb.html', + 'VideoGamer' => '/news/videogamer.html', + 'Pocket Tactics' => '/news/pocket-tactics.html', + 'WiredGaming' => '/news/wiredgaming.html', + 'AllGamesBeta' => '/news/allgamesbeta.html', + 'OnGamers' => '/news/ongamers.html', + 'Reddit GameBundles' => '/news/reddit-gamebundles.html', + 'Kotaku' => '/news/kotaku.html', + 'PCGamer' => '/news/pcgamer.html', + ], + 'Investing' => [ + 'Seeking Alpha' => '/news/seeking-alpha.html', + 'BBC Business' => '/news/bbc-business.html', + 'Harvard Biz' => '/news/harvard-biz.html', + 'Market Watch' => '/news/market-watch.html', + 'Investor Place' => '/news/investor-place.html', + 'Money Week' => '/news/money-week.html', + 'Moneybeat' => '/news/moneybeat.html', + 'Dealbook' => '/news/dealbook.html', + 'Economist Business' => '/news/economist-business.html', + 'Economist' => '/news/economist.html', + 'Economist CN' => '/news/economist-cn.html', + ], + 'Long' => [ + 'The Atlantic' => '/news/the-atlantic.html', + 'Reddit Long' => '/news/reddit-long.html', + 'Paris Review' => '/news/paris-review.html', + 'New Yorker' => '/news/new-yorker.html', + 'LongForm' => '/news/longform.html', + 'LongReads' => '/news/longreads.html', + 'The Browser' => '/news/the-browser.html', + 'The Feature' => '/news/the-feature.html', + ], + 'MMA' => [ + 'MMA Weekly' => '/news/mma-weekly.html', + 'MMAFighting' => '/news/mmafighting.html', + 'Reddit MMA' => '/news/reddit-mma.html', + 'Sherdog Articles' => '/news/sherdog-articles.html', + 'FightLand Vice' => '/news/fightland-vice.html', + 'Sherdog Forum' => '/news/sherdog-forum.html', + 'MMA Junkie' => '/news/mma-junkie.html', + 'Sherdog MMA Video' => '/news/sherdog-mma-video.html', + 'BloodyElbow' => '/news/bloodyelbow.html', + 'CageWriter' => '/news/cagewriter.html', + 'Sherdog News' => '/news/sherdog-news.html', + 'MMAForum' => '/news/mmaforum.html', + 'MMA Junkie Radio' => '/news/mma-junkie-radio.html', + 'UFC News' => '/news/ufc-news.html', + 'FightLinker' => '/news/fightlinker.html', + 'Bodybuilding MMA' => '/news/bodybuilding-mma.html', + 'BleacherReport MMA' => '/news/bleacherreport-mma.html', + 'FiveOuncesofPain' => '/news/fiveouncesofpain.html', + 'Sherdog Pictures' => '/news/sherdog-pictures.html', + 'CagePotato' => '/news/cagepotato.html', + 'Sherdog Radio' => '/news/sherdog-radio.html', + 'ProMMARadio' => '/news/prommaradio.html', + ], + 'Mobile' => [ + 'Macrumors' => '/news/macrumors.html', + 'Android Police' => '/news/android-police.html', + 'GSM Arena' => '/news/gsm-arena.html', + 'DigiTrend Mobile' => '/news/digitrend-mobile.html', + 'Mobile Nation' => '/news/mobile-nation.html', + 'TechRadar' => '/news/techradar.html', + 'ZDNET Mobile' => '/news/zdnet-mobile.html', + 'MacWorld' => '/news/macworld.html', + 'Android Dev Blog' => '/news/android-dev-blog.html', + ], + 'News' => [ + 'Daily Mail' => '/news/daily-mail.html', + 'Business Insider' => '/news/business-insider.html', + 'The Guardian' => '/news/the-guardian.html', + 'Fox' => '/news/fox.html', + 'BBC World' => '/news/bbc-world.html', + 'MSNBC' => '/news/msnbc.html', + 'ABC News' => '/news/abc-news.html', + 'Al Jazeera' => '/news/al-jazeera.html', + 'Business Insider India' => '/news/business-insider-india.html', + 'Observer' => '/news/observer.html', + 'NYT Tech' => '/news/nyt-tech.html', + 'NYT World' => '/news/nyt-world.html', + 'CNN' => '/news/cnn.html', + 'Japan Times' => '/news/japan-times.html', + 'WorldCrunch' => '/news/worldcrunch.html', + 'Pro publica' => '/news/pro-publica.html', + 'OZY' => '/news/ozy.html', + 'Times of India' => '/news/times-of-india.html', + 'The Australian' => '/news/the-australian.html', + 'Harpers' => '/news/harpers.html', + 'Moscow Times' => '/news/moscow-times.html', + 'The Times' => '/news/the-times.html', + 'Reuters Tech' => '/news/reuters-tech.html', + ], + 'Politics' => [ + 'FreeRepublic' => '/news/freerepublic.html', + 'Salon' => '/news/salon.html', + 'DrudgeReport' => '/news/drudgereport.html', + 'TheHill' => '/news/thehill.html', + 'TheBlaze' => '/news/theblaze.html', + 'InfoWars' => '/news/infowars.html', + 'New Republic' => '/news/new-republic.html', + 'WashTimes' => '/news/washtimes.html', + 'RealCleanPol' => '/news/realcleanpol.html', + 'Fact Check' => '/news/fact-check.html', + 'DailyKos' => '/news/dailykos.html', + 'NewsMax' => '/news/newsmax.html', + 'Politico' => '/news/politico.html', + 'Michelle Malkin' => '/news/michelle-malkin.html', + ], + 'Reddit' => [ + 'R Movies' => '/news/r-movies.html', + 'R News' => '/news/r-news.html', + 'Futurology' => '/news/futurology.html', + 'R All' => '/news/r-all.html', + 'R Music' => '/news/r-music.html', + 'R Askscience' => '/news/r-askscience.html', + 'R Technology' => '/news/r-technology.html', + 'R Bestof' => '/news/r-bestof.html', + 'R Askreddit' => '/news/r-askreddit.html', + 'R Worldnews' => '/news/r-worldnews.html', + 'R Explainlikeimfive' => '/news/r-explainlikeimfive.html', + 'R Iama' => '/news/r-iama.html', + ], + 'Science' => [ + 'PhysOrg' => '/news/physorg.html', + 'Hack-a-day' => '/news/hack-a-day.html', + 'Reddit Science' => '/news/reddit-science.html', + 'Stats Blog' => '/news/stats-blog.html', + 'Flowing Data' => '/news/flowing-data.html', + 'Eureka Alert' => '/news/eureka-alert.html', + 'Robotics BizRev' => '/news/robotics-bizrev.html', + 'Planet big Data' => '/news/planet-big-data.html', + 'Makezine' => '/news/makezine.html', + 'MIT Tech' => '/news/mit-tech.html', + 'R Bloggers' => '/news/r-bloggers.html', + 'DataIsBeautiful' => '/news/dataisbeautiful.html', + 'Ted Videos' => '/news/ted-videos.html', + 'Advanced Science' => '/news/advanced-science.html', + 'Robotiq' => '/news/robotiq.html', + 'Science Daily' => '/news/science-daily.html', + 'IEEE Robotics' => '/news/ieee-robotics.html', + 'PSFK' => '/news/psfk.html', + 'Discover Magazine' => '/news/discover-magazine.html', + 'DataTau' => '/news/datatau.html', + 'RoboHub' => '/news/robohub.html', + 'Discovery' => '/news/discovery.html', + 'Smart Data' => '/news/smart-data.html', + 'Whats Big Data' => '/news/whats-big-data.html', + ], + 'Tech' => [ + 'Hacker News' => '/news/hacker-news.html', + 'The Verge' => '/news/the-verge.html', + 'Lifehacker' => '/news/lifehacker.html', + 'Fast Company' => '/news/fast-company.html', + 'ArsTechnica' => '/news/arstechnica.html', + 'MakeUseOf' => '/news/makeuseof.html', + 'FastCoExist' => '/news/fastcoexist.html', + 'How to Geek' => '/news/how-to-geek.html', + 'The Next Web' => '/news/the-next-web.html', + 'Engadget' => '/news/engadget.html', + 'Gizmag' => '/news/gizmag.html', + 'QZ' => '/news/qz.html', + 'Wired' => '/news/wired.html', + 'Techcrunch' => '/news/techcrunch.html', + 'Slashdot' => '/news/slashdot.html', + 'Extreme Tech' => '/news/extreme-tech.html', + 'AnandTech' => '/news/anandtech.html', + 'Digital Trends' => '/news/digital-trends.html', + 'Next Big Future' => '/news/next-big-future.html', + 'Apple Insider' => '/news/apple-insider.html', + 'Geek' => '/news/geek.html', + 'BBC Technology' => '/news/bbc-technology.html', + 'Bit-Tech' => '/news/bit-tech.html', + 'Packet Storm Sec' => '/news/packet-storm-sec.html', + 'Design' => '/news/design.html', + 'High Scalability' => '/news/high-scalability.html', + 'Smashing Mag' => '/news/smashing-mag.html', + 'The Tech Block' => '/news/the-tech-block.html', + 'A VC' => '/news/a-vc.html', + 'Tech in Asia' => '/news/tech-in-asia.html', + 'ReadWriteWeb' => '/news/readwriteweb.html', + 'PC Mag' => '/news/pc-mag.html', + 'Continuations' => '/news/continuations.html', + 'Copyblogger' => '/news/copyblogger.html', + 'Cult of Mac' => '/news/cult-of-mac.html', + 'BetaBeat' => '/news/betabeat.html', + 'MedGadget' => '/news/medgadget.html', + 'SecuriTeam' => '/news/securiteam.html', + 'Venture Beat' => '/news/venture-beat.html', + ], + 'Trend' => [ + 'Trend Hunter' => '/news/trend-hunter.html', + 'ApartmentT' => '/news/apartmentt.html', + 'GQ' => '/news/gq.html', + 'Digital Trends' => '/news/digital-trends.html', + 'Cool Hunting' => '/news/cool-hunting.html', + 'FastCoDesign' => '/news/fastcodesign.html', + 'TC Startups' => '/news/tc-startups.html', + 'Killer Startups' => '/news/killer-startups.html', + 'DigiInfo' => '/news/digiinfo.html', + 'New Startups' => '/news/new-startups.html', + 'DigiTrends' => '/news/digitrends.html', + ], + 'Watches' => [ + 'Hodinkee' => '/news/hodinkee.html', + 'Quill and Pad' => '/news/quill-and-pad.html', + 'Monochrome' => '/news/monochrome.html', + 'Deployant' => '/news/deployant.html', + 'Watches by SJX' => '/news/watches-by-sjx.html', + 'Fratello Watches' => '/news/fratello-watches.html', + 'A Blog to Watch' => '/news/a-blog-to-watch.html', + 'Wound for Life' => '/news/wound-for-life.html', + 'Watch Paper' => '/news/watch-paper.html', + 'Watch Report' => '/news/watch-report.html', + 'Perpetuelle' => '/news/perpetuelle.html', + ], + 'Youtube' => [ + 'LinusTechTips' => '/news/linustechtips.html', + 'MetalJesusRocks' => '/news/metaljesusrocks.html', + 'TotalBiscuit' => '/news/totalbiscuit.html', + 'DexBonus' => '/news/dexbonus.html', + 'Lon Siedman' => '/news/lon-siedman.html', + 'MKBHD' => '/news/mkbhd.html', + 'Terry A Davis' => '/news/terry-a-davis.html', + 'HappyConsole' => '/news/happyconsole.html', + 'Austin Evans' => '/news/austin-evans.html', + 'NCIX' => '/news/ncix.html', + ], + ] + ], + ], + self::CONTEXT_CUSTOM => [ + 'config' => [ + 'name' => 'Configuration', + 'type' => 'text', + 'required' => true, + 'title' => 'Enter feed numbers from Skimfeed! e.g: 5,8,2,l,p,9,23', + 'exampleValue' => '5' + ] + ], + 'global' => [ + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'title' => 'Limits the number of returned items in the feed', + 'exampleValue' => 10 + ] + ] + ]; + + public function getURI() + { + switch ($this->queriedContext) { + case self::CONTEXT_NEWS_BOX: + $channel = $this->getInput('box_channel'); + + if ($channel) { + return static::URI . $channel; + } + + break; + + case self::CONTEXT_HOT_TOPICS: + return static::URI; + + case self::CONTEXT_TECH_NEWS: + $channel = $this->getInput('tech_channel'); + + if ($channel) { + return static::URI . $channel; + } + + break; + + case self::CONTEXT_CUSTOM: + $config = $this->getInput('config'); + + return static::URI . '/custom.php?f=' . urlencode($config); + } + + return parent::getURI(); + } + + public function detectParameters($url) + { + if (0 !== strpos($url, static::URI)) { + return null; + } + + foreach (self::PARAMETERS as $channels) { + foreach ($channels as $box_name => $box) { + foreach ($box['values'] as $name => $channel_url) { + if (static::URI . $channel_url === $url) { + return [ + $box_name => $name, + ]; + } + } + } + } + + return null; + } + + public function getName() + { + switch ($this->queriedContext) { + case self::CONTEXT_NEWS_BOX: + $channel = $this->getInput('box_channel'); + + $title = array_search( + $channel, + static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values'] + ); + + return $title . ' - ' . static::NAME; + + case self::CONTEXT_HOT_TOPICS: + return 'Hot topics - ' . static::NAME; + + case self::CONTEXT_TECH_NEWS: + $channel = $this->getInput('tech_channel'); + + $titles = []; + + foreach (static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) { + $titles = array_merge($titles, $ch); + } + + $title = array_search($channel, $titles); + + return $title . ' - ' . static::NAME; + + case self::CONTEXT_CUSTOM: + return 'Custom - ' . static::NAME; + } + + return parent::getName(); + } + + public function collectData() + { + // enable to export parameter lists + // $this->exportBoxChannels(); die; + // $this->exportTechChannels(); die; - case self::CONTEXT_TECH_NEWS: + $html = getSimpleHTMLDOM($this->getURI()); - $channel = $this->getInput('tech_channel'); + defaultLinkTo($html, static::URI); - if($channel) { - return static::URI . $channel; - } + switch ($this->queriedContext) { + case self::CONTEXT_NEWS_BOX: + $author = array_search( + $this->getInput('box_channel'), + static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values'] + ); - break; + $author = '<a href="' + . $this->getURI() + . '">' + . $author + . '</a>'; - case self::CONTEXT_CUSTOM: + $this->extractFeed($html, $author); + break; - $config = $this->getInput('config'); + case self::CONTEXT_HOT_TOPICS: + $this->extractHotTopics($html); + break; - return static::URI . '/custom.php?f=' . urlencode($config); + case self::CONTEXT_TECH_NEWS: + $authors = []; - } + foreach (static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) { + $authors = array_merge($authors, $ch); + } - return parent::getURI(); + $author = '<a href="' + . $this->getURI() + . '">' + . array_search($this->getInput('tech_channel'), $authors) + . '</a>'; - } + $this->extractFeed($html, $author); + break; - public function detectParameters($url) { + case self::CONTEXT_CUSTOM: + $this->extractCustomFeed($html); + break; + } + } - if (0 !== strpos($url, static::URI)) { - return null; - } + private function extractFeed($html, $author) + { + $articles = $html->find('li') + or returnServerError('Could not find articles!'); - foreach(self::PARAMETERS as $channels) { + if ( + count($articles) === 1 + && stristr($articles[0]->plaintext, 'Nothing new in the last 48 hours') + ) { + return; // Nothing to show + } - foreach($channels as $box_name => $box) { + $limit = $this->getInput('limit') ?: -1; - foreach($box['values'] as $name => $channel_url) { + foreach ($articles as $article) { + $anchor = $article->find('a', 0) + or returnServerError('Could not find anchor!'); - if (static::URI . $channel_url === $url) { - return array( - $box_name => $name, - ); + $item = []; - } + $item['uri'] = $this->getTarget($anchor); + $item['title'] = trim($anchor->plaintext); - } + // The timestamp is encoded as relative time (max. the last 48 hours) + // like this: "- 7 hours". It should always be at the end of the article: + $age = substr($article->plaintext, strrpos($article->plaintext, '-')); - } + $item['timestamp'] = strtotime($age); + $item['author'] = $author; - } + $this->items[] = $item; - return null; + if ($limit > 0 && count($this->items) >= $limit) { + return; + } + } + } - } + private function extractHotTopics($html) + { + $topics = $html->find('#popbox ul li') + or returnServerError('Could not find topics!'); + + $limit = $this->getInput('limit') ?: -1; - public function getName() { + foreach ($topics as $topic) { + $anchor = $topic->find('a', 0) + or returnServerError('Could not find anchor!'); + + $item = []; + + $item['uri'] = $this->getTarget($anchor); + $item['title'] = $anchor->title; - switch($this->queriedContext) { + $this->items[] = $item; - case self::CONTEXT_NEWS_BOX: + if ($limit > 0 && count($this->items) >= $limit) { + return; + } + } + } + + private function extractCustomFeed($html) + { + $boxes = $html->find('#boxx .boxes') + or returnServerError('Could not find boxes!'); + + foreach ($boxes as $box) { + $anchor = $box->find('span.boxtitles a', 0) + or returnServerError('Could not find box anchor!'); - $channel = $this->getInput('box_channel'); + $author = '<a href="' . $anchor->href . '">' . trim($anchor->plaintext) . '</a>'; + $uri = $anchor->href; - $title = array_search( - $channel, - static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values'] - ); + $box_html = getSimpleHTMLDOM($uri) + or returnServerError('Could not load custom feed!'); - return $title . ' - ' . static::NAME; + $this->extractFeed($box_html, $author); + } + } - case self::CONTEXT_HOT_TOPICS: - return 'Hot topics - ' . static::NAME; + private function getTarget($anchor) + { + // Anchors are linked to Skimfeed, luckily the target URI is encoded + // in that URI via '&u=<URI>': + $query = parse_url($anchor->href, PHP_URL_QUERY); + + foreach (explode('&', $query) as $parameter) { + list($key, $value) = explode('=', $parameter); - case self::CONTEXT_TECH_NEWS: + if ($key !== 'u') { + continue; + } - $channel = $this->getInput('tech_channel'); + return urldecode($value); + } + } - $titles = array(); + /** + * dev-mode! + * Requires '&format=Html' + * + * Returns the 'box' array from the source site + */ + private function exportBoxChannels() + { + $html = getSimpleHTMLDOMCached(static::URI) + or returnServerError('No contents received from Skimfeed!'); - foreach(static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) { - $titles = array_merge($titles, $ch); - } + if (!$this->isCompatible($html)) { + returnServerError('Skimfeed version is not compatible!'); + } - $title = array_search($channel, $titles); + $boxes = $html->find('#boxx .boxes') + or returnServerError('Could not find boxes!'); - return $title . ' - ' . static::NAME; - - case self::CONTEXT_CUSTOM: - return 'Custom - ' . static::NAME; - - } - - return parent::getName(); - - } - - public function collectData() { - - // enable to export parameter lists - // $this->exportBoxChannels(); die; - // $this->exportTechChannels(); die; - - $html = getSimpleHTMLDOM($this->getURI()); - - defaultLinkTo($html, static::URI); - - switch($this->queriedContext) { - - case self::CONTEXT_NEWS_BOX: - - $author = array_search( - $this->getInput('box_channel'), - static::PARAMETERS[self::CONTEXT_NEWS_BOX]['box_channel']['values'] - ); - - $author = '<a href="' - . $this->getURI() - . '">' - . $author - . '</a>'; - - $this->extractFeed($html, $author); - break; - - case self::CONTEXT_HOT_TOPICS: - $this->extractHotTopics($html); - break; - - case self::CONTEXT_TECH_NEWS: - $authors = array(); - - foreach(static::PARAMETERS[self::CONTEXT_TECH_NEWS]['tech_channel']['values'] as $ch) { - $authors = array_merge($authors, $ch); - } - - $author = '<a href="' - . $this->getURI() - . '">' - . array_search($this->getInput('tech_channel'), $authors) - . '</a>'; - - $this->extractFeed($html, $author); - break; - - case self::CONTEXT_CUSTOM: - $this->extractCustomFeed($html); - break; - - } - - } - - private function extractFeed($html, $author) { - - $articles = $html->find('li') - or returnServerError('Could not find articles!'); - - if(count($articles) === 1 - && stristr($articles[0]->plaintext, 'Nothing new in the last 48 hours')) { - return; // Nothing to show - } - - $limit = $this->getInput('limit') ?: -1; - - foreach($articles as $article) { - - $anchor = $article->find('a', 0) - or returnServerError('Could not find anchor!'); - - $item = array(); - - $item['uri'] = $this->getTarget($anchor); - $item['title'] = trim($anchor->plaintext); - - // The timestamp is encoded as relative time (max. the last 48 hours) - // like this: "- 7 hours". It should always be at the end of the article: - $age = substr($article->plaintext, strrpos($article->plaintext, '-')); - - $item['timestamp'] = strtotime($age); - $item['author'] = $author; - - $this->items[] = $item; - - if($limit > 0 && count($this->items) >= $limit) { - return; - } - - } - - } - - private function extractHotTopics($html) { - - $topics = $html->find('#popbox ul li') - or returnServerError('Could not find topics!'); - - $limit = $this->getInput('limit') ?: -1; - - foreach($topics as $topic) { - - $anchor = $topic->find('a', 0) - or returnServerError('Could not find anchor!'); - - $item = array(); - - $item['uri'] = $this->getTarget($anchor); - $item['title'] = $anchor->title; - - $this->items[] = $item; - - if($limit > 0 && count($this->items) >= $limit) { - return; - } - - } - - } - - private function extractCustomFeed($html) { - - $boxes = $html->find('#boxx .boxes') - or returnServerError('Could not find boxes!'); - - foreach($boxes as $box) { - - $anchor = $box->find('span.boxtitles a', 0) - or returnServerError('Could not find box anchor!'); - - $author = '<a href="' . $anchor->href . '">' . trim($anchor->plaintext) . '</a>'; - $uri = $anchor->href; - - $box_html = getSimpleHTMLDOM($uri) - or returnServerError('Could not load custom feed!'); - - $this->extractFeed($box_html, $author); - - } - - } - - private function getTarget($anchor) { - - // Anchors are linked to Skimfeed, luckily the target URI is encoded - // in that URI via '&u=<URI>': - $query = parse_url($anchor->href, PHP_URL_QUERY); - - foreach(explode('&', $query) as $parameter) { - - list($key, $value) = explode('=', $parameter); - - if($key !== 'u') { - continue; - } - - return urldecode($value); - - } - - } - - /** - * dev-mode! - * Requires '&format=Html' - * - * Returns the 'box' array from the source site - */ - private function exportBoxChannels() { - $html = getSimpleHTMLDOMCached(static::URI) - or returnServerError('No contents received from Skimfeed!'); - - if(!$this->isCompatible($html)) { - returnServerError('Skimfeed version is not compatible!'); - } - - $boxes = $html->find('#boxx .boxes') - or returnServerError('Could not find boxes!'); - - // begin of 'channel' list - $message = <<<EOD + // begin of 'channel' list + $message = <<<EOD 'box_channel' => array( 'name' => 'Channel', 'type' => 'list', @@ -717,26 +685,24 @@ class SkimfeedBridge extends BridgeAbstract { EOD; - foreach($boxes as $box) { - - $anchor = $box->find('span.boxtitles a', 0) - or returnServerError('Could not find box anchor!'); - - $title = trim($anchor->plaintext); - $uri = $anchor->href; + foreach ($boxes as $box) { + $anchor = $box->find('span.boxtitles a', 0) + or returnServerError('Could not find box anchor!'); - // add value - $message .= "\t\t'{$title}' => '{$uri}', \n"; + $title = trim($anchor->plaintext); + $uri = $anchor->href; - } + // add value + $message .= "\t\t'{$title}' => '{$uri}', \n"; + } - // end of 'box' list - $message .= <<<EOD + // end of 'box' list + $message .= <<<EOD ) ), EOD; - echo <<<EOD + echo <<<EOD <!DOCTYPE html> <html> @@ -745,28 +711,28 @@ EOD; </body> </html> EOD; - - } - - /** - * dev-mode! - * Requires '&format=Html' - * - * Returns the 'techs' array from the source site - */ - private function exportTechChannels() { - $html = getSimpleHTMLDOMCached(static::URI) - or returnServerError('No contents received from Skimfeed!'); - - if(!$this->isCompatible($html)) { - returnServerError('Skimfeed version is not compatible!'); - } - - $channels = $html->find('#menubar a') - or returnServerError('Could not find channels!'); - - // begin of 'tech_channel' list - $message = <<<EOD + } + + /** + * dev-mode! + * Requires '&format=Html' + * + * Returns the 'techs' array from the source site + */ + private function exportTechChannels() + { + $html = getSimpleHTMLDOMCached(static::URI) + or returnServerError('No contents received from Skimfeed!'); + + if (!$this->isCompatible($html)) { + returnServerError('Skimfeed version is not compatible!'); + } + + $channels = $html->find('#menubar a') + or returnServerError('Could not find channels!'); + + // begin of 'tech_channel' list + $message = <<<EOD 'tech_channel' => array( 'name' => 'Tech channel', 'type' => 'list', @@ -776,50 +742,48 @@ EOD; EOD; - foreach($channels as $channel) { - - if($channel->href === '#' - || $channel->class === 'homelink' - || $channel->plaintext === 'Twitter' - || $channel->plaintext === 'Weather' - || $channel->plaintext === '+Custom') { - continue; - } - - $title = trim($channel->plaintext); - $uri = '/' . $channel->href; - - $message .= "\t\t'{$title}' => array(\n"; - - $channel_html = getSimpleHTMLDOMCached(static::URI . $uri) - or returnServerError('Could not load tech channel ' . $channel->plaintext . '!'); + foreach ($channels as $channel) { + if ( + $channel->href === '#' + || $channel->class === 'homelink' + || $channel->plaintext === 'Twitter' + || $channel->plaintext === 'Weather' + || $channel->plaintext === '+Custom' + ) { + continue; + } - $boxes = $channel_html->find('#boxx .boxes') - or returnServerError('Could not find boxes!'); + $title = trim($channel->plaintext); + $uri = '/' . $channel->href; - foreach($boxes as $box) { + $message .= "\t\t'{$title}' => array(\n"; - $anchor = $box->find('span.boxtitles a', 0) - or returnServerError('Could not find box anchor!'); + $channel_html = getSimpleHTMLDOMCached(static::URI . $uri) + or returnServerError('Could not load tech channel ' . $channel->plaintext . '!'); - $boxtitle = trim($anchor->plaintext); - $boxuri = $anchor->href; + $boxes = $channel_html->find('#boxx .boxes') + or returnServerError('Could not find boxes!'); - $message .= "\t\t\t'{$boxtitle}' => '{$boxuri}', \n"; + foreach ($boxes as $box) { + $anchor = $box->find('span.boxtitles a', 0) + or returnServerError('Could not find box anchor!'); - } + $boxtitle = trim($anchor->plaintext); + $boxuri = $anchor->href; - $message .= "\t\t),\n"; + $message .= "\t\t\t'{$boxtitle}' => '{$boxuri}', \n"; + } - } + $message .= "\t\t),\n"; + } - // end of 'box' list - $message .= <<<EOD + // end of 'box' list + $message .= <<<EOD ) ), EOD; - echo <<<EOD + echo <<<EOD <!DOCTYPE html> <html> @@ -828,22 +792,23 @@ EOD; </body> </html> EOD; - } + } - /** - * Checks if the reported skimfeed version is compatible - */ - private function isCompatible($html) { - $title = $html->find('title', 0); + /** + * Checks if the reported skimfeed version is compatible + */ + private function isCompatible($html) + { + $title = $html->find('title', 0); - if(!$title) { - return false; - } + if (!$title) { + return false; + } - if($title->plaintext === 'Skimfeed V5.5 - Tech News') { - return true; - } + if ($title->plaintext === 'Skimfeed V5.5 - Tech News') { + return true; + } - return false; - } + return false; + } } diff --git a/bridges/SlusheBridge.php b/bridges/SlusheBridge.php index b05acec5..12bed13a 100644 --- a/bridges/SlusheBridge.php +++ b/bridges/SlusheBridge.php @@ -1,176 +1,179 @@ <?php -class SlusheBridge extends BridgeAbstract { - const MAINTAINER = 'quickwick'; - const NAME = 'Slushe'; - const URI = 'https://slushe.com'; - const DESCRIPTION = 'Returns latest posts from Slushe'; +class SlusheBridge extends BridgeAbstract +{ + const MAINTAINER = 'quickwick'; + const NAME = 'Slushe'; + const URI = 'https://slushe.com'; + const DESCRIPTION = 'Returns latest posts from Slushe'; - const PARAMETERS = array( - 'Artist' => array( - 'artist_name' => array( - 'name' => 'Artist name', - 'required' => true, - 'exampleValue' => 'lexx228', - 'title' => 'Enter an artist name' - ) - ), - 'Category' => array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'defaultValue' => 'Safe for Work', - 'title' => 'Choose a category', - 'values' => array( - '2D' => '29', - '3DX' => '58', - 'Animation' => '60', - 'Anime Fan Art' => '46', - 'BDSM' => '47', - 'Big Butt' => '73', - 'Big Dick' => '52', - 'Bit Tits' => '49', - 'Bisexual' => '69', - 'Comic' => '51', - 'Couple' => '3', - 'Dickgirl/Futanari' => '56', - 'Feet' => '75', - 'Game Fan Art' => '63', - 'Gay' => '36', - 'GIF' => '42', - 'Group Sex/ Orgy' => '62', - 'Lesbian' => '67', - 'Mature' => '72', - 'Misc. Fan Art' => '68', - 'Monster' => '64', - 'Pin-Up' => '28', - 'Safe for Work' => '71', - 'SFM' => '70', - 'Solo' => '66', - 'Threesome' => '38', - 'TV & Film Fan Art' => '34', - 'Western Fan Art' => '33' - ) - ) - ), - 'Search' => array( - 'search_term' => array( - 'name' => 'Search term(s)', - 'required' => true, - 'exampleValue' => 'pole dance', - 'title' => 'Enter one or more search terms, separated by spaces' - ) - ) - ); + const PARAMETERS = [ + 'Artist' => [ + 'artist_name' => [ + 'name' => 'Artist name', + 'required' => true, + 'exampleValue' => 'lexx228', + 'title' => 'Enter an artist name' + ] + ], + 'Category' => [ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'defaultValue' => 'Safe for Work', + 'title' => 'Choose a category', + 'values' => [ + '2D' => '29', + '3DX' => '58', + 'Animation' => '60', + 'Anime Fan Art' => '46', + 'BDSM' => '47', + 'Big Butt' => '73', + 'Big Dick' => '52', + 'Bit Tits' => '49', + 'Bisexual' => '69', + 'Comic' => '51', + 'Couple' => '3', + 'Dickgirl/Futanari' => '56', + 'Feet' => '75', + 'Game Fan Art' => '63', + 'Gay' => '36', + 'GIF' => '42', + 'Group Sex/ Orgy' => '62', + 'Lesbian' => '67', + 'Mature' => '72', + 'Misc. Fan Art' => '68', + 'Monster' => '64', + 'Pin-Up' => '28', + 'Safe for Work' => '71', + 'SFM' => '70', + 'Solo' => '66', + 'Threesome' => '38', + 'TV & Film Fan Art' => '34', + 'Western Fan Art' => '33' + ] + ] + ], + 'Search' => [ + 'search_term' => [ + 'name' => 'Search term(s)', + 'required' => true, + 'exampleValue' => 'pole dance', + 'title' => 'Enter one or more search terms, separated by spaces' + ] + ] + ]; - public function getName(){ - switch($this->queriedContext) { - case 'Artist': - return 'Slushe Artist: ' . $this->getInput('artist_name'); - break; - case 'Category': - return 'Slushe Category: ' . $this->getInput('category'); - break; - case 'Search': - return 'Slushe Search: ' . $this->getInput('search_term'); - break; - default: - return self::NAME; - } - } + public function getName() + { + switch ($this->queriedContext) { + case 'Artist': + return 'Slushe Artist: ' . $this->getInput('artist_name'); + break; + case 'Category': + return 'Slushe Category: ' . $this->getInput('category'); + break; + case 'Search': + return 'Slushe Search: ' . $this->getInput('search_term'); + break; + default: + return self::NAME; + } + } - public function collectData(){ - switch($this->queriedContext) { - case 'Artist': - $uri = self::URI . '/' . $this->getInput('artist_name'); - break; - case 'Category': - $uri = self::URI . '/search/posts/channels?niche=' . - $this->getInput('category'); - break; - case 'Search': - $uri = self::URI . '/search/posts/' . $this->getInput('search_term') . - '?s=1'; - break; - } + public function collectData() + { + switch ($this->queriedContext) { + case 'Artist': + $uri = self::URI . '/' . $this->getInput('artist_name'); + break; + case 'Category': + $uri = self::URI . '/search/posts/channels?niche=' . + $this->getInput('category'); + break; + case 'Search': + $uri = self::URI . '/search/posts/' . $this->getInput('search_term') . + '?s=1'; + break; + } - $headers = array( - 'Authority : slushe.com', - 'Cookie: age-verify=1;', - 'sec-ch-ua: "Chromium";v="100", " Not A;Brand";v="99"', - 'sec-ch-ua-mobile: ?0', - 'sec-ch-ua-platform: "Windows"', - 'sec-fetch-dest: document', - 'sec-fetch-mode: navigate', - 'sec-fetch-site: same-origin', - 'sec-fetch-user: ?1', - 'upgrade-insecure-requests: 1' - ); - // Add user-agent string to headers with implode, due to line length limit - $user_agent_string = [ - 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/', - '537.36(KHTML, like Gecko) Chrome/100.0.4896.147 Safari/537.36' - ]; - $headers[] = implode('', $user_agent_string); + $headers = [ + 'Authority : slushe.com', + 'Cookie: age-verify=1;', + 'sec-ch-ua: "Chromium";v="100", " Not A;Brand";v="99"', + 'sec-ch-ua-mobile: ?0', + 'sec-ch-ua-platform: "Windows"', + 'sec-fetch-dest: document', + 'sec-fetch-mode: navigate', + 'sec-fetch-site: same-origin', + 'sec-fetch-user: ?1', + 'upgrade-insecure-requests: 1' + ]; + // Add user-agent string to headers with implode, due to line length limit + $user_agent_string = [ + 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/', + '537.36(KHTML, like Gecko) Chrome/100.0.4896.147 Safari/537.36' + ]; + $headers[] = implode('', $user_agent_string); - $html = getSimpleHTMLDOM($uri, $headers); + $html = getSimpleHTMLDOM($uri, $headers); - //Debug::log($html); - //Debug::log($html->find('div.blog-item')[0]); + //Debug::log($html); + //Debug::log($html->find('div.blog-item')[0]); - //Loop on each entry - foreach($html->find('div.blog-item') as $element) { - //Debug::log($element); + //Loop on each entry + foreach ($html->find('div.blog-item') as $element) { + //Debug::log($element); - $title = $element->find('h3.title', 0)->first_child()->innertext; - $article_uri = $element->find('h3.title', 0)->first_child()->href; - $timestamp = $element->find('div.publication-date', 0)->innertext; - $author = $element->find('div.artist', 0)-> - first_child()->first_child()->innertext; + $title = $element->find('h3.title', 0)->first_child()->innertext; + $article_uri = $element->find('h3.title', 0)->first_child()->href; + $timestamp = $element->find('div.publication-date', 0)->innertext; + $author = $element->find('div.artist', 0)-> + first_child()->first_child()->innertext; - // Create & populate item - $item = array(); - $item['uri'] = $article_uri; - $item['id'] = $item['uri']; - $item['timestamp'] = $timestamp; - $item['title'] = $title; - $item['author'] = $author; + // Create & populate item + $item = []; + $item['uri'] = $article_uri; + $item['id'] = $item['uri']; + $item['timestamp'] = $timestamp; + $item['title'] = $title; + $item['author'] = $author; - $media_html = ''; + $media_html = ''; - // Look for image thumbnails - $media_uris = $element->find('div.thumb', 0); - if (isset($media_uris)) { - // Add gallery image count, if it exists - $gallery_count = $media_uris->find('span.count', 0); - if (isset($gallery_count)) { - $media_html .= '<p>Gallery count: ' . - $gallery_count->first_child()->innertext . '</p>'; - } - // Add image thumbnail(s) - foreach($media_uris->find('img') as $media_uri) { - $media_html .= '<a href="' . $article_uri . '">' . $media_uri . '</a>'; - //Debug::log('Adding to enclosures: ' . str_replace(' ', '%20', $media_uri->src)); - $item['enclosures'][] = str_replace(' ', '%20', $media_uri->src); - } - } + // Look for image thumbnails + $media_uris = $element->find('div.thumb', 0); + if (isset($media_uris)) { + // Add gallery image count, if it exists + $gallery_count = $media_uris->find('span.count', 0); + if (isset($gallery_count)) { + $media_html .= '<p>Gallery count: ' . + $gallery_count->first_child()->innertext . '</p>'; + } + // Add image thumbnail(s) + foreach ($media_uris->find('img') as $media_uri) { + $media_html .= '<a href="' . $article_uri . '">' . $media_uri . '</a>'; + //Debug::log('Adding to enclosures: ' . str_replace(' ', '%20', $media_uri->src)); + $item['enclosures'][] = str_replace(' ', '%20', $media_uri->src); + } + } - // Look for video thumbnails - $media_uris = $element->find('div.thumb-holder', 0); - // Add video thumbnail(s) - if (isset($media_uris)) { - foreach($media_uris->find('img') as $media_uri) { - $media_html .= '<p>Video:</p><a href="' . - $article_uri . '">' . $media_uri . '</a>'; - //Debug::log('Adding to enclosures: ' . $media_uri->src); - $item['enclosures'][] = $media_uri->src; - } - } - $item['content'] = $media_html; + // Look for video thumbnails + $media_uris = $element->find('div.thumb-holder', 0); + // Add video thumbnail(s) + if (isset($media_uris)) { + foreach ($media_uris->find('img') as $media_uri) { + $media_html .= '<p>Video:</p><a href="' . + $article_uri . '">' . $media_uri . '</a>'; + //Debug::log('Adding to enclosures: ' . $media_uri->src); + $item['enclosures'][] = $media_uri->src; + } + } + $item['content'] = $media_html; - if(isset($item['title'])) { - $this->items[] = $item; - } - } - } + if (isset($item['title'])) { + $this->items[] = $item; + } + } + } } diff --git a/bridges/SoundcloudBridge.php b/bridges/SoundcloudBridge.php index 8df7c5e4..b2fce61d 100644 --- a/bridges/SoundcloudBridge.php +++ b/bridges/SoundcloudBridge.php @@ -1,237 +1,254 @@ <?php -class SoundCloudBridge extends BridgeAbstract { - const MAINTAINER = 'kranack, Roliga'; - const NAME = 'Soundcloud Bridge'; - const URI = 'https://soundcloud.com/'; - const CACHE_TIMEOUT = 600; // 10min - const DESCRIPTION = 'Returns 10 newest music from user profile'; - - const PARAMETERS = array(array( - 'u' => array( - 'name' => 'username', - 'exampleValue' => 'thekidlaroi', - 'required' => true - ), - 't' => array( - 'name' => 'Content', - 'type' => 'list', - 'defaultValue' => 'tracks', - 'values' => array( - 'All (except likes)' => 'all', - 'Tracks' => 'tracks', - 'Albums' => 'albums', - 'Playlists' => 'playlists', - 'Reposts' => 'reposts', - 'Likes' => 'likes' - ) - ) - )); - - private $apiUrl = 'https://api-v2.soundcloud.com/'; - // Without url=http, player URL returns a 404 - private $playerUrl = 'https://w.soundcloud.com/player/?url=http'; - private $widgetUrl = 'https://widget.sndcdn.com/'; - - private $feedTitle = null; - private $feedIcon = null; - private $clientIDCache = null; - - private $clientIdRegex = '/client_id.*?"(.+?)"/'; - private $widgetRegex = '/widget-.+?\.js/'; - - public function collectData() { - $res = $this->getUser($this->getInput('u')); - - $this->feedTitle = $res->username; - $this->feedIcon = $res->avatar_url; - - $apiItems = $this->getUserItems($res->id, $this->getInput('t')) - or returnServerError('No results for ' . $this->getInput('t')); - - $hasTrackObject = array('all', 'reposts', 'likes'); - - foreach ($apiItems->collection as $index => $apiItem) { - if (in_array($this->getInput('t'), $hasTrackObject) === true) { - $apiItem = $apiItem->track; - } - - $item = array(); - $item['author'] = $apiItem->user->username; - $item['title'] = $apiItem->user->username . ' - ' . $apiItem->title; - $item['timestamp'] = strtotime($apiItem->created_at); - - $description = nl2br($apiItem->description); - - $item['content'] = <<<HTML + +class SoundCloudBridge extends BridgeAbstract +{ + const MAINTAINER = 'kranack, Roliga'; + const NAME = 'Soundcloud Bridge'; + const URI = 'https://soundcloud.com/'; + const CACHE_TIMEOUT = 600; // 10min + const DESCRIPTION = 'Returns 10 newest music from user profile'; + + const PARAMETERS = [[ + 'u' => [ + 'name' => 'username', + 'exampleValue' => 'thekidlaroi', + 'required' => true + ], + 't' => [ + 'name' => 'Content', + 'type' => 'list', + 'defaultValue' => 'tracks', + 'values' => [ + 'All (except likes)' => 'all', + 'Tracks' => 'tracks', + 'Albums' => 'albums', + 'Playlists' => 'playlists', + 'Reposts' => 'reposts', + 'Likes' => 'likes' + ] + ] + ]]; + + private $apiUrl = 'https://api-v2.soundcloud.com/'; + // Without url=http, player URL returns a 404 + private $playerUrl = 'https://w.soundcloud.com/player/?url=http'; + private $widgetUrl = 'https://widget.sndcdn.com/'; + + private $feedTitle = null; + private $feedIcon = null; + private $clientIDCache = null; + + private $clientIdRegex = '/client_id.*?"(.+?)"/'; + private $widgetRegex = '/widget-.+?\.js/'; + + public function collectData() + { + $res = $this->getUser($this->getInput('u')); + + $this->feedTitle = $res->username; + $this->feedIcon = $res->avatar_url; + + $apiItems = $this->getUserItems($res->id, $this->getInput('t')) + or returnServerError('No results for ' . $this->getInput('t')); + + $hasTrackObject = ['all', 'reposts', 'likes']; + + foreach ($apiItems->collection as $index => $apiItem) { + if (in_array($this->getInput('t'), $hasTrackObject) === true) { + $apiItem = $apiItem->track; + } + + $item = []; + $item['author'] = $apiItem->user->username; + $item['title'] = $apiItem->user->username . ' - ' . $apiItem->title; + $item['timestamp'] = strtotime($apiItem->created_at); + + $description = nl2br($apiItem->description); + + $item['content'] = <<<HTML <p>{$description}</p> HTML; - if (isset($apiItem->tracks) && $apiItem->track_count > 0) { - $list = $this->getTrackList($apiItem->tracks); + if (isset($apiItem->tracks) && $apiItem->track_count > 0) { + $list = $this->getTrackList($apiItem->tracks); - $item['content'] .= <<<HTML + $item['content'] .= <<<HTML <p><strong>Tracks ({$apiItem->track_count})</strong></p> {$list} HTML; - } - - $item['enclosures'][] = $apiItem->artwork_url; - $item['id'] = $apiItem->permalink_url; - $item['uri'] = $apiItem->permalink_url; - $this->items[] = $item; - - if (count($this->items) >= 10) { - break; - } - } - } - - public function getIcon(){ - if ($this->feedIcon) { - return $this->feedIcon; - } - - return parent::getIcon(); - } - - public function getURI() { - if ($this->getInput('u')) { - return self::URI . $this->getInput('u') . '/' . $this->getInput('t'); - } - - return parent::getURI(); - } - - public function getName() { - if($this->feedTitle) { - return $this->feedTitle . ' - ' . ucfirst($this->getInput('t')) . ' - ' . self::NAME; - } - - return parent::getName(); - } - - private function initClientIDCache(){ - if($this->clientIDCache !== null) - return; - - $cacheFac = new CacheFactory(); - - $this->clientIDCache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $this->clientIDCache->setScope(get_called_class()); - $this->clientIDCache->setKey(array('client_id')); - } - - private function getClientID(){ - $this->initClientIDCache(); - - $clientID = $this->clientIDCache->loadData(); - - if($clientID == null) { - return $this->refreshClientID(); - } else { - return $clientID; - } - } - - private function refreshClientID(){ - $this->initClientIDCache(); - - $playerHTML = getContents($this->playerUrl); - - // Extract widget JS filenames from player page - if(preg_match_all($this->widgetRegex, $playerHTML, $matches) == false) - returnServerError('Unable to find widget JS URL.'); - - $clientID = ''; - - // Loop widget js files and extract client ID - foreach ($matches[0] as $widgetFile) { - $widgetURL = $this->widgetUrl . $widgetFile; - - $widgetJS = getContents($widgetURL); - - if(preg_match($this->clientIdRegex, $widgetJS, $matches)) { - $clientID = $matches[1]; - $this->clientIDCache->saveData($clientID); - - return $clientID; - } - } - - if (empty($clientID)) { - returnServerError('Unable to find client ID.'); - } - } - - private function buildApiUrl($endpoint, $parameters) { - return $this->apiUrl - . $endpoint - . '?' - . http_build_query($parameters); - } - - private function getUser($username) { - $parameters = array('url' => self::URI . $username); - - return $this->getApi('resolve', $parameters); - } - - private function getUserItems($userId, $type) { - $parameters = array('limit' => 10); - $endpoint = 'users/' . $userId . '/' . $type; - - if ($type === 'playlists') { - $endpoint = 'users/' . $userId . '/playlists_without_albums'; - } - - if ($type === 'all') { - $endpoint = 'stream/users/' . $userId; - } - - if ($type === 'reposts') { - $endpoint = 'stream/users/' . $userId . '/' . $type; - } - - return $this->getApi($endpoint, $parameters); - } - - private function getApi($endpoint, $parameters) { - $parameters['client_id'] = $this->getClientID(); - $url = $this->buildApiUrl($endpoint, $parameters); - - try { - return json_decode(getContents($url)); - } catch (Exception $e) { - // Retry once with refreshed client ID - $parameters['client_id'] = $this->refreshClientID(); - $url = $this->buildApiUrl($endpoint, $parameters); - - return json_decode(getContents($url)); - } - } - - private function getTrackList($tracks) { - $trackids = ''; - - foreach ($tracks as $track) { - $trackids .= $track->id . ','; - } - - $apiItems = $this->getApi( - 'tracks', array('ids' => $trackids) - ); - - $list = ''; - foreach($apiItems as $track) { - $list .= <<<HTML + } + + $item['enclosures'][] = $apiItem->artwork_url; + $item['id'] = $apiItem->permalink_url; + $item['uri'] = $apiItem->permalink_url; + $this->items[] = $item; + + if (count($this->items) >= 10) { + break; + } + } + } + + public function getIcon() + { + if ($this->feedIcon) { + return $this->feedIcon; + } + + return parent::getIcon(); + } + + public function getURI() + { + if ($this->getInput('u')) { + return self::URI . $this->getInput('u') . '/' . $this->getInput('t'); + } + + return parent::getURI(); + } + + public function getName() + { + if ($this->feedTitle) { + return $this->feedTitle . ' - ' . ucfirst($this->getInput('t')) . ' - ' . self::NAME; + } + + return parent::getName(); + } + + private function initClientIDCache() + { + if ($this->clientIDCache !== null) { + return; + } + + $cacheFac = new CacheFactory(); + + $this->clientIDCache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $this->clientIDCache->setScope(get_called_class()); + $this->clientIDCache->setKey(['client_id']); + } + + private function getClientID() + { + $this->initClientIDCache(); + + $clientID = $this->clientIDCache->loadData(); + + if ($clientID == null) { + return $this->refreshClientID(); + } else { + return $clientID; + } + } + + private function refreshClientID() + { + $this->initClientIDCache(); + + $playerHTML = getContents($this->playerUrl); + + // Extract widget JS filenames from player page + if (preg_match_all($this->widgetRegex, $playerHTML, $matches) == false) { + returnServerError('Unable to find widget JS URL.'); + } + + $clientID = ''; + + // Loop widget js files and extract client ID + foreach ($matches[0] as $widgetFile) { + $widgetURL = $this->widgetUrl . $widgetFile; + + $widgetJS = getContents($widgetURL); + + if (preg_match($this->clientIdRegex, $widgetJS, $matches)) { + $clientID = $matches[1]; + $this->clientIDCache->saveData($clientID); + + return $clientID; + } + } + + if (empty($clientID)) { + returnServerError('Unable to find client ID.'); + } + } + + private function buildApiUrl($endpoint, $parameters) + { + return $this->apiUrl + . $endpoint + . '?' + . http_build_query($parameters); + } + + private function getUser($username) + { + $parameters = ['url' => self::URI . $username]; + + return $this->getApi('resolve', $parameters); + } + + private function getUserItems($userId, $type) + { + $parameters = ['limit' => 10]; + $endpoint = 'users/' . $userId . '/' . $type; + + if ($type === 'playlists') { + $endpoint = 'users/' . $userId . '/playlists_without_albums'; + } + + if ($type === 'all') { + $endpoint = 'stream/users/' . $userId; + } + + if ($type === 'reposts') { + $endpoint = 'stream/users/' . $userId . '/' . $type; + } + + return $this->getApi($endpoint, $parameters); + } + + private function getApi($endpoint, $parameters) + { + $parameters['client_id'] = $this->getClientID(); + $url = $this->buildApiUrl($endpoint, $parameters); + + try { + return json_decode(getContents($url)); + } catch (Exception $e) { + // Retry once with refreshed client ID + $parameters['client_id'] = $this->refreshClientID(); + $url = $this->buildApiUrl($endpoint, $parameters); + + return json_decode(getContents($url)); + } + } + + private function getTrackList($tracks) + { + $trackids = ''; + + foreach ($tracks as $track) { + $trackids .= $track->id . ','; + } + + $apiItems = $this->getApi( + 'tracks', + ['ids' => $trackids] + ); + + $list = ''; + foreach ($apiItems as $track) { + $list .= <<<HTML <li>{$track->user->username} — <a href="{$track->permalink_url}">{$track->title}</a></li> HTML; - } + } - $html = <<<HTML + $html = <<<HTML <ul>{$list}</ul> HTML; - return $html; - } + return $html; + } } diff --git a/bridges/SplCenterBridge.php b/bridges/SplCenterBridge.php index 492f6a40..396de3b5 100644 --- a/bridges/SplCenterBridge.php +++ b/bridges/SplCenterBridge.php @@ -1,63 +1,66 @@ <?php -class SplCenterBridge extends FeedExpander { - const NAME = 'Southern Poverty Law Center Bridge'; - const URI = 'https://www.splcenter.org'; - const DESCRIPTION = 'Returns the newest posts from the Southern Poverty Law Center'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(array( - 'content' => array( - 'name' => 'Content', - 'type' => 'list', - 'values' => array( - 'News' => 'news', - 'Hatewatch' => 'hatewatch', - ), - 'defaultValue' => 'news', - ) - ) - ); - - const CACHE_TIMEOUT = 3600; // 1 hour - - protected function parseItem($item) { - $item = parent::parseItem($item); - - $articleHtml = getSimpleHTMLDOMCached($item['uri']); - - foreach ($articleHtml->find('.file') as $index => $media) { - $articleHtml->find('div.file', $index)->outertext = '<em>' . $media->outertext . '</em>'; - } - - $item['content'] = $articleHtml->find('div#group-content-container', 0)->innertext; - $item['enclosures'][] = $articleHtml->find('meta[name="twitter:image"]', 0)->content; - - return $item; - } - - public function collectData() { - $this->collectExpandableDatas($this->getURI() . '/rss.xml'); - } - - public function getURI() { - - if (!is_null($this->getInput('content'))) { - return self::URI . '/' . $this->getInput('content'); - } - - return parent::getURI(); - } - - public function getName() { - - if (!is_null($this->getInput('content'))) { - $parameters = $this->getParameters(); - - $contentValues = array_flip($parameters[0]['content']['values']); - - return $contentValues[$this->getInput('content')] . ' - Southern Poverty Law Center'; - } - - return parent::getName(); - } +class SplCenterBridge extends FeedExpander +{ + const NAME = 'Southern Poverty Law Center Bridge'; + const URI = 'https://www.splcenter.org'; + const DESCRIPTION = 'Returns the newest posts from the Southern Poverty Law Center'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [[ + 'content' => [ + 'name' => 'Content', + 'type' => 'list', + 'values' => [ + 'News' => 'news', + 'Hatewatch' => 'hatewatch', + ], + 'defaultValue' => 'news', + ] + ] + ]; + + const CACHE_TIMEOUT = 3600; // 1 hour + + protected function parseItem($item) + { + $item = parent::parseItem($item); + + $articleHtml = getSimpleHTMLDOMCached($item['uri']); + + foreach ($articleHtml->find('.file') as $index => $media) { + $articleHtml->find('div.file', $index)->outertext = '<em>' . $media->outertext . '</em>'; + } + + $item['content'] = $articleHtml->find('div#group-content-container', 0)->innertext; + $item['enclosures'][] = $articleHtml->find('meta[name="twitter:image"]', 0)->content; + + return $item; + } + + public function collectData() + { + $this->collectExpandableDatas($this->getURI() . '/rss.xml'); + } + + public function getURI() + { + if (!is_null($this->getInput('content'))) { + return self::URI . '/' . $this->getInput('content'); + } + + return parent::getURI(); + } + + public function getName() + { + if (!is_null($this->getInput('content'))) { + $parameters = $this->getParameters(); + + $contentValues = array_flip($parameters[0]['content']['values']); + + return $contentValues[$this->getInput('content')] . ' - Southern Poverty Law Center'; + } + + return parent::getName(); + } } diff --git a/bridges/SpotifyBridge.php b/bridges/SpotifyBridge.php index e6598f23..312eddc6 100644 --- a/bridges/SpotifyBridge.php +++ b/bridges/SpotifyBridge.php @@ -1,238 +1,264 @@ <?php -class SpotifyBridge extends BridgeAbstract { - const NAME = 'Spotify'; - const URI = 'https://spotify.com/'; - const DESCRIPTION = 'Fetches the latest ten albums from one or more artists'; - const MAINTAINER = 'Paroleen'; - const CACHE_TIMEOUT = 3600; - const PARAMETERS = array( array( - 'clientid' => array( - 'name' => 'Client ID', - 'type' => 'text', - 'required' => true - ), - 'clientsecret' => array( - 'name' => 'Client secret', - 'type' => 'text', - 'required' => true - ), - 'spotifyuri' => array( - 'name' => 'Spotify URIs', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ [,spotify:artist:3JsMj0DEzyWc0VDlHuy9Bx]', - ), - 'albumtype' => array( - 'name' => 'Album type', - 'type' => 'text', - 'required' => false, - 'exampleValue' => 'album,single,appears_on,compilation', - 'defaultValue' => 'album,single' - ), - 'country' => array( - 'name' => 'Country', - 'type' => 'text', - 'required' => false, - 'exampleValue' => 'US', - 'defaultValue' => 'US' - ) - )); - - const TOKENURI = 'https://accounts.spotify.com/api/token'; - const APIURI = 'https://api.spotify.com/v1/'; - - private $uri = ''; - private $name = ''; - private $token = ''; - private $artists = array(); - private $albums = array(); - - public function getURI() { - if(empty($this->uri)) - $this->getArtist(); - - return $this->uri; - } - - public function getName() { - if(empty($this->name)) - $this->getArtist(); - - return $this->name; - } - - public function getIcon() { - return 'https://www.scdn.co/i/_global/favicon.png'; - } - - private function getId($artist) { - return explode(':', $artist)[2]; - } - - private function getDate($album_date) { - if(strlen($album_date) == 4) - $album_date .= '-01-01'; - elseif(strlen($album_date) == 7) - $album_date .= '-01'; - - return DateTime::createFromFormat('Y-m-d', $album_date)->getTimestamp(); - } - - private function getAlbumType() { - return $this->getInput('albumtype'); - } - - private function getCountry() { - return $this->getInput('country'); - } - - private function getToken() { - $cacheFac = new CacheFactory(); - - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope(get_called_class()); - $cache->setKey(array('token')); - - if($cache->getTime()) { - $time = (new DateTime)->getTimestamp() - $cache->getTime(); - Debug::log('Token time: ' . $time); - } - - if($cache->getTime() == false || $time >= 3600) { - Debug::log('Fetching token from Spotify'); - $this->fetchToken(); - $cache->saveData($this->token); - } else { - Debug::log('Loading token from cache'); - $this->token = $cache->loadData(); - } - - Debug::log('Token: ' . $this->token); - } - - private function getArtist() { - if(!is_null($this->getInput('spotifyuri')) && strpos($this->getInput('spotifyuri'), ',') === false) { - $artist = $this->fetchContent(self::APIURI . 'artists/' - . $this->getId($this->artists[0])); - $this->uri = $artist['external_urls']['spotify']; - $this->name = $artist['name'] . ' - Spotify'; - } else { - $this->uri = parent::getURI(); - $this->name = parent::getName(); - } - } - - private function getAllArtists() { - Debug::log('Parsing all artists'); - $this->artists = explode(',', $this->getInput('spotifyuri')); - } - - private function getAllAlbums() { - $this->albums = array(); - - $this->getAllArtists(); - - Debug::log('Fetching all albums'); - foreach($this->artists as $artist) { - $fetch = true; - $offset = 0; - - while($fetch) { - $partial_albums = $this->fetchContent(self::APIURI . 'artists/' - . $this->getId($artist) - . '/albums?limit=50&include_groups=' - . $this->getAlbumType() - . '&country=' - . $this->getCountry() - . '&offset=' - . $offset); - - if(!empty($partial_albums['items'])) - $this->albums = array_merge($this->albums, - $partial_albums['items']); - else - $fetch = false; - - $offset += 50; - } - } - } - - private function fetchToken() { - $curl = curl_init(); - - curl_setopt($curl, CURLOPT_URL, self::TOKENURI); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials'); - curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Basic ' - . base64_encode($this->getInput('clientid') - . ':' - . $this->getInput('clientsecret')))); - - $json = curl_exec($curl); - $json = json_decode($json)->access_token; - curl_close($curl); - - $this->token = $json; - } - - private function fetchContent($url) { - $this->getToken(); - $curl = curl_init(); - - curl_setopt($curl, CURLOPT_URL, $url); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' - . $this->token)); - - Debug::log('Fetching content from ' . $url); - $json = curl_exec($curl); - $json = json_decode($json, true); - curl_close($curl); - - return $json; - } - - private function sortAlbums() { - Debug::log('Sorting albums'); - usort($this->albums, function($album1, $album2) { - if($this->getDate($album1['release_date']) < $this->getDate($album2['release_date'])) - return 1; - else - return -1; - }); - } - - public function collectData() { - $offset = 0; - - $this->getAllAlbums(); - $this->sortAlbums(); - - Debug::log('Building RSS feed'); - foreach($this->albums as $album) { - $item = array(); - $item['title'] = $album['name']; - $item['uri'] = $album['external_urls']['spotify']; - - $item['timestamp'] = $this->getDate($album['release_date']); - $item['author'] = $album['artists'][0]['name']; - $item['categories'] = array($album['album_type']); - - $item['content'] = '<img style="width: 256px" src="' - . $album['images'][0]['url'] - . '">'; - - if($album['total_tracks'] > 1) - $item['content'] .= '<p>Total tracks: ' - . $album['total_tracks'] - . '</p>'; - - $this->items[] = $item; - - if(count($this->items) >= 10) - break; - } - } + +class SpotifyBridge extends BridgeAbstract +{ + const NAME = 'Spotify'; + const URI = 'https://spotify.com/'; + const DESCRIPTION = 'Fetches the latest ten albums from one or more artists'; + const MAINTAINER = 'Paroleen'; + const CACHE_TIMEOUT = 3600; + const PARAMETERS = [ [ + 'clientid' => [ + 'name' => 'Client ID', + 'type' => 'text', + 'required' => true + ], + 'clientsecret' => [ + 'name' => 'Client secret', + 'type' => 'text', + 'required' => true + ], + 'spotifyuri' => [ + 'name' => 'Spotify URIs', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ [,spotify:artist:3JsMj0DEzyWc0VDlHuy9Bx]', + ], + 'albumtype' => [ + 'name' => 'Album type', + 'type' => 'text', + 'required' => false, + 'exampleValue' => 'album,single,appears_on,compilation', + 'defaultValue' => 'album,single' + ], + 'country' => [ + 'name' => 'Country', + 'type' => 'text', + 'required' => false, + 'exampleValue' => 'US', + 'defaultValue' => 'US' + ] + ]]; + + const TOKENURI = 'https://accounts.spotify.com/api/token'; + const APIURI = 'https://api.spotify.com/v1/'; + + private $uri = ''; + private $name = ''; + private $token = ''; + private $artists = []; + private $albums = []; + + public function getURI() + { + if (empty($this->uri)) { + $this->getArtist(); + } + + return $this->uri; + } + + public function getName() + { + if (empty($this->name)) { + $this->getArtist(); + } + + return $this->name; + } + + public function getIcon() + { + return 'https://www.scdn.co/i/_global/favicon.png'; + } + + private function getId($artist) + { + return explode(':', $artist)[2]; + } + + private function getDate($album_date) + { + if (strlen($album_date) == 4) { + $album_date .= '-01-01'; + } elseif (strlen($album_date) == 7) { + $album_date .= '-01'; + } + + return DateTime::createFromFormat('Y-m-d', $album_date)->getTimestamp(); + } + + private function getAlbumType() + { + return $this->getInput('albumtype'); + } + + private function getCountry() + { + return $this->getInput('country'); + } + + private function getToken() + { + $cacheFac = new CacheFactory(); + + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope(get_called_class()); + $cache->setKey(['token']); + + if ($cache->getTime()) { + $time = (new DateTime())->getTimestamp() - $cache->getTime(); + Debug::log('Token time: ' . $time); + } + + if ($cache->getTime() == false || $time >= 3600) { + Debug::log('Fetching token from Spotify'); + $this->fetchToken(); + $cache->saveData($this->token); + } else { + Debug::log('Loading token from cache'); + $this->token = $cache->loadData(); + } + + Debug::log('Token: ' . $this->token); + } + + private function getArtist() + { + if (!is_null($this->getInput('spotifyuri')) && strpos($this->getInput('spotifyuri'), ',') === false) { + $artist = $this->fetchContent(self::APIURI . 'artists/' + . $this->getId($this->artists[0])); + $this->uri = $artist['external_urls']['spotify']; + $this->name = $artist['name'] . ' - Spotify'; + } else { + $this->uri = parent::getURI(); + $this->name = parent::getName(); + } + } + + private function getAllArtists() + { + Debug::log('Parsing all artists'); + $this->artists = explode(',', $this->getInput('spotifyuri')); + } + + private function getAllAlbums() + { + $this->albums = []; + + $this->getAllArtists(); + + Debug::log('Fetching all albums'); + foreach ($this->artists as $artist) { + $fetch = true; + $offset = 0; + + while ($fetch) { + $partial_albums = $this->fetchContent(self::APIURI . 'artists/' + . $this->getId($artist) + . '/albums?limit=50&include_groups=' + . $this->getAlbumType() + . '&country=' + . $this->getCountry() + . '&offset=' + . $offset); + + if (!empty($partial_albums['items'])) { + $this->albums = array_merge( + $this->albums, + $partial_albums['items'] + ); + } else { + $fetch = false; + } + + $offset += 50; + } + } + } + + private function fetchToken() + { + $curl = curl_init(); + + curl_setopt($curl, CURLOPT_URL, self::TOKENURI); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials'); + curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Basic ' + . base64_encode($this->getInput('clientid') + . ':' + . $this->getInput('clientsecret'))]); + + $json = curl_exec($curl); + $json = json_decode($json)->access_token; + curl_close($curl); + + $this->token = $json; + } + + private function fetchContent($url) + { + $this->getToken(); + $curl = curl_init(); + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' + . $this->token]); + + Debug::log('Fetching content from ' . $url); + $json = curl_exec($curl); + $json = json_decode($json, true); + curl_close($curl); + + return $json; + } + + private function sortAlbums() + { + Debug::log('Sorting albums'); + usort($this->albums, function ($album1, $album2) { + if ($this->getDate($album1['release_date']) < $this->getDate($album2['release_date'])) { + return 1; + } else { + return -1; + } + }); + } + + public function collectData() + { + $offset = 0; + + $this->getAllAlbums(); + $this->sortAlbums(); + + Debug::log('Building RSS feed'); + foreach ($this->albums as $album) { + $item = []; + $item['title'] = $album['name']; + $item['uri'] = $album['external_urls']['spotify']; + + $item['timestamp'] = $this->getDate($album['release_date']); + $item['author'] = $album['artists'][0]['name']; + $item['categories'] = [$album['album_type']]; + + $item['content'] = '<img style="width: 256px" src="' + . $album['images'][0]['url'] + . '">'; + + if ($album['total_tracks'] > 1) { + $item['content'] .= '<p>Total tracks: ' + . $album['total_tracks'] + . '</p>'; + } + + $this->items[] = $item; + + if (count($this->items) >= 10) { + break; + } + } + } } diff --git a/bridges/SpottschauBridge.php b/bridges/SpottschauBridge.php index c9d1952f..a2720274 100644 --- a/bridges/SpottschauBridge.php +++ b/bridges/SpottschauBridge.php @@ -1,39 +1,42 @@ <?php -class SpottschauBridge extends BridgeAbstract { - const NAME = 'Härringers Spottschau Bridge'; - const URI = 'https://spottschau.com/'; - const DESCRIPTION = 'Der Fußball-Comic'; - const MAINTAINER = 'sal0max'; - const PARAMETERS = array(); - const CACHE_TIMEOUT = 3600; // 1 hour +class SpottschauBridge extends BridgeAbstract +{ + const NAME = 'Härringers Spottschau Bridge'; + const URI = 'https://spottschau.com/'; + const DESCRIPTION = 'Der Fußball-Comic'; + const MAINTAINER = 'sal0max'; + const PARAMETERS = []; - public function collectData() { - $html = getSimpleHTMLDOM(self::URI); + const CACHE_TIMEOUT = 3600; // 1 hour - $item = array(); - $item['uri'] = urljoin(self::URI, $html->find('div.strip>a', 0)->attr['href']); - $item['title'] = $html->find('div.text>h2', 0)->innertext; + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $date = preg_replace('/.*, /', '', $item['title']); - $date = preg_replace('/\\d\\d\\.\\//', '', $date); - try { - $item['timestamp'] = DateTime::createFromFormat('d.m.y', $date) - ->setTimezone(new DateTimeZone('Europe/Berlin')) - ->setTime(0, 0) - ->getTimestamp(); - } catch (Throwable $ignored) { - $item['timestamp'] = null; - } + $item = []; + $item['uri'] = urljoin(self::URI, $html->find('div.strip>a', 0)->attr['href']); + $item['title'] = $html->find('div.text>h2', 0)->innertext; - $image = $html->find('div.strip>a>img', 0); - $imageUrl = urljoin(self::URI, $image->attr['src']); - $imageAlt = $image->attr['alt']; + $date = preg_replace('/.*, /', '', $item['title']); + $date = preg_replace('/\\d\\d\\.\\//', '', $date); + try { + $item['timestamp'] = DateTime::createFromFormat('d.m.y', $date) + ->setTimezone(new DateTimeZone('Europe/Berlin')) + ->setTime(0, 0) + ->getTimestamp(); + } catch (Throwable $ignored) { + $item['timestamp'] = null; + } - $item['content'] = <<<EOD + $image = $html->find('div.strip>a>img', 0); + $imageUrl = urljoin(self::URI, $image->attr['src']); + $imageAlt = $image->attr['alt']; + + $item['content'] = <<<EOD <img src="{$imageUrl}" alt="{$imageAlt}"/> <br/> EOD; - $this->items[] = $item; - } + $this->items[] = $item; + } } diff --git a/bridges/StanfordSIRbookreviewBridge.php b/bridges/StanfordSIRbookreviewBridge.php index f57a0b1b..b60f8fc1 100644 --- a/bridges/StanfordSIRbookreviewBridge.php +++ b/bridges/StanfordSIRbookreviewBridge.php @@ -1,42 +1,44 @@ <?php -class StanfordSIRbookreviewBridge extends BridgeAbstract { - const MAINTAINER = 'Kidman1670'; - const NAME = 'StanfordSIRbookreviewBridge'; - const URI = 'https://ssir.org/books/'; - const CACHE_TIMEOUT = 21600; - const DESCRIPTION = 'Return results from SSIR book review.'; - const PARAMETERS = array( array( - 'style' => array( - 'name' => 'style', - 'type' => 'list', - 'values' => array( - 'reviews' => 'reviews', - 'excerpts' => 'excerpts', - ) - ) - ) - ); - public function collectData() { - switch($this->getInput('style')) { - case 'reviews': - $url = self::URI . 'reviews'; - break; - case 'excerpts': - $url = self::URI . 'excerpts'; - break; - } +class StanfordSIRbookreviewBridge extends BridgeAbstract +{ + const MAINTAINER = 'Kidman1670'; + const NAME = 'StanfordSIRbookreviewBridge'; + const URI = 'https://ssir.org/books/'; + const CACHE_TIMEOUT = 21600; + const DESCRIPTION = 'Return results from SSIR book review.'; + const PARAMETERS = [ [ + 'style' => [ + 'name' => 'style', + 'type' => 'list', + 'values' => [ + 'reviews' => 'reviews', + 'excerpts' => 'excerpts', + ] + ] + ] + ]; - $html = getSimpleHTMLDOM($url) - or returnServerError('Failed loading content!'); - foreach($html->find('article') as $element) { - $item = array(); - $item['title'] = $element->find('div > h4 > a', 0)->plaintext; - $item['uri'] = $element->find('div > h4 > a', 0)->href; - $item['content'] = $element->find('div > div.article-entry > p', 2)->plaintext; - $item['author'] = $element->find('div > div > p', 0)->plaintext; - $this->items[] = $item; + public function collectData() + { + switch ($this->getInput('style')) { + case 'reviews': + $url = self::URI . 'reviews'; + break; + case 'excerpts': + $url = self::URI . 'excerpts'; + break; + } - } - } + $html = getSimpleHTMLDOM($url) + or returnServerError('Failed loading content!'); + foreach ($html->find('article') as $element) { + $item = []; + $item['title'] = $element->find('div > h4 > a', 0)->plaintext; + $item['uri'] = $element->find('div > h4 > a', 0)->href; + $item['content'] = $element->find('div > div.article-entry > p', 2)->plaintext; + $item['author'] = $element->find('div > div > p', 0)->plaintext; + $this->items[] = $item; + } + } } diff --git a/bridges/SteamBridge.php b/bridges/SteamBridge.php index 47d1bdac..c4a720fc 100644 --- a/bridges/SteamBridge.php +++ b/bridges/SteamBridge.php @@ -1,121 +1,111 @@ <?php -class SteamBridge extends BridgeAbstract { - const NAME = 'Steam Bridge'; - const URI = 'https://store.steampowered.com/'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Returns apps list'; - const MAINTAINER = 'jacknumber'; - const PARAMETERS = array( - 'Wishlist' => array( - 'userid' => array( - 'name' => 'Steamid64 (find it on steamid.io)', - 'title' => 'User ID (17 digits). Find your user ID with steamid.io or steamidfinder.com', - 'required' => true, - 'exampleValue' => '76561198821231205', - 'pattern' => '[0-9]{17}', - ), - 'only_discount' => array( - 'name' => 'Only discount', - 'type' => 'checkbox', - ) - ) - ); - - public function collectData(){ - - $userid = $this->getInput('userid'); - - $sourceUrl = self::URI . 'wishlist/profiles/' . $userid . '/wishlistdata?p=0'; - $sort = array(); - - $json = getContents($sourceUrl); - - $appsData = json_decode($json); - - foreach($appsData as $id => $element) { - - $appType = $element->type; - $appIsBuyable = 0; - $appHasDiscount = 0; - $appIsFree = 0; - - if($element->subs) { - $appIsBuyable = 1; - $priceBlock = str_get_html($element->subs[0]->discount_block); - $appPrice = str_replace('--', '00', $priceBlock->find('.discount_final_price', 0)->plaintext); - - if($element->subs[0]->discount_pct) { - - $appHasDiscount = 1; - $discountBlock = str_get_html($element->subs[0]->discount_block); - $appDiscountValue = $discountBlock->find('.discount_pct', 0)->plaintext; - $appOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext; - - } else { - - if($this->getInput('only_discount')) { - continue; - } - - } - - } else { - - if($this->getInput('only_discount')) { - continue; - } - - if(isset($element->free) && $element->free = 1) { - $appIsFree = 1; - } - } - - $coverUrl = str_replace('_292x136', '', strtok($element->capsule, '?')); - $picturesPath = pathinfo($coverUrl)['dirname'] . '/'; - - $item = array(); - $item['uri'] = "http://store.steampowered.com/app/$id/"; - $item['title'] = $element->name; - $item['type'] = $appType; - $item['cover'] = $coverUrl; - $item['timestamp'] = $element->added; - $item['isBuyable'] = $appIsBuyable; - $item['hasDiscount'] = $appHasDiscount; - $item['isFree'] = $appIsFree; - $item['priority'] = $element->priority; - - if($appIsBuyable) { - - $item['price'] = floatval(str_replace(',', '.', $appPrice)); - $item['content'] = $appPrice; - - } - - if($appIsFree) { - $item['content'] = 'Free'; - } - - if($appHasDiscount) { - - $item['discount']['value'] = $appDiscountValue; - $item['discount']['oldPrice'] = $appOldPrice; - $item['content'] = '<s>' . $appOldPrice . '</s> <b>' . $appPrice . '</b> (' . $appDiscountValue . ')'; - - } - - $item['enclosures'] = array(); - $item['enclosures'][] = $coverUrl; - - foreach($element->screenshots as $screenshotFileName) { - $item['enclosures'][] = $picturesPath . $screenshotFileName; - } - - $sort[$id] = $element->priority; - - $this->items[] = $item; - } - - array_multisort($sort, SORT_ASC, $this->items); - } +class SteamBridge extends BridgeAbstract +{ + const NAME = 'Steam Bridge'; + const URI = 'https://store.steampowered.com/'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Returns apps list'; + const MAINTAINER = 'jacknumber'; + const PARAMETERS = [ + 'Wishlist' => [ + 'userid' => [ + 'name' => 'Steamid64 (find it on steamid.io)', + 'title' => 'User ID (17 digits). Find your user ID with steamid.io or steamidfinder.com', + 'required' => true, + 'exampleValue' => '76561198821231205', + 'pattern' => '[0-9]{17}', + ], + 'only_discount' => [ + 'name' => 'Only discount', + 'type' => 'checkbox', + ] + ] + ]; + + public function collectData() + { + $userid = $this->getInput('userid'); + + $sourceUrl = self::URI . 'wishlist/profiles/' . $userid . '/wishlistdata?p=0'; + $sort = []; + + $json = getContents($sourceUrl); + + $appsData = json_decode($json); + + foreach ($appsData as $id => $element) { + $appType = $element->type; + $appIsBuyable = 0; + $appHasDiscount = 0; + $appIsFree = 0; + + if ($element->subs) { + $appIsBuyable = 1; + $priceBlock = str_get_html($element->subs[0]->discount_block); + $appPrice = str_replace('--', '00', $priceBlock->find('.discount_final_price', 0)->plaintext); + + if ($element->subs[0]->discount_pct) { + $appHasDiscount = 1; + $discountBlock = str_get_html($element->subs[0]->discount_block); + $appDiscountValue = $discountBlock->find('.discount_pct', 0)->plaintext; + $appOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext; + } else { + if ($this->getInput('only_discount')) { + continue; + } + } + } else { + if ($this->getInput('only_discount')) { + continue; + } + + if (isset($element->free) && $element->free = 1) { + $appIsFree = 1; + } + } + + $coverUrl = str_replace('_292x136', '', strtok($element->capsule, '?')); + $picturesPath = pathinfo($coverUrl)['dirname'] . '/'; + + $item = []; + $item['uri'] = "http://store.steampowered.com/app/$id/"; + $item['title'] = $element->name; + $item['type'] = $appType; + $item['cover'] = $coverUrl; + $item['timestamp'] = $element->added; + $item['isBuyable'] = $appIsBuyable; + $item['hasDiscount'] = $appHasDiscount; + $item['isFree'] = $appIsFree; + $item['priority'] = $element->priority; + + if ($appIsBuyable) { + $item['price'] = floatval(str_replace(',', '.', $appPrice)); + $item['content'] = $appPrice; + } + + if ($appIsFree) { + $item['content'] = 'Free'; + } + + if ($appHasDiscount) { + $item['discount']['value'] = $appDiscountValue; + $item['discount']['oldPrice'] = $appOldPrice; + $item['content'] = '<s>' . $appOldPrice . '</s> <b>' . $appPrice . '</b> (' . $appDiscountValue . ')'; + } + + $item['enclosures'] = []; + $item['enclosures'][] = $coverUrl; + + foreach ($element->screenshots as $screenshotFileName) { + $item['enclosures'][] = $picturesPath . $screenshotFileName; + } + + $sort[$id] = $element->priority; + + $this->items[] = $item; + } + + array_multisort($sort, SORT_ASC, $this->items); + } } diff --git a/bridges/SteamCommunityBridge.php b/bridges/SteamCommunityBridge.php index b0f08cf0..37ed555d 100644 --- a/bridges/SteamCommunityBridge.php +++ b/bridges/SteamCommunityBridge.php @@ -1,191 +1,209 @@ <?php -class SteamCommunityBridge extends BridgeAbstract { - const NAME = 'Steam Community'; - const URI = 'https://www.steamcommunity.com'; - const DESCRIPTION = 'Get the latest community updates for a game on Steam.'; - const MAINTAINER = 'thefranke'; - const CACHE_TIMEOUT = 3600; // 1h - - const PARAMETERS = array( - array( - 'i' => array( - 'name' => 'App ID', - 'exampleValue' => '730', - 'required' => true - ), - 'category' => array( - 'name' => 'category', - 'type' => 'list', - 'exampleValue' => 'Artwork', - 'title' => 'Select a category', - 'values' => array( - 'Artwork' => 'images', - 'Screenshots' => 'screenshots', - 'Videos' => 'videos', - 'Workshop' => 'workshop' - ) - ) - ) - ); - - public function getIcon() { - return self::URI . '/favicon.ico'; - } - - protected function getMainPage() { - $category = $this->getInput('category'); - $html = getSimpleHTMLDOM($this->getURI()); - - return $html; - } - - public function getName() { - $category = $this->getInput('category'); - - if (is_null('i') || is_null($category)) { - return self::NAME; - } - - $html = $this->getMainPage(); - - $titleItem = $html->find('div.apphub_AppName', 0); - - if (!$titleItem) - return self::NAME; - - return $titleItem->innertext . ' (' . ucwords($category) . ')'; - } - - public function getURI() { - if ($this->getInput('category') === 'workshop') - return self::URI . '/workshop/browse/?appid=' - . $this->getInput('i') . '&browsesort=mostrecent'; - - return self::URI . '/app/' - . $this->getInput('i') . '/' - . $this->getInput('category') - . '/?p=1&browsefilter=mostrecent'; - } - - private function collectMedia() { - $category = $this->getInput('category'); - $html = $this->getMainPage(); - $cards = $html->find('div.apphub_Card'); - - foreach($cards as $card) { - $uri = $card->getAttribute('data-modal-content-url'); - - $htmlCard = getSimpleHTMLDOMCached($uri); - - $author = $card->find('div.apphub_CardContentAuthorName', 0)->innertext; - $author = strip_tags($author); - - $title = $author . '\'s screenshot'; - - if ($category != 'screenshots') - $title = $htmlCard->find('div.workshopItemTitle', 0)->innertext; - - $date = $htmlCard->find('div.detailsStatRight', 0)->innertext; - - // create item - $item = array(); - $item['title'] = $title; - $item['uri'] = $uri; - $item['timestamp'] = strtotime($date); - $item['author'] = $author; - $item['categories'] = $category; - - $media = $htmlCard->getElementById('ActualMedia'); - $mediaURI = $media->getAttribute('src'); - $downloadURI = $mediaURI; - - if ($category == 'videos') { - preg_match('/.*\/embed\/(.*)\?/', $mediaURI, $result); - $youtubeID = $result[1]; - $mediaURI = 'https://img.youtube.com/vi/' . $youtubeID . '/hqdefault.jpg'; - $downloadURI = 'https://www.youtube.com/watch?v=' . $youtubeID; - } - - $desc = ''; - - if ($category == 'screenshots') { - $descItem = $htmlCard->find('div.screenshotDescription', 0); - if ($descItem) - $desc = $descItem->innertext; - } - if ($category == 'images') { - $descItem = $htmlCard->find('div.nonScreenshotDescription', 0); - if ($descItem) - $desc = $descItem->innertext; - $downloadURI = $htmlCard->find('a.downloadImage', 0)->href; - } - - $item['content'] = '<p><a href="' . $downloadURI . '"><img src="' . $mediaURI . '"/></a></p>'; - $item['content'] .= '<p>' . $desc . '</p>'; - - $this->items[] = $item; - - if (count($this->items) >= 10) - break; - } - } - - private function collectWorkshop() { - $category = $this->getInput('category'); - $html = $this->getMainPage(); - $workShopItems = $html->find('div.workshopItem'); - - foreach($workShopItems as $workShopItem) { - $author = $workShopItem->find('div.workshopItemAuthorName', 0)->find('a', 0); - $author = $author->innertext; - - $fileRating = $workShopItem->find('img.fileRating', 0); - - $uri = $workShopItem->find('a.ugc', 0)->getAttribute('href'); - - $htmlItem = getSimpleHTMLDOMCached($uri); - - $title = $htmlItem->find('div.workshopItemTitle', 0)->innertext; - $date = $htmlItem->find('div.detailsStatRight', 0)->innertext; - $description = $htmlItem->find('div.workshopItemDescription', 0)->innertext; - - $previewImage = $htmlItem->find('#previewImage', 0); - - $htmlTags = $htmlItem->find('div.workshopTags'); - - $tags = ''; - - foreach($htmlTags as $htmlTag) { - if ($tags !== '') - $tags .= ','; - - $tags .= $htmlTag->find('a', 0)->innertext; - } - - // create item - $item = array(); - $item['title'] = $title; - $item['uri'] = $uri; - $item['timestamp'] = strtotime($date); - $item['author'] = $author; - $item['categories'] = $category; - - $item['content'] = '<p><a href="' . $uri . '">' - . $previewImage . '</a></p><p>' . $fileRating - . '</p><p>' . $description . '</p>'; - - $this->items[] = $item; - - if (count($this->items) >= 10) - break; - } - } - - public function collectData() { - if ($this->getInput('category') === 'workshop') - $this->collectWorkshop(); - else - $this->collectMedia(); - } +class SteamCommunityBridge extends BridgeAbstract +{ + const NAME = 'Steam Community'; + const URI = 'https://www.steamcommunity.com'; + const DESCRIPTION = 'Get the latest community updates for a game on Steam.'; + const MAINTAINER = 'thefranke'; + const CACHE_TIMEOUT = 3600; // 1h + + const PARAMETERS = [ + [ + 'i' => [ + 'name' => 'App ID', + 'exampleValue' => '730', + 'required' => true + ], + 'category' => [ + 'name' => 'category', + 'type' => 'list', + 'exampleValue' => 'Artwork', + 'title' => 'Select a category', + 'values' => [ + 'Artwork' => 'images', + 'Screenshots' => 'screenshots', + 'Videos' => 'videos', + 'Workshop' => 'workshop' + ] + ] + ] + ]; + + public function getIcon() + { + return self::URI . '/favicon.ico'; + } + + protected function getMainPage() + { + $category = $this->getInput('category'); + $html = getSimpleHTMLDOM($this->getURI()); + + return $html; + } + + public function getName() + { + $category = $this->getInput('category'); + + if (is_null('i') || is_null($category)) { + return self::NAME; + } + + $html = $this->getMainPage(); + + $titleItem = $html->find('div.apphub_AppName', 0); + + if (!$titleItem) { + return self::NAME; + } + + return $titleItem->innertext . ' (' . ucwords($category) . ')'; + } + + public function getURI() + { + if ($this->getInput('category') === 'workshop') { + return self::URI . '/workshop/browse/?appid=' + . $this->getInput('i') . '&browsesort=mostrecent'; + } + + return self::URI . '/app/' + . $this->getInput('i') . '/' + . $this->getInput('category') + . '/?p=1&browsefilter=mostrecent'; + } + + private function collectMedia() + { + $category = $this->getInput('category'); + $html = $this->getMainPage(); + $cards = $html->find('div.apphub_Card'); + + foreach ($cards as $card) { + $uri = $card->getAttribute('data-modal-content-url'); + + $htmlCard = getSimpleHTMLDOMCached($uri); + + $author = $card->find('div.apphub_CardContentAuthorName', 0)->innertext; + $author = strip_tags($author); + + $title = $author . '\'s screenshot'; + + if ($category != 'screenshots') { + $title = $htmlCard->find('div.workshopItemTitle', 0)->innertext; + } + + $date = $htmlCard->find('div.detailsStatRight', 0)->innertext; + + // create item + $item = []; + $item['title'] = $title; + $item['uri'] = $uri; + $item['timestamp'] = strtotime($date); + $item['author'] = $author; + $item['categories'] = $category; + + $media = $htmlCard->getElementById('ActualMedia'); + $mediaURI = $media->getAttribute('src'); + $downloadURI = $mediaURI; + + if ($category == 'videos') { + preg_match('/.*\/embed\/(.*)\?/', $mediaURI, $result); + $youtubeID = $result[1]; + $mediaURI = 'https://img.youtube.com/vi/' . $youtubeID . '/hqdefault.jpg'; + $downloadURI = 'https://www.youtube.com/watch?v=' . $youtubeID; + } + + $desc = ''; + + if ($category == 'screenshots') { + $descItem = $htmlCard->find('div.screenshotDescription', 0); + if ($descItem) { + $desc = $descItem->innertext; + } + } + + if ($category == 'images') { + $descItem = $htmlCard->find('div.nonScreenshotDescription', 0); + if ($descItem) { + $desc = $descItem->innertext; + } + $downloadURI = $htmlCard->find('a.downloadImage', 0)->href; + } + + $item['content'] = '<p><a href="' . $downloadURI . '"><img src="' . $mediaURI . '"/></a></p>'; + $item['content'] .= '<p>' . $desc . '</p>'; + + $this->items[] = $item; + + if (count($this->items) >= 10) { + break; + } + } + } + + private function collectWorkshop() + { + $category = $this->getInput('category'); + $html = $this->getMainPage(); + $workShopItems = $html->find('div.workshopItem'); + + foreach ($workShopItems as $workShopItem) { + $author = $workShopItem->find('div.workshopItemAuthorName', 0)->find('a', 0); + $author = $author->innertext; + + $fileRating = $workShopItem->find('img.fileRating', 0); + + $uri = $workShopItem->find('a.ugc', 0)->getAttribute('href'); + + $htmlItem = getSimpleHTMLDOMCached($uri); + + $title = $htmlItem->find('div.workshopItemTitle', 0)->innertext; + $date = $htmlItem->find('div.detailsStatRight', 0)->innertext; + $description = $htmlItem->find('div.workshopItemDescription', 0)->innertext; + + $previewImage = $htmlItem->find('#previewImage', 0); + + $htmlTags = $htmlItem->find('div.workshopTags'); + + $tags = ''; + + foreach ($htmlTags as $htmlTag) { + if ($tags !== '') { + $tags .= ','; + } + + $tags .= $htmlTag->find('a', 0)->innertext; + } + + // create item + $item = []; + $item['title'] = $title; + $item['uri'] = $uri; + $item['timestamp'] = strtotime($date); + $item['author'] = $author; + $item['categories'] = $category; + + $item['content'] = '<p><a href="' . $uri . '">' + . $previewImage . '</a></p><p>' . $fileRating + . '</p><p>' . $description . '</p>'; + + $this->items[] = $item; + + if (count($this->items) >= 10) { + break; + } + } + } + + public function collectData() + { + if ($this->getInput('category') === 'workshop') { + $this->collectWorkshop(); + } else { + $this->collectMedia(); + } + } } diff --git a/bridges/StockFilingsBridge.php b/bridges/StockFilingsBridge.php index f774244a..2817dd97 100644 --- a/bridges/StockFilingsBridge.php +++ b/bridges/StockFilingsBridge.php @@ -1,80 +1,86 @@ <?php -class StockFilingsBridge extends FeedExpander { - const MAINTAINER = 'captn3m0'; - const NAME = 'SEC Stock filings'; - const URI = 'https://www.sec.gov/edgar/searchedgar/companysearch.html'; - const CACHE_TIMEOUT = 3600; // 1h - const DESCRIPTION = 'Tracks SEC Filings for a single company'; - const SEARCH_URL = 'https://www.sec.gov/cgi-bin/browse-edgar?owner=exclude&action=getcompany&CIK='; - const WEBSITE_ROOT = 'https://www.sec.gov'; +class StockFilingsBridge extends FeedExpander +{ + const MAINTAINER = 'captn3m0'; + const NAME = 'SEC Stock filings'; + const URI = 'https://www.sec.gov/edgar/searchedgar/companysearch.html'; + const CACHE_TIMEOUT = 3600; // 1h + const DESCRIPTION = 'Tracks SEC Filings for a single company'; + const SEARCH_URL = 'https://www.sec.gov/cgi-bin/browse-edgar?owner=exclude&action=getcompany&CIK='; + const WEBSITE_ROOT = 'https://www.sec.gov'; - const PARAMETERS = array( - array( - 'ticker' => array( - 'name' => 'cik', - 'required' => true, - 'exampleValue' => 'AMD', - // https://stackoverflow.com/a/12827734 - 'pattern' => '[A-Za-z0-9]+', - ), - )); + const PARAMETERS = [ + [ + 'ticker' => [ + 'name' => 'cik', + 'required' => true, + 'exampleValue' => 'AMD', + // https://stackoverflow.com/a/12827734 + 'pattern' => '[A-Za-z0-9]+', + ], + ]]; - public function getIcon() { - return 'https://www.sec.gov/favicon.ico'; - } + public function getIcon() + { + return 'https://www.sec.gov/favicon.ico'; + } - /** - * Generates search URL - */ - private function getSearchUrl() { - return self::SEARCH_URL . $this->getInput('ticker'); - } + /** + * Generates search URL + */ + private function getSearchUrl() + { + return self::SEARCH_URL . $this->getInput('ticker'); + } - /** - * Returns the Company Name - */ - private function getRssFeed($html) { - $links = $html->find('#contentDiv a'); + /** + * Returns the Company Name + */ + private function getRssFeed($html) + { + $links = $html->find('#contentDiv a'); - foreach ($links as $link) { - $href = $link->href; + foreach ($links as $link) { + $href = $link->href; - if (substr($href, 0, 4) !== 'http') { - $href = self::WEBSITE_ROOT . $href; - } - parse_str(html_entity_decode(parse_url($href, PHP_URL_QUERY)), $query); + if (substr($href, 0, 4) !== 'http') { + $href = self::WEBSITE_ROOT . $href; + } + parse_str(html_entity_decode(parse_url($href, PHP_URL_QUERY)), $query); - if (isset($query['output']) and ($query['output'] == 'atom')) { - return $href; - } - } + if (isset($query['output']) and ($query['output'] == 'atom')) { + return $href; + } + } - return false; - } + return false; + } - /** - * Return \simple_html_dom object - * for the entire html of the product page - */ - private function getHtml() { - $uri = $this->getSearchUrl(); + /** + * Return \simple_html_dom object + * for the entire html of the product page + */ + private function getHtml() + { + $uri = $this->getSearchUrl(); - return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request SEC.'); - } + return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request SEC.'); + } - /** - * Scrape the SEC Stock Filings RSS Feed URL - * and redirect there - */ - public function collectData() { - $html = $this->getHtml(); - $rssFeedUrl = $this->getRssFeed($html); + /** + * Scrape the SEC Stock Filings RSS Feed URL + * and redirect there + */ + public function collectData() + { + $html = $this->getHtml(); + $rssFeedUrl = $this->getRssFeed($html); - if ($rssFeedUrl) { - parent::collectExpandableDatas($rssFeedUrl); - } else { - returnClientError('Could not find RSS Feed URL. Are you sure you used a valid CIK?'); - } - } + if ($rssFeedUrl) { + parent::collectExpandableDatas($rssFeedUrl); + } else { + returnClientError('Could not find RSS Feed URL. Are you sure you used a valid CIK?'); + } + } } diff --git a/bridges/StripeAPIChangeLogBridge.php b/bridges/StripeAPIChangeLogBridge.php index 1db34c14..43b80e01 100644 --- a/bridges/StripeAPIChangeLogBridge.php +++ b/bridges/StripeAPIChangeLogBridge.php @@ -1,22 +1,25 @@ <?php -class StripeAPIChangeLogBridge extends BridgeAbstract { - const MAINTAINER = 'Pierre Mazière'; - const NAME = 'Stripe API Changelog'; - const URI = 'https://stripe.com/docs/upgrades'; - const CACHE_TIMEOUT = 86400; // 24h - const DESCRIPTION = 'Returns the changes made to the stripe.com API'; - public function collectData(){ - $html = getSimpleHTMLDOM(self::URI); +class StripeAPIChangeLogBridge extends BridgeAbstract +{ + const MAINTAINER = 'Pierre Mazière'; + const NAME = 'Stripe API Changelog'; + const URI = 'https://stripe.com/docs/upgrades'; + const CACHE_TIMEOUT = 86400; // 24h + const DESCRIPTION = 'Returns the changes made to the stripe.com API'; - foreach($html->find('h3') as $change) { - $item = array(); - $item['title'] = trim($change->plaintext); - $item['uri'] = self::URI . '#' . $item['title']; - $item['author'] = 'stripe'; - $item['content'] = $change->nextSibling()->outertext; - $item['timestamp'] = strtotime($item['title']); - $this->items[] = $item; - } - } + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); + + foreach ($html->find('h3') as $change) { + $item = []; + $item['title'] = trim($change->plaintext); + $item['uri'] = self::URI . '#' . $item['title']; + $item['author'] = 'stripe'; + $item['content'] = $change->nextSibling()->outertext; + $item['timestamp'] = strtotime($item['title']); + $this->items[] = $item; + } + } } diff --git a/bridges/SummitsOnTheAirBridge.php b/bridges/SummitsOnTheAirBridge.php index 83383330..53bba7ab 100644 --- a/bridges/SummitsOnTheAirBridge.php +++ b/bridges/SummitsOnTheAirBridge.php @@ -1,37 +1,38 @@ <?php -class SummitsOnTheAirBridge extends BridgeAbstract { - const MAINTAINER = 's0lesurviv0r'; - const NAME = 'Summits On The Air Spots'; - const URI = 'https://api2.sota.org.uk/api/spots/'; - const CACHE_TIMEOUT = 60; // 1m - const DESCRIPTION = 'Summits On The Air Activator Spots'; - - const PARAMETERS = array( - 'Count' => array( - 'c' => array( - 'name' => 'count', - 'required' => true, - 'defaultValue' => 10 - ) - ) - ); - - public function collectData() - { - $header = array('Content-type:application/json'); - $opts = array(CURLOPT_HTTPGET => 1); - $json = getContents($this->getURI() . $this->getInput('c'), $header, $opts); - - $spots = json_decode($json, true); - - foreach ($spots as $spot) { - $summit = $spot['associationCode'] . '/' . $spot['summitCode']; - - $title = $spot['activatorCallsign'] . ' @ ' . $summit . ' ' . - $spot['frequency'] . ' MHz'; - - $content = <<<EOL +class SummitsOnTheAirBridge extends BridgeAbstract +{ + const MAINTAINER = 's0lesurviv0r'; + const NAME = 'Summits On The Air Spots'; + const URI = 'https://api2.sota.org.uk/api/spots/'; + const CACHE_TIMEOUT = 60; // 1m + const DESCRIPTION = 'Summits On The Air Activator Spots'; + + const PARAMETERS = [ + 'Count' => [ + 'c' => [ + 'name' => 'count', + 'required' => true, + 'defaultValue' => 10 + ] + ] + ]; + + public function collectData() + { + $header = ['Content-type:application/json']; + $opts = [CURLOPT_HTTPGET => 1]; + $json = getContents($this->getURI() . $this->getInput('c'), $header, $opts); + + $spots = json_decode($json, true); + + foreach ($spots as $spot) { + $summit = $spot['associationCode'] . '/' . $spot['summitCode']; + + $title = $spot['activatorCallsign'] . ' @ ' . $summit . ' ' . + $spot['frequency'] . ' MHz'; + + $content = <<<EOL <a href="http://summits.sota.org.uk/summit/{$summit}"> {$summit}, {$spot['summitDetails']}</a><br /> Frequency: {$spot['frequency']} MHz<br /> @@ -39,12 +40,12 @@ class SummitsOnTheAirBridge extends BridgeAbstract { Comments: {$spot['comments']} EOL; - $this->items[] = array( - 'uri' => 'https://sotawatch.sota.org.uk/en/', - 'title' => $title, - 'content' => $content, - 'timestamp' => $spot['timeStamp'] - ); - } - } + $this->items[] = [ + 'uri' => 'https://sotawatch.sota.org.uk/en/', + 'title' => $title, + 'content' => $content, + 'timestamp' => $spot['timeStamp'] + ]; + } + } } diff --git a/bridges/SuperSmashBlogBridge.php b/bridges/SuperSmashBlogBridge.php index fd3ace63..4a6478ea 100644 --- a/bridges/SuperSmashBlogBridge.php +++ b/bridges/SuperSmashBlogBridge.php @@ -1,45 +1,46 @@ <?php -class SuperSmashBlogBridge extends BridgeAbstract { - const MAINTAINER = 'corenting'; - const NAME = 'Super Smash Blog'; - const URI = 'https://www.smashbros.com/en_US/blog/index.html'; - const CACHE_TIMEOUT = 7200; // 2h - const DESCRIPTION = 'Latest articles from the Super Smash Blog blog'; - - public function collectData(){ - $dlUrl = 'https://www.smashbros.com/data/bs/en_US/json/en_US.json'; - - $jsonString = getContents($dlUrl); - $json = json_decode($jsonString, true); - - foreach($json as $article) { - - // Build content - $picture = $article['acf']['image1']['url']; - if (strlen($picture) != 0) { - $picture = str_get_html('<img src="https://www.smashbros.com/' . substr($picture, 8) . '"/>'); - } else { - $picture = ''; - } - - $video = $article['acf']['link_url']; - if (strlen($video) != 0) { - $video = str_get_html('<a href="' . $video . '">Youtube video</a>'); - } else { - $video = ''; - } - $text = str_get_html($article['acf']['editor']); - $content = $picture . $video . $text; - - // Build final item - $item = array(); - $item['title'] = $article['title']['rendered']; - $item['timestamp'] = strtotime($article['date']); - $item['content'] = $content; - $item['uri'] = self::URI . '?post=' . $article['id']; - - $this->items[] = $item; - } - } +class SuperSmashBlogBridge extends BridgeAbstract +{ + const MAINTAINER = 'corenting'; + const NAME = 'Super Smash Blog'; + const URI = 'https://www.smashbros.com/en_US/blog/index.html'; + const CACHE_TIMEOUT = 7200; // 2h + const DESCRIPTION = 'Latest articles from the Super Smash Blog blog'; + + public function collectData() + { + $dlUrl = 'https://www.smashbros.com/data/bs/en_US/json/en_US.json'; + + $jsonString = getContents($dlUrl); + $json = json_decode($jsonString, true); + + foreach ($json as $article) { + // Build content + $picture = $article['acf']['image1']['url']; + if (strlen($picture) != 0) { + $picture = str_get_html('<img src="https://www.smashbros.com/' . substr($picture, 8) . '"/>'); + } else { + $picture = ''; + } + + $video = $article['acf']['link_url']; + if (strlen($video) != 0) { + $video = str_get_html('<a href="' . $video . '">Youtube video</a>'); + } else { + $video = ''; + } + $text = str_get_html($article['acf']['editor']); + $content = $picture . $video . $text; + + // Build final item + $item = []; + $item['title'] = $article['title']['rendered']; + $item['timestamp'] = strtotime($article['date']); + $item['content'] = $content; + $item['uri'] = self::URI . '?post=' . $article['id']; + + $this->items[] = $item; + } + } } diff --git a/bridges/SymfonyCastsBridge.php b/bridges/SymfonyCastsBridge.php index 63b908ce..29ba87cd 100644 --- a/bridges/SymfonyCastsBridge.php +++ b/bridges/SymfonyCastsBridge.php @@ -1,33 +1,34 @@ <?php -class SymfonyCastsBridge extends BridgeAbstract { - const NAME = 'SymfonyCasts Bridge'; - const URI = 'https://symfonycasts.com/'; - const DESCRIPTION = 'Follow new updates on symfonycasts.com'; - const MAINTAINER = 'Park0'; - const CACHE_TIMEOUT = 3600; +class SymfonyCastsBridge extends BridgeAbstract +{ + const NAME = 'SymfonyCasts Bridge'; + const URI = 'https://symfonycasts.com/'; + const DESCRIPTION = 'Follow new updates on symfonycasts.com'; + const MAINTAINER = 'Park0'; + const CACHE_TIMEOUT = 3600; - public function collectData() { - $html = getSimpleHTMLDOM('https://symfonycasts.com/updates/find'); - $dives = $html->find('div'); + public function collectData() + { + $html = getSimpleHTMLDOM('https://symfonycasts.com/updates/find'); + $dives = $html->find('div'); - /* @var simple_html_dom $div */ - foreach ($dives as $div) { - $id = $div->getAttribute('data-mark-update-id-value'); - $type = $div->find('h5', 0); - $title = $div->find('span', 0); - $dateString = $div->find('h5.font-gray', 0); - $href = $div->find('a', 0); - $url = 'https://symfonycasts.com' . $href->getAttribute('href'); + /* @var simple_html_dom $div */ + foreach ($dives as $div) { + $id = $div->getAttribute('data-mark-update-id-value'); + $type = $div->find('h5', 0); + $title = $div->find('span', 0); + $dateString = $div->find('h5.font-gray', 0); + $href = $div->find('a', 0); + $url = 'https://symfonycasts.com' . $href->getAttribute('href'); - $item = array(); // Create an empty item - $item['uid'] = $id; - $item['title'] = $title->innertext; - $item['timestamp'] = $dateString->innertext; - $item['content'] = $type->plaintext . '<a href="' . $url . '">' . $title . '</a>'; - $item['uri'] = $url; - $this->items[] = $item; // Add item to the list - } - - } + $item = []; // Create an empty item + $item['uid'] = $id; + $item['title'] = $title->innertext; + $item['timestamp'] = $dateString->innertext; + $item['content'] = $type->plaintext . '<a href="' . $url . '">' . $title . '</a>'; + $item['uri'] = $url; + $this->items[] = $item; // Add item to the list + } + } } diff --git a/bridges/TbibBridge.php b/bridges/TbibBridge.php index 509b1e11..88f046fc 100644 --- a/bridges/TbibBridge.php +++ b/bridges/TbibBridge.php @@ -1,15 +1,16 @@ <?php -class TbibBridge extends GelbooruBridge { +class TbibBridge extends GelbooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Tbib'; + const URI = 'https://tbib.org/'; + const DESCRIPTION = 'Returns images from given page'; - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Tbib'; - const URI = 'https://tbib.org/'; - const DESCRIPTION = 'Returns images from given page'; - - protected function buildThumbnailURI($element){ - $regex = '/\.\w+$/'; - return $this->getURI() . 'thumbnails/' . $element->directory - . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image); - } + protected function buildThumbnailURI($element) + { + $regex = '/\.\w+$/'; + return $this->getURI() . 'thumbnails/' . $element->directory + . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image); + } } diff --git a/bridges/TebeoBridge.php b/bridges/TebeoBridge.php index ba44d0e2..aef303c4 100644 --- a/bridges/TebeoBridge.php +++ b/bridges/TebeoBridge.php @@ -1,41 +1,45 @@ <?php -class TebeoBridge extends FeedExpander { - const NAME = 'Tébéo Bridge'; - const URI = 'http://www.tebeo.bzh/'; - const CACHE_TIMEOUT = 21600; //6h - const DESCRIPTION = 'Returns the newest Tébéo videos by category'; - const MAINTAINER = 'Mitsukarenai'; - const PARAMETERS = array( array( - 'cat' => array( - 'name' => 'Catégorie', - 'type' => 'list', - 'values' => array( - 'Toutes les vidéos' => '/', - 'Actualité' => '/14-actualite', - 'Sport' => '/3-sport', - 'Culture-Loisirs' => '/5-culture-loisirs', - 'Société' => '/15-societe', - 'Langue Bretonne' => '/9-langue-bretonne' - ) - ) - )); +class TebeoBridge extends FeedExpander +{ + const NAME = 'Tébéo Bridge'; + const URI = 'http://www.tebeo.bzh/'; + const CACHE_TIMEOUT = 21600; //6h + const DESCRIPTION = 'Returns the newest Tébéo videos by category'; + const MAINTAINER = 'Mitsukarenai'; - public function getIcon() { - return self::URI . 'images/header_logo.png'; - } + const PARAMETERS = [ [ + 'cat' => [ + 'name' => 'Catégorie', + 'type' => 'list', + 'values' => [ + 'Toutes les vidéos' => '/', + 'Actualité' => '/14-actualite', + 'Sport' => '/3-sport', + 'Culture-Loisirs' => '/5-culture-loisirs', + 'Société' => '/15-societe', + 'Langue Bretonne' => '/9-langue-bretonne' + ] + ] + ]]; - public function collectData(){ - $url = self::URI . '/le-replay/' . $this->getInput('cat'); - $html = getSimpleHTMLDOM($url); + public function getIcon() + { + return self::URI . 'images/header_logo.png'; + } - foreach($html->find('div[id=items_replay] div.replay') as $element) { - $item = array(); - $item['uri'] = $element->find('a', 0)->href; - $item['title'] = $element->find('h3', 0)->plaintext; - $item['timestamp'] = strtotime($element->find('p.moment-format-day', 0)->plaintext); - $item['content'] = '<a href="' . $item['uri'] . '"><img alt="" src="' . $element->find('img', 0)->src . '"></a>'; - $this->items[] = $item; - } - } + public function collectData() + { + $url = self::URI . '/le-replay/' . $this->getInput('cat'); + $html = getSimpleHTMLDOM($url); + + foreach ($html->find('div[id=items_replay] div.replay') as $element) { + $item = []; + $item['uri'] = $element->find('a', 0)->href; + $item['title'] = $element->find('h3', 0)->plaintext; + $item['timestamp'] = strtotime($element->find('p.moment-format-day', 0)->plaintext); + $item['content'] = '<a href="' . $item['uri'] . '"><img alt="" src="' . $element->find('img', 0)->src . '"></a>'; + $this->items[] = $item; + } + } } diff --git a/bridges/TelegramBridge.php b/bridges/TelegramBridge.php index 415bc636..a980f57a 100644 --- a/bridges/TelegramBridge.php +++ b/bridges/TelegramBridge.php @@ -1,355 +1,372 @@ <?php -class TelegramBridge extends BridgeAbstract { - const NAME = 'Telegram Bridge'; - const URI = 'https://t.me'; - const DESCRIPTION = 'Returns newest posts from a public Telegram channel'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(array( - 'username' => array( - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '@rssbridge', - ) - ) - ); - const TEST_DETECT_PARAMETERS = array( - 'https://t.me/s/durov' => array('username' => 'durov'), - 'https://t.me/durov' => array('username' => 'durov'), - 'http://t.me/durov' => array('username' => 'durov'), - ); - - const CACHE_TIMEOUT = 900; // 15 mins - - private $feedName = ''; - private $enclosures = array(); - private $itemTitle = ''; - - private $backgroundImageRegex = "/background-image:url\('(.*)'\)/"; - private $detectParamsRegex = '/^https?:\/\/t.me\/(?:s\/)?([\w]+)$/'; - - public function detectParameters($url) { - $params = array(); - - if(preg_match($this->detectParamsRegex, $url, $matches) > 0) { - $params['username'] = $matches[1]; - return $params; - } - - return null; - } - - public function collectData() { - - $html = getSimpleHTMLDOM($this->getURI()); - - $channelTitle = htmlspecialchars_decode( - $html->find('div.tgme_channel_info_header_title span', 0)->plaintext, - ENT_QUOTES - ); - - $this->feedName = $channelTitle . ' (@' . $this->processUsername() . ')'; - - foreach($html->find('div.tgme_widget_message_wrap.js-widget_message_wrap') as $index => $messageDiv) { - $this->itemTitle = ''; - $this->enclosures = array(); - $item = array(); - - $item['uri'] = $this->processUri($messageDiv); - $item['content'] = $this->processContent($messageDiv); - $item['title'] = $this->itemTitle; - $item['timestamp'] = $this->processDate($messageDiv); - $item['enclosures'] = $this->enclosures; - $author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext); - $item['author'] = html_entity_decode($author, ENT_QUOTES); - - $this->items[] = $item; - } - $this->items = array_reverse($this->items); - } - - public function getURI() { - if (!is_null($this->getInput('username'))) { - return self::URI . '/s/' . $this->processUsername(); - } - - return parent::getURI(); - } - - public function getName() { - if (!empty($this->feedName)) { - return $this->feedName . ' - Telegram'; - } - - return parent::getName(); - } - - private function processUsername() { - if (substr($this->getInput('username'), 0, 1) === '@') { - return substr($this->getInput('username'), 1); - } - - return $this->getInput('username'); - } - - private function processUri($messageDiv) { - return $messageDiv->find('a.tgme_widget_message_date', 0)->href; - } - - private function processDate($messageDiv) { - $messageMeta = $messageDiv->find('span.tgme_widget_message_meta', 0); - return $messageMeta->find('time', 0)->datetime; - } - - private function processContent($messageDiv) { - $message = ''; - - if ($messageDiv->find('div.tgme_widget_message_forwarded_from', 0)) { - $message = $messageDiv->find('div.tgme_widget_message_forwarded_from', 0)->innertext . '<br><br>'; - } - - if ($messageDiv->find('a.tgme_widget_message_reply', 0)) { - $message .= $this->processReply($messageDiv); - } - if ($messageDiv->find('div.tgme_widget_message_sticker_wrap', 0)) { - $message .= $this->processSticker($messageDiv); - } - - if ($messageDiv->find('div.tgme_widget_message_poll', 0)) { - $message .= $this->processPoll($messageDiv); - } - - if ($messageDiv->find('video', 0)) { - $message .= $this->processVideo($messageDiv); - } - - if ($messageDiv->find('a.tgme_widget_message_photo_wrap', 0)) { - $message .= $this->processPhoto($messageDiv); - } - - if ($messageDiv->find('a.not_supported', 0)) { - $message .= $this->processNotSupported($messageDiv); - } - - if ($messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)) { - $message .= $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0); - - $this->itemTitle = $this->ellipsisTitle( - $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)->plaintext - ); - } - - if ($messageDiv->find('div.tgme_widget_message_document', 0)) { - $message .= $this->processAttachment($messageDiv); - } - - if ($messageDiv->find('a.tgme_widget_message_link_preview', 0)) { - $message .= $this->processLinkPreview($messageDiv); - } - - if ($messageDiv->find('a.tgme_widget_message_location_wrap', 0)) { - $message .= $this->processLocation($messageDiv); - } - - return $message; - } - - private function processReply($messageDiv) { - $reply = $messageDiv->find('a.tgme_widget_message_reply', 0); - $author = $reply->find('span.tgme_widget_message_author_name', 0)->plaintext; - $text = ''; - - if ($reply->find('div.tgme_widget_message_metatext', 0)) { - $text = $reply->find('div.tgme_widget_message_metatext', 0)->innertext; - } - - if ($reply->find('div.tgme_widget_message_text', 0)) { - $text = $reply->find('div.tgme_widget_message_text', 0)->innertext; - } - - return <<<EOD +class TelegramBridge extends BridgeAbstract +{ + const NAME = 'Telegram Bridge'; + const URI = 'https://t.me'; + const DESCRIPTION = 'Returns newest posts from a public Telegram channel'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [[ + 'username' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '@rssbridge', + ] + ] + ]; + const TEST_DETECT_PARAMETERS = [ + 'https://t.me/s/durov' => ['username' => 'durov'], + 'https://t.me/durov' => ['username' => 'durov'], + 'http://t.me/durov' => ['username' => 'durov'], + ]; + + const CACHE_TIMEOUT = 900; // 15 mins + + private $feedName = ''; + private $enclosures = []; + private $itemTitle = ''; + + private $backgroundImageRegex = "/background-image:url\('(.*)'\)/"; + private $detectParamsRegex = '/^https?:\/\/t.me\/(?:s\/)?([\w]+)$/'; + + public function detectParameters($url) + { + $params = []; + + if (preg_match($this->detectParamsRegex, $url, $matches) > 0) { + $params['username'] = $matches[1]; + return $params; + } + + return null; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $channelTitle = htmlspecialchars_decode( + $html->find('div.tgme_channel_info_header_title span', 0)->plaintext, + ENT_QUOTES + ); + + $this->feedName = $channelTitle . ' (@' . $this->processUsername() . ')'; + + foreach ($html->find('div.tgme_widget_message_wrap.js-widget_message_wrap') as $index => $messageDiv) { + $this->itemTitle = ''; + $this->enclosures = []; + $item = []; + + $item['uri'] = $this->processUri($messageDiv); + $item['content'] = $this->processContent($messageDiv); + $item['title'] = $this->itemTitle; + $item['timestamp'] = $this->processDate($messageDiv); + $item['enclosures'] = $this->enclosures; + $author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext); + $item['author'] = html_entity_decode($author, ENT_QUOTES); + + $this->items[] = $item; + } + $this->items = array_reverse($this->items); + } + + public function getURI() + { + if (!is_null($this->getInput('username'))) { + return self::URI . '/s/' . $this->processUsername(); + } + + return parent::getURI(); + } + + public function getName() + { + if (!empty($this->feedName)) { + return $this->feedName . ' - Telegram'; + } + + return parent::getName(); + } + + private function processUsername() + { + if (substr($this->getInput('username'), 0, 1) === '@') { + return substr($this->getInput('username'), 1); + } + + return $this->getInput('username'); + } + + private function processUri($messageDiv) + { + return $messageDiv->find('a.tgme_widget_message_date', 0)->href; + } + + private function processDate($messageDiv) + { + $messageMeta = $messageDiv->find('span.tgme_widget_message_meta', 0); + return $messageMeta->find('time', 0)->datetime; + } + + private function processContent($messageDiv) + { + $message = ''; + + if ($messageDiv->find('div.tgme_widget_message_forwarded_from', 0)) { + $message = $messageDiv->find('div.tgme_widget_message_forwarded_from', 0)->innertext . '<br><br>'; + } + + if ($messageDiv->find('a.tgme_widget_message_reply', 0)) { + $message .= $this->processReply($messageDiv); + } + + if ($messageDiv->find('div.tgme_widget_message_sticker_wrap', 0)) { + $message .= $this->processSticker($messageDiv); + } + + if ($messageDiv->find('div.tgme_widget_message_poll', 0)) { + $message .= $this->processPoll($messageDiv); + } + + if ($messageDiv->find('video', 0)) { + $message .= $this->processVideo($messageDiv); + } + + if ($messageDiv->find('a.tgme_widget_message_photo_wrap', 0)) { + $message .= $this->processPhoto($messageDiv); + } + + if ($messageDiv->find('a.not_supported', 0)) { + $message .= $this->processNotSupported($messageDiv); + } + + if ($messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)) { + $message .= $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0); + + $this->itemTitle = $this->ellipsisTitle( + $messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)->plaintext + ); + } + + if ($messageDiv->find('div.tgme_widget_message_document', 0)) { + $message .= $this->processAttachment($messageDiv); + } + + if ($messageDiv->find('a.tgme_widget_message_link_preview', 0)) { + $message .= $this->processLinkPreview($messageDiv); + } + + if ($messageDiv->find('a.tgme_widget_message_location_wrap', 0)) { + $message .= $this->processLocation($messageDiv); + } + + return $message; + } + + private function processReply($messageDiv) + { + $reply = $messageDiv->find('a.tgme_widget_message_reply', 0); + $author = $reply->find('span.tgme_widget_message_author_name', 0)->plaintext; + $text = ''; + + if ($reply->find('div.tgme_widget_message_metatext', 0)) { + $text = $reply->find('div.tgme_widget_message_metatext', 0)->innertext; + } + + if ($reply->find('div.tgme_widget_message_text', 0)) { + $text = $reply->find('div.tgme_widget_message_text', 0)->innertext; + } + + return <<<EOD <blockquote>{$author}<br> {$text} <a href="{$reply->href}">{$reply->href}</a></blockquote><hr> EOD; - } - - private function processSticker($messageDiv) { - if (empty($this->itemTitle)) { - $this->itemTitle = '@' . $this->processUsername() . ' posted a sticker'; - } - - $stickerDiv = $messageDiv->find('div.tgme_widget_message_sticker_wrap', 0); + } - if ($stickerDiv->find('picture', 0)) { - $stickerDiv->find('picture', 0)->find('div', 0)->style = ''; - $stickerDiv->find('picture', 0)->style = ''; + private function processSticker($messageDiv) + { + if (empty($this->itemTitle)) { + $this->itemTitle = '@' . $this->processUsername() . ' posted a sticker'; + } - return $stickerDiv; + $stickerDiv = $messageDiv->find('div.tgme_widget_message_sticker_wrap', 0); - } elseif (preg_match($this->backgroundImageRegex, $stickerDiv->find('i', 0)->style, $sticker)) { + if ($stickerDiv->find('picture', 0)) { + $stickerDiv->find('picture', 0)->find('div', 0)->style = ''; + $stickerDiv->find('picture', 0)->style = ''; - return <<<EOD + return $stickerDiv; + } elseif (preg_match($this->backgroundImageRegex, $stickerDiv->find('i', 0)->style, $sticker)) { + return <<<EOD <a href="{$stickerDiv->children(0)->herf}"><img src="{$sticker[1]}"></a> EOD; - } - } + } + } - private function processPoll($messageDiv) { + private function processPoll($messageDiv) + { + $poll = $messageDiv->find('div.tgme_widget_message_poll', 0); - $poll = $messageDiv->find('div.tgme_widget_message_poll', 0); + $title = $poll->find('div.tgme_widget_message_poll_question', 0)->plaintext; + $type = $poll->find('div.tgme_widget_message_poll_type', 0)->plaintext; - $title = $poll->find('div.tgme_widget_message_poll_question', 0)->plaintext; - $type = $poll->find('div.tgme_widget_message_poll_type', 0)->plaintext; + if (empty($this->itemTitle)) { + $this->itemTitle = $title; + } - if (empty($this->itemTitle)) { - $this->itemTitle = $title; - } + $pollOptions = '<ul>'; - $pollOptions = '<ul>'; + foreach ($poll->find('div.tgme_widget_message_poll_option') as $option) { + $pollOptions .= '<li>' . $option->children(0)->plaintext . ' - ' . + $option->find('div.tgme_widget_message_poll_option_text', 0)->plaintext . '</li>'; + } + $pollOptions .= '</ul>'; - foreach ($poll->find('div.tgme_widget_message_poll_option') as $option) { - $pollOptions .= '<li>' . $option->children(0)->plaintext . ' - ' . - $option->find('div.tgme_widget_message_poll_option_text', 0)->plaintext . '</li>'; - } - $pollOptions .= '</ul>'; - - return <<<EOD + return <<<EOD {$title}<br><small>$type</small><br>{$pollOptions} EOD; - } - - private function processLinkPreview($messageDiv) { - $image = ''; - $title = ''; - $site = ''; - $description = ''; + } - $preview = $messageDiv->find('a.tgme_widget_message_link_preview', 0); + private function processLinkPreview($messageDiv) + { + $image = ''; + $title = ''; + $site = ''; + $description = ''; - if (trim($preview->innertext) === '') { - return ''; - } + $preview = $messageDiv->find('a.tgme_widget_message_link_preview', 0); - if($preview->find('i', 0) && - preg_match($this->backgroundImageRegex, $preview->find('i', 0)->style, $photo)) { + if (trim($preview->innertext) === '') { + return ''; + } - $image = '<img src="' . $photo[1] . '"/>'; - } + if ( + $preview->find('i', 0) && + preg_match($this->backgroundImageRegex, $preview->find('i', 0)->style, $photo) + ) { + $image = '<img src="' . $photo[1] . '"/>'; + } - if ($preview->find('div.link_preview_title', 0)) { - $title = $preview->find('div.link_preview_title', 0)->plaintext; - } + if ($preview->find('div.link_preview_title', 0)) { + $title = $preview->find('div.link_preview_title', 0)->plaintext; + } - if ($preview->find('div.link_preview_site_name', 0)) { - $site = $preview->find('div.link_preview_site_name', 0)->plaintext; - } + if ($preview->find('div.link_preview_site_name', 0)) { + $site = $preview->find('div.link_preview_site_name', 0)->plaintext; + } - if ($preview->find('div.link_preview_description', 0)) { - $description = $preview->find('div.link_preview_description', 0)->plaintext; - } + if ($preview->find('div.link_preview_description', 0)) { + $description = $preview->find('div.link_preview_description', 0)->plaintext; + } - return <<<EOD + return <<<EOD <blockquote><a href="{$preview->href}">{$image}</a><br><a href="{$preview->href}"> {$title} - {$site}</a><br>{$description}</blockquote> EOD; - } + } - private function processVideo($messageDiv) { - if (empty($this->itemTitle)) { - $this->itemTitle = '@' . $this->processUsername() . ' posted a video'; - } + private function processVideo($messageDiv) + { + if (empty($this->itemTitle)) { + $this->itemTitle = '@' . $this->processUsername() . ' posted a video'; + } - if ($messageDiv->find('i.tgme_widget_message_video_thumb')) { - preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo); - } elseif ($messageDiv->find('i.link_preview_video_thumb')) { - preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo); - } + if ($messageDiv->find('i.tgme_widget_message_video_thumb')) { + preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo); + } elseif ($messageDiv->find('i.link_preview_video_thumb')) { + preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo); + } - $this->enclosures[] = $photo[1]; + $this->enclosures[] = $photo[1]; - return <<<EOD + return <<<EOD <video controls="" poster="{$photo[1]}" style="max-width:100%;" preload="none"> <source src="{$messageDiv->find('video', 0)->src}" type="video/mp4"> </video> EOD; - } + } - private function processPhoto($messageDiv) { - if (empty($this->itemTitle)) { - $this->itemTitle = '@' . $this->processUsername() . ' posted a photo'; - } + private function processPhoto($messageDiv) + { + if (empty($this->itemTitle)) { + $this->itemTitle = '@' . $this->processUsername() . ' posted a photo'; + } - $photos = ''; + $photos = ''; - foreach ($messageDiv->find('a.tgme_widget_message_photo_wrap') as $photoWrap) { - preg_match($this->backgroundImageRegex, $photoWrap->style, $photo); + foreach ($messageDiv->find('a.tgme_widget_message_photo_wrap') as $photoWrap) { + preg_match($this->backgroundImageRegex, $photoWrap->style, $photo); - $photos .= <<<EOD + $photos .= <<<EOD <a href="{$photoWrap->href}"><img src="{$photo[1]}"/></a><br> EOD; - } - return $photos; - } - - private function processNotSupported($messageDiv) { - if (empty($this->itemTitle)) { - $this->itemTitle = '@' . $this->processUsername() . ' posted a video'; - } - - if ($messageDiv->find('i.tgme_widget_message_video_thumb')) { - preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo); - } elseif ($messageDiv->find('i.link_preview_video_thumb')) { - preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo); - } - - return <<<EOD + } + return $photos; + } + + private function processNotSupported($messageDiv) + { + if (empty($this->itemTitle)) { + $this->itemTitle = '@' . $this->processUsername() . ' posted a video'; + } + + if ($messageDiv->find('i.tgme_widget_message_video_thumb')) { + preg_match($this->backgroundImageRegex, $messageDiv->find('i.tgme_widget_message_video_thumb', 0)->style, $photo); + } elseif ($messageDiv->find('i.link_preview_video_thumb')) { + preg_match($this->backgroundImageRegex, $messageDiv->find('i.link_preview_video_thumb', 0)->style, $photo); + } + + return <<<EOD <a href="{$messageDiv->find('a.not_supported', 0)->href}"> {$messageDiv->find('div.message_media_not_supported_label', 0)->innertext}<br><br> {$messageDiv->find('span.message_media_view_in_telegram', 0)->innertext}<br><br> <img src="{$photo[1]}"/></a> EOD; - } + } - private function processAttachment($messageDiv) { - $attachments = 'File attachments:<br>'; + private function processAttachment($messageDiv) + { + $attachments = 'File attachments:<br>'; - if (empty($this->itemTitle)) { - $this->itemTitle = '@' . $this->processUsername() . ' posted an attachment'; - } + if (empty($this->itemTitle)) { + $this->itemTitle = '@' . $this->processUsername() . ' posted an attachment'; + } - foreach ($messageDiv->find('div.tgme_widget_message_document') as $document) { - $attachments .= <<<EOD + foreach ($messageDiv->find('div.tgme_widget_message_document') as $document) { + $attachments .= <<<EOD {$document->find('div.tgme_widget_message_document_title', 0)->plaintext} - {$document->find('div.tgme_widget_message_document_extra', 0)->plaintext}<br> EOD; - } + } - return $attachments; - } + return $attachments; + } - private function processLocation($messageDiv) { - if (empty($this->itemTitle)) { - $this->itemTitle = '@' . $this->processUsername() . ' posted a location'; - } + private function processLocation($messageDiv) + { + if (empty($this->itemTitle)) { + $this->itemTitle = '@' . $this->processUsername() . ' posted a location'; + } - preg_match($this->backgroundImageRegex, $messageDiv->find('div.tgme_widget_message_location', 0)->style, $image); + preg_match($this->backgroundImageRegex, $messageDiv->find('div.tgme_widget_message_location', 0)->style, $image); - $link = $messageDiv->find('a.tgme_widget_message_location_wrap', 0)->href; + $link = $messageDiv->find('a.tgme_widget_message_location_wrap', 0)->href; - return <<<EOD + return <<<EOD <a href="{$link}"><img src="{$image[1]}"></a> EOD; - } - - private function ellipsisTitle($text) { - $length = 100; - - if (strlen($text) > $length) { - $text = explode('<br>', wordwrap($text, $length, '<br>')); - return $text[0] . '...'; - } - return $text; - } + } + + private function ellipsisTitle($text) + { + $length = 100; + + if (strlen($text) > $length) { + $text = explode('<br>', wordwrap($text, $length, '<br>')); + return $text[0] . '...'; + } + return $text; + } } diff --git a/bridges/TheFarSideBridge.php b/bridges/TheFarSideBridge.php index f8e5a37f..cd3ad9ae 100644 --- a/bridges/TheFarSideBridge.php +++ b/bridges/TheFarSideBridge.php @@ -1,49 +1,52 @@ <?php -class TheFarSideBridge extends BridgeAbstract { - const NAME = 'The Far Side Bridge'; - const URI = 'https://www.thefarside.com'; - const DESCRIPTION = 'Returns the daily dose'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(); - const CACHE_TIMEOUT = 3600; // 1 hour +class TheFarSideBridge extends BridgeAbstract +{ + const NAME = 'The Far Side Bridge'; + const URI = 'https://www.thefarside.com'; + const DESCRIPTION = 'Returns the daily dose'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = []; - public function collectData() { - $html = getSimpleHTMLDOM(self::URI); + const CACHE_TIMEOUT = 3600; // 1 hour - $div = $html->find('div.tfs-page-container__cows', 0); + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); - $item = array(); - $item['uri'] = $html->find('meta[property="og:url"]', 0)->content; - $item['title'] = $div->find('h3', 0)->innertext; - $item['timestamp'] = $div->find('h3', 0)->innertext; - $item['content'] = ''; + $div = $html->find('div.tfs-page-container__cows', 0); - foreach($div->find('div.card-body') as $index => $card) { - $image = $card->find('img', 0); - $imageUrl = $image->attr['data-src']; + $item = []; + $item['uri'] = $html->find('meta[property="og:url"]', 0)->content; + $item['title'] = $div->find('h3', 0)->innertext; + $item['timestamp'] = $div->find('h3', 0)->innertext; + $item['content'] = ''; - // Images are downloaded to bypass the hotlink protection. - $image = getContents($imageUrl, array('Referer: ' . self::URI)); + foreach ($div->find('div.card-body') as $index => $card) { + $image = $card->find('img', 0); + $imageUrl = $image->attr['data-src']; - // Encode image as base64 - $imageBase64 = base64_encode($image); + // Images are downloaded to bypass the hotlink protection. + $image = getContents($imageUrl, ['Referer: ' . self::URI]); - $caption = ''; + // Encode image as base64 + $imageBase64 = base64_encode($image); - if ($card->find('figcaption', 0)) { - $caption = $card->find('figcaption', 0)->innertext; - } + $caption = ''; - $item['content'] .= <<<EOD + if ($card->find('figcaption', 0)) { + $caption = $card->find('figcaption', 0)->innertext; + } + + $item['content'] .= <<<EOD <figure> <img title="{$caption}" src="data:image/jpeg;base64,{$imageBase64}"/> <figcaption>{$caption}</figcaption> </figure> <br/> EOD; - } + } - $this->items[] = $item; - } + $this->items[] = $item; + } } diff --git a/bridges/TheGuardianBridge.php b/bridges/TheGuardianBridge.php index e655f0ef..d3b1147c 100644 --- a/bridges/TheGuardianBridge.php +++ b/bridges/TheGuardianBridge.php @@ -1,96 +1,101 @@ <?php -class TheGuardianBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'The Guardian Bridge'; - const URI = 'https://www.theguardian.com/'; - const CACHE_TIMEOUT = 600; // This is a news site, so don't cache for more than 10 mins - const DESCRIPTION = 'RSS feed for The Guardian'; - const PARAMETERS = array( array( - 'feed' => array( - 'name' => 'Feed', - 'type' => 'list', - 'values' => array( - 'World News' => 'world/rss', - 'US News' => '/us-news/rss', - 'UK News' => '/uk-news/rss', - 'Europe News' => '/world/europe-news/rss', - 'Asia News' => '/world/asia/rss', - 'Tech' => '/uk/technology/rss', - 'Business News' => '/uk/business/rss', - 'Opinion' => '/uk/commentisfree/rss', - 'Lifestyle' => '/uk/lifeandstyle/rss', - 'Culture' => '/uk/culture/rss', - 'Sports' => '/uk/sport/rss' - ) - ) - - /* - - Topicwise Links - - You can find the base feed for any topic by appending /rss to the url. - - Example: - - https://feeds.theguardian.com/theguardian/uk-news/rss - https://feeds.theguardian.com/theguardian/us-news/rss - - Or simply - - https://www.theguardian.com/world/rss - - Just add that topic as a value in the PARAMETERS const. - - */ - - - )); - - public function collectData(){ - $feed = $this->getInput('feed'); - $feedURL = 'https://feeds.theguardian.com/theguardian/' . $feed; - $this->collectExpandableDatas($feedURL, 10); - } - - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - - // --- Recovering the article --- - - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); - // figure contain's the main article image - $article = $articlePage->find('figure', 0); - // content__article-body has the actual article - foreach($articlePage->find('.content__article-body') as $element) - $article = $article . $element; - - // --- Fixing ugly elements --- - - // Replace the image viewer and BS with the image itself - foreach($articlePage->find('a.article__img-container') as $uslElementLoc) { - $main_img = $uslElementLoc->find('img', 0); - $article = str_replace($uslElementLoc, $main_img, $article); - } - - // List of all the crap in the article - $uselessElements = array( - '#show-caption', - '.element-atom', - '.submeta', - 'youtube-media-atom', - 'svg' - ); - - // Remove the listed crap - foreach($uselessElements as $uslElement) { - foreach($articlePage->find($uslElement) as $uslElementLoc) { - $article = str_replace($uslElementLoc, '', $article); - } - } - - $item['content'] = $article; - - return $item; - } + +class TheGuardianBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'The Guardian Bridge'; + const URI = 'https://www.theguardian.com/'; + const CACHE_TIMEOUT = 600; // This is a news site, so don't cache for more than 10 mins + const DESCRIPTION = 'RSS feed for The Guardian'; + const PARAMETERS = [ [ + 'feed' => [ + 'name' => 'Feed', + 'type' => 'list', + 'values' => [ + 'World News' => 'world/rss', + 'US News' => '/us-news/rss', + 'UK News' => '/uk-news/rss', + 'Europe News' => '/world/europe-news/rss', + 'Asia News' => '/world/asia/rss', + 'Tech' => '/uk/technology/rss', + 'Business News' => '/uk/business/rss', + 'Opinion' => '/uk/commentisfree/rss', + 'Lifestyle' => '/uk/lifeandstyle/rss', + 'Culture' => '/uk/culture/rss', + 'Sports' => '/uk/sport/rss' + ] + ] + + /* + + Topicwise Links + + You can find the base feed for any topic by appending /rss to the url. + + Example: + + https://feeds.theguardian.com/theguardian/uk-news/rss + https://feeds.theguardian.com/theguardian/us-news/rss + + Or simply + + https://www.theguardian.com/world/rss + + Just add that topic as a value in the PARAMETERS const. + + */ + + + ]]; + + public function collectData() + { + $feed = $this->getInput('feed'); + $feedURL = 'https://feeds.theguardian.com/theguardian/' . $feed; + $this->collectExpandableDatas($feedURL, 10); + } + + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + + // --- Recovering the article --- + + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + // figure contain's the main article image + $article = $articlePage->find('figure', 0); + // content__article-body has the actual article + foreach ($articlePage->find('.content__article-body') as $element) { + $article = $article . $element; + } + + // --- Fixing ugly elements --- + + // Replace the image viewer and BS with the image itself + foreach ($articlePage->find('a.article__img-container') as $uslElementLoc) { + $main_img = $uslElementLoc->find('img', 0); + $article = str_replace($uslElementLoc, $main_img, $article); + } + + // List of all the crap in the article + $uselessElements = [ + '#show-caption', + '.element-atom', + '.submeta', + 'youtube-media-atom', + 'svg' + ]; + + // Remove the listed crap + foreach ($uselessElements as $uslElement) { + foreach ($articlePage->find($uslElement) as $uslElementLoc) { + $article = str_replace($uslElementLoc, '', $article); + } + } + + $item['content'] = $article; + + return $item; + } } diff --git a/bridges/TheHackerNewsBridge.php b/bridges/TheHackerNewsBridge.php index b91b9504..dfe07543 100644 --- a/bridges/TheHackerNewsBridge.php +++ b/bridges/TheHackerNewsBridge.php @@ -1,79 +1,77 @@ <?php -class TheHackerNewsBridge extends BridgeAbstract { - const MAINTAINER = 'ORelio'; - const NAME = 'The Hacker News Bridge'; - const URI = 'https://thehackernews.com/'; - const DESCRIPTION = 'Cyber Security, Hacking, Technology News.'; +class TheHackerNewsBridge extends BridgeAbstract +{ + const MAINTAINER = 'ORelio'; + const NAME = 'The Hacker News Bridge'; + const URI = 'https://thehackernews.com/'; + const DESCRIPTION = 'Cyber Security, Hacking, Technology News.'; - public function collectData(){ + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + $limit = 0; - $html = getSimpleHTMLDOM($this->getURI()); - $limit = 0; + foreach ($html->find('div.body-post') as $element) { + if ($limit < 5) { + $article_url = $element->find('a.story-link', 0)->href; + $article_author = trim($element->find('i.icon-user', 0)->parent()->plaintext); + $article_author = str_replace('', '', $article_author); + $article_title = $element->find('h2.home-title', 0)->plaintext; - foreach($html->find('div.body-post') as $element) { - if($limit < 5) { + //Date without time + $article_timestamp = strtotime( + extractFromDelimiters( + $element->find('i.icon-calendar', 0)->parent()->outertext, + '</i>', + '<span>' + ) + ); - $article_url = $element->find('a.story-link', 0)->href; - $article_author = trim($element->find('i.icon-user', 0)->parent()->plaintext); - $article_author = str_replace('', '', $article_author); - $article_title = $element->find('h2.home-title', 0)->plaintext; + //Article thumbnail in lazy-loading image + if (is_object($element->find('img[data-echo]', 0))) { + $article_thumbnail = [ + extractFromDelimiters( + $element->find('img[data-echo]', 0)->outertext, + "data-echo='", + "'" + ) + ]; + } else { + $article_thumbnail = []; + } - //Date without time - $article_timestamp = strtotime( - extractFromDelimiters( - $element->find('i.icon-calendar', 0)->parent()->outertext, - '</i>', - '<span>' - ) - ); + if ($article = getSimpleHTMLDOMCached($article_url)) { + //Article body + $contents = $article->find('div.articlebody', 0)->innertext; + $contents = stripRecursiveHtmlSection($contents, 'div', '<div class="ad_'); + $contents = stripWithDelimiters($contents, 'id="google_ads', '</iframe>'); + $contents = stripWithDelimiters($contents, '<script', '</script>'); - //Article thumbnail in lazy-loading image - if (is_object($element->find('img[data-echo]', 0))) { - $article_thumbnail = array( - extractFromDelimiters( - $element->find('img[data-echo]', 0)->outertext, - "data-echo='", - "'" - ) - ); - } else { - $article_thumbnail = array(); - } + //Date with time + if (is_object($article->find('meta[itemprop=dateModified]', 0))) { + $article_timestamp = strtotime( + extractFromDelimiters( + $article->find('meta[itemprop=dateModified]', 0)->outertext, + "content='", + "'" + ) + ); + } + } else { + $contents = 'Could not request TheHackerNews: ' . $article_url; + } - if ($article = getSimpleHTMLDOMCached($article_url)) { - - //Article body - $contents = $article->find('div.articlebody', 0)->innertext; - $contents = stripRecursiveHtmlSection($contents, 'div', '<div class="ad_'); - $contents = stripWithDelimiters($contents, 'id="google_ads', '</iframe>'); - $contents = stripWithDelimiters($contents, '<script', '</script>'); - - //Date with time - if (is_object($article->find('meta[itemprop=dateModified]', 0))) { - $article_timestamp = strtotime( - extractFromDelimiters( - $article->find('meta[itemprop=dateModified]', 0)->outertext, - "content='", - "'" - ) - ); - } - } else { - $contents = 'Could not request TheHackerNews: ' . $article_url; - } - - $item = array(); - $item['uri'] = $article_url; - $item['title'] = $article_title; - $item['author'] = $article_author; - $item['enclosures'] = $article_thumbnail; - $item['timestamp'] = $article_timestamp; - $item['content'] = trim($contents); - $this->items[] = $item; - $limit++; - } - } - - } + $item = []; + $item['uri'] = $article_url; + $item['title'] = $article_title; + $item['author'] = $article_author; + $item['enclosures'] = $article_thumbnail; + $item['timestamp'] = $article_timestamp; + $item['content'] = trim($contents); + $this->items[] = $item; + $limit++; + } + } + } } diff --git a/bridges/ThePirateBayBridge.php b/bridges/ThePirateBayBridge.php index 68c39d5e..13095062 100644 --- a/bridges/ThePirateBayBridge.php +++ b/bridges/ThePirateBayBridge.php @@ -3,309 +3,325 @@ /** * Much of the logic here is copied from https://thepiratebay.org/static/main.js */ -class ThePirateBayBridge extends BridgeAbstract { - - const MAINTAINER = 'dvikan'; - const NAME = 'The Pirate Bay'; - const URI = 'https://thepiratebay.org'; - const DESCRIPTION = 'Returns results for the keywords. You can put several +class ThePirateBayBridge extends BridgeAbstract +{ + const MAINTAINER = 'dvikan'; + const NAME = 'The Pirate Bay'; + const URI = 'https://thepiratebay.org'; + const DESCRIPTION = 'Returns results for the keywords. You can put several list of keywords by separating them with a semicolon (e.g. "one show;another show"). Category based search needs the category number as input. User based search takes the Uploader name. Search can be done in a specified category'; - const PARAMETERS = array( array( - 'q' => array( - 'name' => 'keywords/username/category, separated by semicolons', - 'exampleValue' => 'simpsons', - 'required' => true - ), - 'crit' => array( - 'type' => 'list', - 'name' => 'Search type', - 'values' => array( - 'search' => 'search', - 'category' => 'cat', - 'user' => 'usr', - ) - ), - 'catCheck' => array( - 'type' => 'checkbox', - 'name' => 'Specify category for keyword search ?', - ), - 'cat' => array( - 'name' => 'Category number', - 'exampleValue' => '100, 200… See TPB for category number' - ), - 'trusted' => array( - 'type' => 'checkbox', - 'name' => 'Only get results from Trusted or VIP users ?', - ), - )); + const PARAMETERS = [ [ + 'q' => [ + 'name' => 'keywords/username/category, separated by semicolons', + 'exampleValue' => 'simpsons', + 'required' => true + ], + 'crit' => [ + 'type' => 'list', + 'name' => 'Search type', + 'values' => [ + 'search' => 'search', + 'category' => 'cat', + 'user' => 'usr', + ] + ], + 'catCheck' => [ + 'type' => 'checkbox', + 'name' => 'Specify category for keyword search ?', + ], + 'cat' => [ + 'name' => 'Category number', + 'exampleValue' => '100, 200… See TPB for category number' + ], + 'trusted' => [ + 'type' => 'checkbox', + 'name' => 'Only get results from Trusted or VIP users ?', + ], + ]]; - const STATIC_SERVER = 'https://torrindex.net'; + const STATIC_SERVER = 'https://torrindex.net'; - const CATEGORIES = array( - '1' => 'Audio', - '2' => 'Video', - '3' => 'Applications', - '4' => 'Games', - '5' => 'Porn', - '6' => 'Other', - '101' => 'Music', - '102' => 'Audio Books', - '103' => 'Sound clips', - '104' => 'FLAC', - '199' => 'Other', - '201' => 'Movies', - '202' => 'Movies DVDR', - '203' => 'Music videos', - '204' => 'Movie Clips', - '205' => 'TV-Shows', - '206' => 'Handheld', - '207' => 'HD Movies', - '208' => 'HD TV-Shows', - '209' => '3D', - '299' => 'Other', - '301' => 'Windows', - '302' => 'Mac/Apple', - '303' => 'UNIX', - '304' => 'Handheld', - '305' => 'IOS(iPad/iPhone)', - '306' => 'Android', - '399' => 'Other OS', - '401' => 'PC', - '402' => 'Mac/Apple', - '403' => 'PSx', - '404' => 'XBOX360', - '405' => 'Wii', - '406' => 'Handheld', - '407' => 'IOS(iPad/iPhone)', - '408' => 'Android', - '499' => 'Other OS', - '501' => 'Movies', - '502' => 'Movies DVDR', - '503' => 'Pictures', - '504' => 'Games', - '505' => 'HD-Movies', - '506' => 'Movie Clips', - '599' => 'Other', - '601' => 'E-books', - '602' => 'Comics', - '603' => 'Pictures', - '604' => 'Covers', - '605' => 'Physibles', - '699' => 'Other', - ); + const CATEGORIES = [ + '1' => 'Audio', + '2' => 'Video', + '3' => 'Applications', + '4' => 'Games', + '5' => 'Porn', + '6' => 'Other', + '101' => 'Music', + '102' => 'Audio Books', + '103' => 'Sound clips', + '104' => 'FLAC', + '199' => 'Other', + '201' => 'Movies', + '202' => 'Movies DVDR', + '203' => 'Music videos', + '204' => 'Movie Clips', + '205' => 'TV-Shows', + '206' => 'Handheld', + '207' => 'HD Movies', + '208' => 'HD TV-Shows', + '209' => '3D', + '299' => 'Other', + '301' => 'Windows', + '302' => 'Mac/Apple', + '303' => 'UNIX', + '304' => 'Handheld', + '305' => 'IOS(iPad/iPhone)', + '306' => 'Android', + '399' => 'Other OS', + '401' => 'PC', + '402' => 'Mac/Apple', + '403' => 'PSx', + '404' => 'XBOX360', + '405' => 'Wii', + '406' => 'Handheld', + '407' => 'IOS(iPad/iPhone)', + '408' => 'Android', + '499' => 'Other OS', + '501' => 'Movies', + '502' => 'Movies DVDR', + '503' => 'Pictures', + '504' => 'Games', + '505' => 'HD-Movies', + '506' => 'Movie Clips', + '599' => 'Other', + '601' => 'E-books', + '602' => 'Comics', + '603' => 'Pictures', + '604' => 'Covers', + '605' => 'Physibles', + '699' => 'Other', + ]; - public function collectData() { - $keywords = explode(';', $this->getInput('q')); + public function collectData() + { + $keywords = explode(';', $this->getInput('q')); - foreach($keywords as $keyword) { - $this->processKeyword($keyword); - } - } + foreach ($keywords as $keyword) { + $this->processKeyword($keyword); + } + } - private function processKeyword($keyword) - { - $keyword = trim($keyword); - switch ($this->getInput('crit')) { - case 'search': - $catCheck = $this->getInput('catCheck'); - if ($catCheck) { - $categories = $this->getInput('cat'); - $query = sprintf( - '/q.php?q=%s&cat=%s', - rawurlencode($keyword), - rawurlencode($categories) - ); - } else { - $query = sprintf('/q.php?q=%s', rawurlencode($keyword)); - } - break; - case 'cat': - $query = sprintf('/q.php?q=category:%s', rawurlencode($keyword)); - break; - case 'usr': - $query = sprintf('/q.php?q=user:%s', rawurlencode($keyword)); - break; - default: - returnClientError('Impossible'); - } - $api = 'https://apibay.org'; - $json = getContents($api . $query); - $result = json_decode($json); + private function processKeyword($keyword) + { + $keyword = trim($keyword); + switch ($this->getInput('crit')) { + case 'search': + $catCheck = $this->getInput('catCheck'); + if ($catCheck) { + $categories = $this->getInput('cat'); + $query = sprintf( + '/q.php?q=%s&cat=%s', + rawurlencode($keyword), + rawurlencode($categories) + ); + } else { + $query = sprintf('/q.php?q=%s', rawurlencode($keyword)); + } + break; + case 'cat': + $query = sprintf('/q.php?q=category:%s', rawurlencode($keyword)); + break; + case 'usr': + $query = sprintf('/q.php?q=user:%s', rawurlencode($keyword)); + break; + default: + returnClientError('Impossible'); + } + $api = 'https://apibay.org'; + $json = getContents($api . $query); + $result = json_decode($json); - if ($result[0]->name === 'No results returned') { - return; - } - foreach ($result as $torrent) { - // This is the check for whether to include results from Trusted or VIP users - if ($this->getInput('trusted') - && !in_array($torrent->status, array('vip', 'trusted')) - ) { - continue; - } - $this->processTorrent($torrent); - } - } + if ($result[0]->name === 'No results returned') { + return; + } + foreach ($result as $torrent) { + // This is the check for whether to include results from Trusted or VIP users + if ( + $this->getInput('trusted') + && !in_array($torrent->status, ['vip', 'trusted']) + ) { + continue; + } + $this->processTorrent($torrent); + } + } - private function processTorrent($torrent) - { - // Extracted these trackers from the magnet links on thepiratebay.org - $trackers = array( - 'udp://tracker.coppersurfer.tk:6969/announce', - 'udp://tracker.openbittorrent.com:6969/announce', - 'udp://9.rarbg.to:2710/announce', - 'udp://9.rarbg.me:2780/announce', - 'udp://9.rarbg.to:2730/announce', - 'udp://tracker.opentrackr.org:1337', - 'http://p4p.arenabg.com:1337/announce', - 'udp://tracker.torrent.eu.org:451/announce', - 'udp://tracker.tiny-vps.com:6969/announce', - 'udp://open.stealth.si:80/announce', - ); + private function processTorrent($torrent) + { + // Extracted these trackers from the magnet links on thepiratebay.org + $trackers = [ + 'udp://tracker.coppersurfer.tk:6969/announce', + 'udp://tracker.openbittorrent.com:6969/announce', + 'udp://9.rarbg.to:2710/announce', + 'udp://9.rarbg.me:2780/announce', + 'udp://9.rarbg.to:2730/announce', + 'udp://tracker.opentrackr.org:1337', + 'http://p4p.arenabg.com:1337/announce', + 'udp://tracker.torrent.eu.org:451/announce', + 'udp://tracker.tiny-vps.com:6969/announce', + 'udp://open.stealth.si:80/announce', + ]; - $magnetLink = sprintf( - 'magnet:?xt=urn:btih:%s&dn=%s', - $torrent->info_hash, - rawurlencode($torrent->name) - ); - foreach ($trackers as $tracker) { - // Build magnet link manually instead of using http_build_query because it - // creates undesirable query such as ?tr[0]=foo&tr[1]=bar&tr[2]=baz - $magnetLink .= '&tr=' . rawurlencode($tracker); - } + $magnetLink = sprintf( + 'magnet:?xt=urn:btih:%s&dn=%s', + $torrent->info_hash, + rawurlencode($torrent->name) + ); + foreach ($trackers as $tracker) { + // Build magnet link manually instead of using http_build_query because it + // creates undesirable query such as ?tr[0]=foo&tr[1]=bar&tr[2]=baz + $magnetLink .= '&tr=' . rawurlencode($tracker); + } - $item = array(); + $item = []; - $item['title'] = $torrent->name; - // This uri should be a magnet link so that feed readers can easily pick it up. - // However, rss-bridge only allows http or https schemes - $item['uri'] = sprintf('%s/description.php?id=%s', self::URI, $torrent->id); - $item['timestamp'] = $torrent->added; - $item['author'] = $torrent->username; + $item['title'] = $torrent->name; + // This uri should be a magnet link so that feed readers can easily pick it up. + // However, rss-bridge only allows http or https schemes + $item['uri'] = sprintf('%s/description.php?id=%s', self::URI, $torrent->id); + $item['timestamp'] = $torrent->added; + $item['author'] = $torrent->username; - $content = '<b>Type:</b> ' - . $this->renderCategory($torrent->category) . '<br>'; - $content .= "<b>Files:</b> $torrent->num_files<br>"; - $content .= '<b>Size:</b> ' . $this->renderSize($torrent->size) . '<br><br>'; + $content = '<b>Type:</b> ' + . $this->renderCategory($torrent->category) . '<br>'; + $content .= "<b>Files:</b> $torrent->num_files<br>"; + $content .= '<b>Size:</b> ' . $this->renderSize($torrent->size) . '<br><br>'; - $content .= '<b>Uploaded:</b> ' - . $this->renderUploadDate($torrent->added) . '<br>'; - $content .= '<b>By:</b> ' . $this->renderUser($torrent) . '<br>'; + $content .= '<b>Uploaded:</b> ' + . $this->renderUploadDate($torrent->added) . '<br>'; + $content .= '<b>By:</b> ' . $this->renderUser($torrent) . '<br>'; - $content .= "<b>Seeders:</b> {$torrent->seeders}<br>"; - $content .= "<b>Leechers:</b> {$torrent->leechers}<br>"; - $content .= "<b>Info hash:</b> {$torrent->info_hash}<br><br>"; + $content .= "<b>Seeders:</b> {$torrent->seeders}<br>"; + $content .= "<b>Leechers:</b> {$torrent->leechers}<br>"; + $content .= "<b>Info hash:</b> {$torrent->info_hash}<br><br>"; - if ($torrent->imdb) { - $content .= '<b>Imdb:</b> ' - . $this->renderImdbLink($torrent->imdb) . '<br><br>'; - } + if ($torrent->imdb) { + $content .= '<b>Imdb:</b> ' + . $this->renderImdbLink($torrent->imdb) . '<br><br>'; + } - $html = <<<HTML + $html = <<<HTML <a href="%s"> <img src="%s/images/icon-magnet.gif"> GET THIS TORRENT </a> <br> HTML; - $content .= sprintf($html, $magnetLink, self::STATIC_SERVER); + $content .= sprintf($html, $magnetLink, self::STATIC_SERVER); - $item['content'] = $content; + $item['content'] = $content; - $this->items[] = $item; - } + $this->items[] = $item; + } - private function renderSize($size) - { - if ($size < 1024) return $size . ' B'; - if ($size < pow(1024, 2)) return round($size / 1024, 2) . ' KB'; - if ($size < pow(1024, 3)) return round($size / pow(1024, 2), 2) . ' MB'; - if ($size < pow(1024, 4)) return round($size / pow(1024, 3), 2) . ' GB'; + private function renderSize($size) + { + if ($size < 1024) { + return $size . ' B'; + } + if ($size < pow(1024, 2)) { + return round($size / 1024, 2) . ' KB'; + } + if ($size < pow(1024, 3)) { + return round($size / pow(1024, 2), 2) . ' MB'; + } + if ($size < pow(1024, 4)) { + return round($size / pow(1024, 3), 2) . ' GB'; + } - return round($size / pow(1024, 4), 2) . ' TB'; - } + return round($size / pow(1024, 4), 2) . ' TB'; + } - private function renderUploadDate($added) - { - return date('Y-m-d', $added ?: time()); - } + private function renderUploadDate($added) + { + return date('Y-m-d', $added ?: time()); + } - private function renderCategory($category) - { - $mainCategory = sprintf( - '<a href="%s/search.php?q=category:%s">%s</a>', - self::URI, - $category[0] . '00', - self::CATEGORIES[$category[0]] - ); + private function renderCategory($category) + { + $mainCategory = sprintf( + '<a href="%s/search.php?q=category:%s">%s</a>', + self::URI, + $category[0] . '00', + self::CATEGORIES[$category[0]] + ); - $subCategory = sprintf( - '<a href="%s/search.php?q=category:%s">%s</a>', - self::URI, - $category, - self::CATEGORIES[$category] - ); + $subCategory = sprintf( + '<a href="%s/search.php?q=category:%s">%s</a>', + self::URI, + $category, + self::CATEGORIES[$category] + ); - return sprintf('%s > %s', $mainCategory, $subCategory); - } + return sprintf('%s > %s', $mainCategory, $subCategory); + } - private function renderUser($torrent) - { - if ($torrent->username === 'Anonymous') { - return $torrent->username . ' ' . $this->renderStatusImage($torrent->status); - } - return sprintf( - '<a href="%s/search.php?q=user:%s">%s %s</a>', - self::URI, - $torrent->username, - $torrent->username, - $this->renderStatusImage($torrent->status) - ); - } + private function renderUser($torrent) + { + if ($torrent->username === 'Anonymous') { + return $torrent->username . ' ' . $this->renderStatusImage($torrent->status); + } + return sprintf( + '<a href="%s/search.php?q=user:%s">%s %s</a>', + self::URI, + $torrent->username, + $torrent->username, + $this->renderStatusImage($torrent->status) + ); + } - private function renderStatusImage($status) - { - if ($status == 'trusted') - return sprintf( - '<img src="%s/images/trusted.png" title="Trusted"/>', - self::STATIC_SERVER - ); - if ($status == 'vip') - return sprintf( - '<img src="%s/images/vip.gif" title="VIP"/>', - self::STATIC_SERVER - ); - if ($status == 'helper') - return sprintf( - '<img src="%s/images/helper.png" title="Helper"/>', - self::STATIC_SERVER - ); - if ($status == 'moderator') - return sprintf( - '<img src="%s/images/moderator.gif" title="Moderator"/>', - self::STATIC_SERVER - ); - if ($status == 'supermod') - return sprintf( - '<img src="%s/images/supermod.png" title="Super Mod"/>', - self::STATIC_SERVER - ); - if ($status == 'admin') - return sprintf( - '<img src="%s/images/admin.gif" title="Admin"/>', - self::STATIC_SERVER - ); + private function renderStatusImage($status) + { + if ($status == 'trusted') { + return sprintf( + '<img src="%s/images/trusted.png" title="Trusted"/>', + self::STATIC_SERVER + ); + } + if ($status == 'vip') { + return sprintf( + '<img src="%s/images/vip.gif" title="VIP"/>', + self::STATIC_SERVER + ); + } + if ($status == 'helper') { + return sprintf( + '<img src="%s/images/helper.png" title="Helper"/>', + self::STATIC_SERVER + ); + } + if ($status == 'moderator') { + return sprintf( + '<img src="%s/images/moderator.gif" title="Moderator"/>', + self::STATIC_SERVER + ); + } + if ($status == 'supermod') { + return sprintf( + '<img src="%s/images/supermod.png" title="Super Mod"/>', + self::STATIC_SERVER + ); + } + if ($status == 'admin') { + return sprintf( + '<img src="%s/images/admin.gif" title="Admin"/>', + self::STATIC_SERVER + ); + } - return ''; - } + return ''; + } - private function renderImdbLink($imdb) - { - return sprintf( - '<a href="%s">%s</a>', - "https://www.imdb.com/title/$imdb", - "https://www.imdb.com/title/$imdb" - ); - } + private function renderImdbLink($imdb) + { + return sprintf( + '<a href="%s">%s</a>', + "https://www.imdb.com/title/$imdb", + "https://www.imdb.com/title/$imdb" + ); + } } diff --git a/bridges/TheWhiteboardBridge.php b/bridges/TheWhiteboardBridge.php index e455d100..c36cc5f6 100644 --- a/bridges/TheWhiteboardBridge.php +++ b/bridges/TheWhiteboardBridge.php @@ -1,22 +1,25 @@ <?php -class TheWhiteboardBridge extends BridgeAbstract { - const NAME = 'The Whiteboard'; - const URI = 'https://www.the-whiteboard.com/'; - const DESCRIPTION = 'Get the latest comic from The Whiteboard'; - const MAINTAINER = 'CyberJacob'; - public function collectData() { - $item = array(); +class TheWhiteboardBridge extends BridgeAbstract +{ + const NAME = 'The Whiteboard'; + const URI = 'https://www.the-whiteboard.com/'; + const DESCRIPTION = 'Get the latest comic from The Whiteboard'; + const MAINTAINER = 'CyberJacob'; - $html = getSimpleHTMLDOM(self::URI); + public function collectData() + { + $item = []; - $image = $html->find('center', 1)->find('img', 0); - $image->src = self::URI . '/' . $image->src; + $html = getSimpleHTMLDOM(self::URI); - $item['title'] = explode("\r\n", $html->find('center', 1)->plaintext)[0]; - $item['content'] = $image; - $item['timestamp'] = explode("\r\n", $html->find('center', 1)->plaintext)[0]; + $image = $html->find('center', 1)->find('img', 0); + $image->src = self::URI . '/' . $image->src; - $this->items[] = $item; - } + $item['title'] = explode("\r\n", $html->find('center', 1)->plaintext)[0]; + $item['content'] = $image; + $item['timestamp'] = explode("\r\n", $html->find('center', 1)->plaintext)[0]; + + $this->items[] = $item; + } } diff --git a/bridges/TheYeteeBridge.php b/bridges/TheYeteeBridge.php index b7867ae9..5c7d8856 100644 --- a/bridges/TheYeteeBridge.php +++ b/bridges/TheYeteeBridge.php @@ -1,39 +1,39 @@ <?php -class TheYeteeBridge extends BridgeAbstract { - const MAINTAINER = 'Monsieur Poutounours'; - const NAME = 'TheYetee'; - const URI = 'https://theyetee.com'; - const CACHE_TIMEOUT = 14400; // 4 h - const DESCRIPTION = 'Fetch daily shirts from The Yetee'; - - public function collectData(){ - - $html = getSimpleHTMLDOM(self::URI); - - $div = $html->find('.module_timed-item.is--full'); - foreach($div as $element) { - - $item = array(); - $item['enclosures'] = array(); - - $title = $element->find('h2', 0)->plaintext; - $item['title'] = $title; - - $author = trim($element->find('.module_timed-item--artist a', 0)->plaintext); - $item['author'] = $author; - - $item['uri'] = static::URI; - - $content = '<p>' . $title . ' by ' . $author . '</p>'; - $photos = $element->find('a.img'); - foreach($photos as $photo) { - $content = $content . "<br /><img src='$photo->href' />"; - $item['enclosures'][] = $photo->src; - } - $item['content'] = $content; - - $this->items[] = $item; - } - } +class TheYeteeBridge extends BridgeAbstract +{ + const MAINTAINER = 'Monsieur Poutounours'; + const NAME = 'TheYetee'; + const URI = 'https://theyetee.com'; + const CACHE_TIMEOUT = 14400; // 4 h + const DESCRIPTION = 'Fetch daily shirts from The Yetee'; + + public function collectData() + { + $html = getSimpleHTMLDOM(self::URI); + + $div = $html->find('.module_timed-item.is--full'); + foreach ($div as $element) { + $item = []; + $item['enclosures'] = []; + + $title = $element->find('h2', 0)->plaintext; + $item['title'] = $title; + + $author = trim($element->find('.module_timed-item--artist a', 0)->plaintext); + $item['author'] = $author; + + $item['uri'] = static::URI; + + $content = '<p>' . $title . ' by ' . $author . '</p>'; + $photos = $element->find('a.img'); + foreach ($photos as $photo) { + $content = $content . "<br /><img src='$photo->href' />"; + $item['enclosures'][] = $photo->src; + } + $item['content'] = $content; + + $this->items[] = $item; + } + } } diff --git a/bridges/TikTokBridge.php b/bridges/TikTokBridge.php index c58363e3..41da917f 100644 --- a/bridges/TikTokBridge.php +++ b/bridges/TikTokBridge.php @@ -1,87 +1,95 @@ <?php -class TikTokBridge extends BridgeAbstract { - const NAME = 'TikTok Bridge'; - const URI = 'https://www.tiktok.com'; - const DESCRIPTION = 'Returns posts'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - 'By user' => array( - 'username' => array( - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '@tiktok', - ) - )); - - const TEST_DETECT_PARAMETERS = array( - 'https://www.tiktok.com/@tiktok' => array( - 'context' => 'By user', 'username' => '@tiktok' - ) - ); - - const CACHE_TIMEOUT = 900; // 15 minutes - - private $feedName = ''; - - public function detectParameters($url) { - - if(preg_match('/tiktok\.com\/(@[\w]+)/', $url, $matches) > 0) { - return array( - 'context' => 'By user', - 'username' => $matches[1] - ); - } - - return null; - } - - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); - - $this->feedName = htmlspecialchars_decode($html->find('h1', 0)->plaintext); - - foreach ($html->find('div.tiktok-x6y88p-DivItemContainerV2') as $div) { - $item = []; - - $link = $div->find('a', 0)->href; - $image = $div->find('img', 0)->src; - $views = $div->find('strong.video-count', 0)->plaintext; - - $item['uri'] = $link; - $item['title'] = $div->find('a', 1)->plaintext; - $item['enclosures'][] = $image; - - $item['content'] = <<<EOD + +class TikTokBridge extends BridgeAbstract +{ + const NAME = 'TikTok Bridge'; + const URI = 'https://www.tiktok.com'; + const DESCRIPTION = 'Returns posts'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + 'By user' => [ + 'username' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '@tiktok', + ] + ]]; + + const TEST_DETECT_PARAMETERS = [ + 'https://www.tiktok.com/@tiktok' => [ + 'context' => 'By user', 'username' => '@tiktok' + ] + ]; + + const CACHE_TIMEOUT = 900; // 15 minutes + + private $feedName = ''; + + public function detectParameters($url) + { + if (preg_match('/tiktok\.com\/(@[\w]+)/', $url, $matches) > 0) { + return [ + 'context' => 'By user', + 'username' => $matches[1] + ]; + } + + return null; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $this->feedName = htmlspecialchars_decode($html->find('h1', 0)->plaintext); + + foreach ($html->find('div.tiktok-x6y88p-DivItemContainerV2') as $div) { + $item = []; + + $link = $div->find('a', 0)->href; + $image = $div->find('img', 0)->src; + $views = $div->find('strong.video-count', 0)->plaintext; + + $item['uri'] = $link; + $item['title'] = $div->find('a', 1)->plaintext; + $item['enclosures'][] = $image; + + $item['content'] = <<<EOD <a href="{$link}"><img src="{$image}"/></a> <p>{$views} views<p> EOD; - $this->items[] = $item; - } - } - - public function getURI() { - switch($this->queriedContext) { - case 'By user': - return self::URI . '/' . $this->processUsername(); - default: return parent::getURI(); - } - } - - public function getName() { - switch($this->queriedContext) { - case 'By user': - return $this->feedName . ' (' . $this->processUsername() . ') - TikTok'; - default: return parent::getName(); - } - } - - private function processUsername() { - if (substr($this->getInput('username'), 0, 1) !== '@') { - return '@' . $this->getInput('username'); - } - - return $this->getInput('username'); - } + $this->items[] = $item; + } + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'By user': + return self::URI . '/' . $this->processUsername(); + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'By user': + return $this->feedName . ' (' . $this->processUsername() . ') - TikTok'; + default: + return parent::getName(); + } + } + + private function processUsername() + { + if (substr($this->getInput('username'), 0, 1) !== '@') { + return '@' . $this->getInput('username'); + } + + return $this->getInput('username'); + } } diff --git a/bridges/TinyLetterBridge.php b/bridges/TinyLetterBridge.php index 96c53331..0ba9bf1d 100644 --- a/bridges/TinyLetterBridge.php +++ b/bridges/TinyLetterBridge.php @@ -1,54 +1,58 @@ <?php -class TinyLetterBridge extends BridgeAbstract { - const NAME = 'Tiny Letter'; - const URI = 'https://tinyletter.com/'; - const DESCRIPTION = 'Tiny Letter is a mailing list service'; - const MAINTAINER = 'somini'; - const PARAMETERS = array( - array( - 'username' => array( - 'name' => 'User Name', - 'required' => true, - 'exampleValue' => 'forwards', - ) - ) - ); - - public function getName() { - $username = $this->getInput('username'); - if (!is_null($username)) { - return static::NAME . ' | ' . $username; - } - - return parent::getName(); - } - - public function getURI() { - $username = $this->getInput('username'); - if (!is_null($username)) { - return static::URI . urlencode($username); - } - - return parent::getURI(); - } - - public function collectData() { - $archives = self::getURI() . '/archive'; - $html = getSimpleHTMLDOMCached($archives); - - foreach($html->find('.message-list li') as $element) { - $item = array(); - - $snippet = $element->find('p.message-snippet', 0); - $link = $element->find('.message-link', 0); - - $item['title'] = $link->plaintext; - $item['content'] = $snippet->innertext; - $item['uri'] = $link->href; - $item['timestamp'] = strtotime($element->find('.message-date', 0)->plaintext); - - $this->items[] = $item; - } - - } + +class TinyLetterBridge extends BridgeAbstract +{ + const NAME = 'Tiny Letter'; + const URI = 'https://tinyletter.com/'; + const DESCRIPTION = 'Tiny Letter is a mailing list service'; + const MAINTAINER = 'somini'; + const PARAMETERS = [ + [ + 'username' => [ + 'name' => 'User Name', + 'required' => true, + 'exampleValue' => 'forwards', + ] + ] + ]; + + public function getName() + { + $username = $this->getInput('username'); + if (!is_null($username)) { + return static::NAME . ' | ' . $username; + } + + return parent::getName(); + } + + public function getURI() + { + $username = $this->getInput('username'); + if (!is_null($username)) { + return static::URI . urlencode($username); + } + + return parent::getURI(); + } + + public function collectData() + { + $archives = self::getURI() . '/archive'; + $html = getSimpleHTMLDOMCached($archives); + + foreach ($html->find('.message-list li') as $element) { + $item = []; + + $snippet = $element->find('p.message-snippet', 0); + $link = $element->find('.message-link', 0); + + $item['title'] = $link->plaintext; + $item['content'] = $snippet->innertext; + $item['uri'] = $link->href; + $item['timestamp'] = strtotime($element->find('.message-date', 0)->plaintext); + + $this->items[] = $item; + } + } } diff --git a/bridges/TorrentGalaxyBridge.php b/bridges/TorrentGalaxyBridge.php index 22f839ef..052af262 100644 --- a/bridges/TorrentGalaxyBridge.php +++ b/bridges/TorrentGalaxyBridge.php @@ -1,81 +1,82 @@ <?php -class TorrentGalaxyBridge extends BridgeAbstract { +class TorrentGalaxyBridge extends BridgeAbstract +{ + const NAME = 'Torrent Galaxy Bridge'; + const URI = 'https://torrentgalaxy.to'; + const DESCRIPTION = 'Returns latest torrents'; + const MAINTAINER = 'GregThib'; + const CACHE_TIMEOUT = 14400; // 24h = 86400s - const NAME = 'Torrent Galaxy Bridge'; - const URI = 'https://torrentgalaxy.to'; - const DESCRIPTION = 'Returns latest torrents'; - const MAINTAINER = 'GregThib'; - const CACHE_TIMEOUT = 14400; // 24h = 86400s + const PARAMETERS = [ + [ + 'search' => [ + 'name' => 'search', + 'required' => true, + 'exampleValue' => 'simpsons', + 'title' => 'Type your query' + ], + 'lang' => [ + 'name' => 'language', + 'type' => 'list', + 'exampleValue' => 'All languages', + 'title' => 'Select your language', + 'values' => [ + 'All languages' => '0', + 'English' => '1', + 'French' => '2', + 'German' => '3', + 'Italian' => '4', + 'Japanese' => '5', + 'Spanish' => '6', + 'Russian' => '7', + 'Hindi' => '8', + 'Other / Multiple' => '9', + 'Korean' => '10', + 'Danish' => '11', + 'Norwegian' => '12', + 'Dutch' => '13', + 'Manderin' => '14', + 'Portuguese' => '15', + 'Bengali' => '16', + 'Polish' => '17', + 'Turkish' => '18', + 'Telugu' => '19', + 'Urdu' => '20', + 'Arabic' => '21', + 'Swedish' => '22', + 'Romanian' => '23' + ] + ] + ] + ]; - const PARAMETERS = array( - array( - 'search' => array( - 'name' => 'search', - 'required' => true, - 'exampleValue' => 'simpsons', - 'title' => 'Type your query' - ), - 'lang' => array( - 'name' => 'language', - 'type' => 'list', - 'exampleValue' => 'All languages', - 'title' => 'Select your language', - 'values' => array( - 'All languages' => '0', - 'English' => '1', - 'French' => '2', - 'German' => '3', - 'Italian' => '4', - 'Japanese' => '5', - 'Spanish' => '6', - 'Russian' => '7', - 'Hindi' => '8', - 'Other / Multiple' => '9', - 'Korean' => '10', - 'Danish' => '11', - 'Norwegian' => '12', - 'Dutch' => '13', - 'Manderin' => '14', - 'Portuguese' => '15', - 'Bengali' => '16', - 'Polish' => '17', - 'Turkish' => '18', - 'Telugu' => '19', - 'Urdu' => '20', - 'Arabic' => '21', - 'Swedish' => '22', - 'Romanian' => '23' - ) - ) - ) - ); + public function collectData() + { + $url = self::URI + . '/torrents.php?search=' . urlencode($this->getInput('search')) + . '&lang=' . $this->getInput('lang') + . '&sort=id&order=desc'; + $html = getSimpleHTMLDOM($url); - public function collectData(){ - $url = self::URI - . '/torrents.php?search=' . urlencode($this->getInput('search')) - . '&lang=' . $this->getInput('lang') - . '&sort=id&order=desc'; - $html = getSimpleHTMLDOM($url); + foreach ($html->find('div.tgxtablerow') as $result) { + $identity = $result->find('div.tgxtablecell', 3)->find('div a', 0); + $authorid = $result->find('div.tgxtablecell', 6)->find('a', 0); + $creadate = $result->find('div.tgxtablecell', 11)->plaintext; + $glxlinks = $result->find('div.tgxtablecell', 4); - foreach($html->find('div.tgxtablerow') as $result) { - $identity = $result->find('div.tgxtablecell', 3)->find('div a', 0); - $authorid = $result->find('div.tgxtablecell', 6)->find('a', 0); - $creadate = $result->find('div.tgxtablecell', 11)->plaintext; - $glxlinks = $result->find('div.tgxtablecell', 4); + $item = []; + $item['uri'] = self::URI . $identity->href; + $item['title'] = $identity->plaintext; - $item = array(); - $item['uri'] = self::URI . $identity->href; - $item['title'] = $identity->plaintext; + // todo: parse date strings such as '1Hr ago' etc. + $createdAt = DateTime::createFromFormat('d/m/y H:i', $creadate); + if ($createdAt) { + $item['timestamp'] = $createdAt->format('U'); + } - // todo: parse date strings such as '1Hr ago' etc. - $createdAt = DateTime::createFromFormat('d/m/y H:i', $creadate); - if ($createdAt) { - $item['timestamp'] = $createdAt->format('U'); - } - - $item['author'] = $authorid->plaintext; - $item['content'] = <<<HTML + $item['author'] = $authorid->plaintext; + $item['content'] = <<<HTML <h1>{$identity->plaintext}</h1> <h2>Links</h2> <p><a href="{$glxlinks->find('a', 1)->href}" title="magnet link">magnet</a></p> @@ -85,42 +86,46 @@ class TorrentGalaxyBridge extends BridgeAbstract { <p>Added by: <a href="{$authorid->href}" title="author profile">{$authorid->plaintext}</a></p> <p>Upload time: {$creadate}</p> HTML; - $item['enclosures'] = array($glxlinks->find('a', 0)->href); - $item['categories'] = array($result->find('div.tgxtablecell', 0)->plaintext); - if (preg_match('#/torrent/([^/]+)/#', self::URI . $identity->href, $torrentid)) { - $item['uid'] = $torrentid[1]; - } - $this->items[] = $item; - } - } + $item['enclosures'] = [$glxlinks->find('a', 0)->href]; + $item['categories'] = [$result->find('div.tgxtablecell', 0)->plaintext]; + if (preg_match('#/torrent/([^/]+)/#', self::URI . $identity->href, $torrentid)) { + $item['uid'] = $torrentid[1]; + } + $this->items[] = $item; + } + } - public function getName(){ - if(!is_null($this->getInput('search'))) { - return $this->getInput('search') . ' : ' . self::NAME; - } - return parent::getName(); - } + public function getName() + { + if (!is_null($this->getInput('search'))) { + return $this->getInput('search') . ' : ' . self::NAME; + } + return parent::getName(); + } - public function getURI(){ - if(!is_null($this->getInput('search'))) { - return self::URI - . '/torrents.php?search=' . urlencode($this->getInput('search')) - . '&lang=' . $this->getInput('lang'); - } - return parent::getURI(); - } + public function getURI() + { + if (!is_null($this->getInput('search'))) { + return self::URI + . '/torrents.php?search=' . urlencode($this->getInput('search')) + . '&lang=' . $this->getInput('lang'); + } + return parent::getURI(); + } - public function getDescription(){ - if(!is_null($this->getInput('search'))) { - return 'Latest torrents for "' . $this->getInput('search') . '"'; - } - return parent::getDescription(); - } + public function getDescription() + { + if (!is_null($this->getInput('search'))) { + return 'Latest torrents for "' . $this->getInput('search') . '"'; + } + return parent::getDescription(); + } - public function getIcon(){ - if(!is_null($this->getInput('search'))) { - return self::URI . '/common/favicon/favicon.ico'; - } - return parent::getIcon(); - } + public function getIcon() + { + if (!is_null($this->getInput('search'))) { + return self::URI . '/common/favicon/favicon.ico'; + } + return parent::getIcon(); + } } diff --git a/bridges/TrelloBridge.php b/bridges/TrelloBridge.php index 5cf69050..ea7eb71b 100644 --- a/bridges/TrelloBridge.php +++ b/bridges/TrelloBridge.php @@ -1,686 +1,700 @@ <?php -class TrelloBridge extends BridgeAbstract { - const NAME = 'Trello Bridge'; - const URI = 'https://trello.com/'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Returns activity on Trello boards or cards'; - const MAINTAINER = 'Roliga'; - const PARAMETERS = array( - 'Board' => array( - 'b' => array( - 'name' => 'Board ID', - 'required' => true, - 'exampleValue' => 'g9mdhdzg', - 'title' => 'Taken from Trello URL, e.g. trello.com/b/[Board ID]' - ) - ), - 'Card' => array( - 'c' => array( - 'name' => 'Card ID', - 'required' => true, - 'exampleValue' => '8vddc9pE', - 'title' => 'Taken from Trello URL, e.g. trello.com/c/[Card ID]' - ) - ) - ); - /* - * This was extracted from webpack on a Trello page, e.g. trello.com/b/g9mdhdzg - * In the browser's inspector/debugger go to the Debugger (Firefox) or - * Sources (Chromium) tab, these values can be found at: - * webpack:///resources/strings/actions/en.json - */ - const ACTION_TEXTS = array( - 'action_accept_enterprise_join_request' - => '{memberCreator} added team {organization} to the enterprise {enterprise}', - 'action_add_attachment_to_card' - => '{memberCreator} attached {attachment} to {card} {attachmentPreview}', - 'action_add_attachment_to_card@card' - => '{memberCreator} attached {attachment} to this card {attachmentPreview}', - 'action_add_checklist_to_card' - => '{memberCreator} added {checklist} to {card}', - 'action_add_checklist_to_card@card' - => '{memberCreator} added {checklist} to this card', - 'action_add_label_to_card' - => '{memberCreator} added the {label} label to {card}', - 'action_add_label_to_card@card' - => '{memberCreator} added the {label} label to this card', - 'action_add_organization_to_enterprise' - => '{memberCreator} added team {organization} to the enterprise {enterprise}', - 'action_add_to_organization_board' - => '{memberCreator} added {board} to {organization}', - 'action_add_to_organization_board@board' - => '{memberCreator} added this board to {organization}', - 'action_added_a_due_date' - => '{memberCreator} set {card} to be due {date}', - 'action_added_a_due_date@card' - => '{memberCreator} set this card to be due {date}', - 'action_added_list_to_board' - => '{memberCreator} added list {list} to {board}', - 'action_added_list_to_board@board' - => '{memberCreator} added {list} to this board', - 'action_added_member_to_board' - => '{memberCreator} added {member} to {board}', - 'action_added_member_to_board@board' - => '{memberCreator} added {member} to this board', - 'action_added_member_to_board_as_admin' - => '{memberCreator} added {member} to {board} as an admin', - 'action_added_member_to_board_as_admin@board' - => '{memberCreator} added {member} to this board as an admin', - 'action_added_member_to_board_as_observer' - => '{memberCreator} added {member} to {board} as an observer', - 'action_added_member_to_board_as_observer@board' - => '{memberCreator} added {member} to this board as an observer', - 'action_added_member_to_card' - => '{memberCreator} added {member} to {card}', - 'action_added_member_to_card@card' - => '{memberCreator} added {member} to this card', - 'action_added_member_to_organization' - => '{memberCreator} added {member} to {organization}', - 'action_added_member_to_organization_as_admin' - => '{memberCreator} added {member} to {organization} as an admin', - 'action_admins_visibility' - => 'its admins', - 'action_another_board' - => 'another board', - 'action_archived_card' - => '{memberCreator} archived {card}', - 'action_archived_card@card' - => '{memberCreator} archived this card', - 'action_archived_list' - => '{memberCreator} archived list {list}', - 'action_became_a_normal_user_in_organization' - => '{memberCreator} became a normal user in {organization}', - 'action_became_a_normal_user_on' - => '{memberCreator} became a normal user on {board}', - 'action_became_a_normal_user_on@board' - => '{memberCreator} became a normal user on this board', - 'action_became_an_admin_of_organization' - => '{memberCreator} became an admin of {organization}', - 'action_board_perm_level' - => '{memberCreator} made {board} visible to {level}', - 'action_board_perm_level@board' - => '{memberCreator} made this board visible to {level}', - 'action_calendar' - => 'calendar', - 'action_cardAging' - => 'card aging', - 'action_changed_a_due_date' - => '{memberCreator} changed the due date of {card} to {date}', - 'action_changed_a_due_date@card' - => '{memberCreator} changed the due date of this card to {date}', - 'action_changed_board_background' - => '{memberCreator} changed the background of {board}', - 'action_changed_board_background@board' - => '{memberCreator} changed the background of this board', - 'action_changed_description_of_card' - => '{memberCreator} changed description of {card}', - 'action_changed_description_of_card@card' - => '{memberCreator} changed description of this card', - 'action_changed_description_of_organization' - => '{memberCreator} changed description of {organization}', - 'action_changed_display_name_of_organization' - => '{memberCreator} changed display name of {organization}', - 'action_changed_name_of_organization' - => '{memberCreator} changed name of {organization}', - 'action_changed_website_of_organization' - => '{memberCreator} changed website of {organization}', - 'action_closed_board' - => '{memberCreator} closed {board}', - 'action_closed_board@board' - => '{memberCreator} closed this board', - 'action_comment_on_card' - => '{memberCreator} {contextOn} {card} {comment}', - 'action_comment_on_card@card' - => '{memberCreator} {comment}', - 'action_completed_checkitem' - => '{memberCreator} completed {checkitem} on {card}', - 'action_completed_checkitem@card' - => '{memberCreator} completed {checkitem} on this card', - 'action_convert_to_card_from_checkitem' - => '{memberCreator} converted {card} from a checklist item on {cardSource}', - 'action_convert_to_card_from_checkitem@card' - => '{memberCreator} converted this card from a checklist item on {cardSource}', - 'action_convert_to_card_from_checkitem@cardSource' - => '{memberCreator} converted {card} from a checklist item on this card', - 'action_copy_board' - => '{memberCreator} copied this board from {board}', - 'action_copy_card' - => '{memberCreator} copied {card} from {cardSource} in list {list}', - 'action_copy_card@card' - => '{memberCreator} copied this card from {cardSource} in list {list}', - 'action_copy_comment_from_card' - => '{memberCreator} copied comment by {member} from card {card} {comment}', - 'action_create_board' - => '{memberCreator} created {board}', - 'action_create_board@board' - => '{memberCreator} created this board', - 'action_create_card' - => '{memberCreator} added {card} to {list}', - 'action_create_card@card' - => '{memberCreator} added this card to {list}', - 'action_create_custom_field' - => '{memberCreator} created the {customField} custom field on {board}', - 'action_create_custom_field@board' - => '{memberCreator} created the {customField} custom field on this board', - 'action_create_enterprise_join_request' - => '{memberCreator} requested to add team {organization} to the enterprise {enterprise}', - 'action_created_an_invitation_to_board' - => '{memberCreator} created an invitation to {board}', - 'action_created_an_invitation_to_board@board' - => '{memberCreator} created an invitation to this board', - 'action_created_an_invitation_to_organization' - => '{memberCreator} created an invitation to {organization}', - 'action_created_checklist_on_board' - => '{memberCreator} created {checklist} on {board}', - 'action_created_checklist_on_board@board' - => '{memberCreator} created {checklist} on this board', - 'action_created_organization' - => '{memberCreator} created {organization}', - 'action_decline_enterprise_join_request' - => '{memberCreator} declined the request to add team {organization} to the enterprise {enterprise}', - 'action_delete_attachment_from_card' - => '{memberCreator} deleted the {attachment} attachment from {card}', - 'action_delete_attachment_from_card@card' - => '{memberCreator} deleted the {attachment} attachment from this card', - 'action_delete_card' - => '{memberCreator} deleted card #{idCard} from {list}', - 'action_delete_custom_field' - => '{memberCreator} deleted the {customField} custom field from {board}', - 'action_delete_custom_field@board' - => '{memberCreator} deleted the {customField} custom field from this board', - 'action_deleted_account' - => '[deleted account]', - 'action_deleted_an_invitation_to_board' - => '{memberCreator} deleted an invitation to {board}', - 'action_deleted_an_invitation_to_board@board' - => '{memberCreator} deleted an invitation to this board', - 'action_deleted_an_invitation_to_organization' - => '{memberCreator} deleted an invitation to {organization}', - 'action_deleted_checkitem' - => '{memberCreator} deleted task {checkitem} on {checklist}', - 'action_disabled_calendar_feed' - => '{memberCreator} disabled the iCalendar feed on {board}', - 'action_disabled_calendar_feed@board' - => '{memberCreator} disabled the iCalendar feed on this board', - 'action_disabled_card_covers' - => '{memberCreator} disabled card cover images on {board}', - 'action_disabled_card_covers@board' - => '{memberCreator} disabled card cover images on this board', - 'action_disabled_commenting' - => '{memberCreator} disabled commenting on {board}', - 'action_disabled_commenting@board' - => '{memberCreator} disabled commenting on this board', - 'action_disabled_inviting' - => '{memberCreator} disabled inviting on {board}', - 'action_disabled_inviting@board' - => '{memberCreator} disabled inviting on this board', - 'action_disabled_plugin' - => '{memberCreator} disabled the {plugin} Power-Up', - 'action_disabled_powerup' - => '{memberCreator} disabled the {powerup} Power-Up', - 'action_disabled_self_join' - => '{memberCreator} disabled self join on {board}', - 'action_disabled_self_join@board' - => '{memberCreator} disabled self join on this board', - 'action_disabled_voting' - => '{memberCreator} disabled voting on {board}', - 'action_disabled_voting@board' - => '{memberCreator} disabled voting on this board', - 'action_due_date_change' - => '{memberCreator}', - 'action_email_card' - => '{memberCreator} emailed {card} to {list}', - 'action_email_card@card' - => '{memberCreator} emailed this card to {list}', - 'action_email_card_from' - => '{memberCreator} emailed {card} to {list} from {from}', - 'action_email_card_from@card' - => '{memberCreator} emailed this card to {list} from {from}', - 'action_enabled_calendar_feed' - => '{memberCreator} enabled the iCalendar feed on {board}', - 'action_enabled_calendar_feed@board' - => '{memberCreator} enabled the iCalendar feed on this board', - 'action_enabled_card_covers' - => '{memberCreator} enabled card cover images on {board}', - 'action_enabled_card_covers@board' - => '{memberCreator} enabled card cover images on this board', - 'action_enabled_plugin' - => '{memberCreator} enabled the {plugin} Power-Up', - 'action_enabled_powerup' - => '{memberCreator} enabled the {powerup} Power-Up', - 'action_enabled_self_join' - => '{memberCreator} enabled self join on {board}', - 'action_enabled_self_join@board' - => '{memberCreator} enabled self join on this board', - 'action_hid_board' - => '{memberCreator} hid {board}', - 'action_hid_board@board' - => '{memberCreator} hid this board', - 'action_invited_an_unconfirmed_member_to_board' - => '{memberCreator} invited an unconfirmed member to {board}', - 'action_invited_an_unconfirmed_member_to_board@board' - => '{memberCreator} invited an unconfirmed member to this board', - 'action_invited_an_unconfirmed_member_to_organization' - => '{memberCreator} invited an unconfirmed member to {organization}', - 'action_joined_board' - => '{memberCreator} joined {board}', - 'action_joined_board@board' - => '{memberCreator} joined this board', - 'action_joined_board_by_invitation_link' - => '{memberCreator} joined {board} with an invitation link from {memberInviter}', - 'action_joined_board_by_invitation_link@board' - => '{memberCreator} joined this board with an invitation link from {memberInviter}', - 'action_joined_organization' - => '{memberCreator} joined {organization}', - 'action_joined_organization_by_invitation_link' - => '{memberCreator} joined {organization} with an invitation link from {memberInviter}', - 'action_left_board' - => '{memberCreator} left {board}', - 'action_left_board@board' - => '{memberCreator} left this board', - 'action_left_organization' - => '{memberCreator} left {organization}', - 'action_made_a_normal_user_in_organization' - => '{memberCreator} made {member} a normal user in {organization}', - 'action_made_a_normal_user_on' - => '{memberCreator} made {member} a normal user on {board}', - 'action_made_a_normal_user_on@board' - => '{memberCreator} made {member} a normal user on this board', - 'action_made_admin_of_board' - => '{memberCreator} made {member} an admin of {board}', - 'action_made_admin_of_board@board' - => '{memberCreator} made {member} an admin of this board', - 'action_made_an_admin_of_organization' - => '{memberCreator} made {member} an admin of {organization}', - 'action_made_commenting_on' - => '{memberCreator} made commenting on {board} available to {level}', - 'action_made_commenting_on@board' - => '{memberCreator} made commenting on this board available to {level}', - 'action_made_inviting_on' - => '{memberCreator} made inviting on {board} available to {level}', - 'action_made_inviting_on@board' - => '{memberCreator} made inviting on this board available to {level}', - 'action_made_observer_of_board' - => '{memberCreator} made {member} an observer of {board}', - 'action_made_observer_of_board@board' - => '{memberCreator} made {member} an observer of this board', - 'action_made_self_admin_of_board' - => '{memberCreator} made themselves an admin of {board}', - 'action_made_self_admin_of_board@board' - => '{memberCreator} made themselves an admin of this board', - 'action_made_self_observer_of_board' - => '{memberCreator} became an observer of {board}', - 'action_made_self_observer_of_board@board' - => '{memberCreator} became an observer of this board', - 'action_made_voting_on' - => '{memberCreator} made voting on {board} available to {level}', - 'action_made_voting_on@board' - => '{memberCreator} made voting on this board available to {level}', - 'action_marked_checkitem_incomplete' - => '{memberCreator} marked {checkitem} incomplete on {card}', - 'action_marked_checkitem_incomplete@card' - => '{memberCreator} marked {checkitem} incomplete on this card', - 'action_marked_the_due_date_complete' - => '{memberCreator} marked the due date on {card} complete', - 'action_marked_the_due_date_complete@card' - => '{memberCreator} marked the due date complete', - 'action_marked_the_due_date_incomplete' - => '{memberCreator} marked the due date on {card} incomplete', - 'action_marked_the_due_date_incomplete@card' - => '{memberCreator} marked the due date incomplete', - 'action_member_joined_card' - => '{memberCreator} joined {card}', - 'action_member_joined_card@card' - => '{memberCreator} joined this card', - 'action_member_left_card' - => '{memberCreator} left {card}', - 'action_member_left_card@card' - => '{memberCreator} left this card', - 'action_members_visibility' - => 'its members', - 'action_move_card_from_board' - => '{memberCreator} transferred {card} to {board}', - 'action_move_card_from_board@card' - => '{memberCreator} transferred this card to {board}', - 'action_move_card_from_list_to_list' - => '{memberCreator} moved {card} from {listBefore} to {listAfter}', - 'action_move_card_from_list_to_list@card' - => '{memberCreator} moved this card from {listBefore} to {listAfter}', - 'action_move_card_to_board' - => '{memberCreator} transferred {card} from {board}', - 'action_move_card_to_board@card' - => '{memberCreator} transferred this card from {board}', - 'action_move_list_from_board' - => '{memberCreator} transferred {list} to {board}', - 'action_move_list_to_board' - => '{memberCreator} transferred {list} from {board}', - 'action_moved_card_higher' - => '{memberCreator} moved {card} higher', - 'action_moved_card_higher@card' - => '{memberCreator} moved this card higher', - 'action_moved_card_lower' - => '{memberCreator} moved {card} lower', - 'action_moved_card_lower@card' - => '{memberCreator} moved this card lower', - 'action_moved_checkitem_higher' - => '{memberCreator} moved {checkitem} higher in the checklist {checklist}', - 'action_moved_checkitem_lower' - => '{memberCreator} moved {checkitem} higher in the checklist {checklist}', - 'action_moved_list_left' - => '{memberCreator} moved list {list} left on {board}', - 'action_moved_list_left@board' - => '{memberCreator} moved {list} left on this board', - 'action_moved_list_right' - => '{memberCreator} moved list {list} right on {board}', - 'action_moved_list_right@board' - => '{memberCreator} moved {list} right on this board', - 'action_observers_visibility' - => 'members and observers', - 'action_on' - => 'on', - 'action_org_visibility' - => 'members of its team', - 'action_public_visibility' - => 'the public', - 'action_remove_checklist_from_card' - => '{memberCreator} removed {checklist} from {card}', - 'action_remove_checklist_from_card@card' - => '{memberCreator} removed {checklist} from this card', - 'action_remove_from_organization_board' - => '{memberCreator} removed {board} from {organization}', - 'action_remove_from_organization_board@board' - => '{memberCreator} removed this board from {organization}', - 'action_remove_label_from_card' - => '{memberCreator} removed the {label} label from {card}', - 'action_remove_label_from_card@card' - => '{memberCreator} removed the {label} label from this card', - 'action_remove_organization_from_enterprise' - => '{memberCreator} removed team {organization} from the enterprise {enterprise}', - 'action_removed_a_due_date' - => '{memberCreator} removed the due date from {card}', - 'action_removed_a_due_date@card' - => '{memberCreator} removed the due date from this card', - 'action_removed_from_board' - => '{memberCreator} removed {member} from {board}', - 'action_removed_from_board@board' - => '{memberCreator} removed {member} from this board', - 'action_removed_member_from_card' - => '{memberCreator} removed {member} from {card}', - 'action_removed_member_from_card@card' - => '{memberCreator} removed {member} from this card', - 'action_removed_member_from_organization' - => '{memberCreator} removed {member} from {organization}', - 'action_removed_vote_for_card' - => '{memberCreator} removed vote for {card}', - 'action_removed_vote_for_card@card' - => '{memberCreator} removed vote for this card', - 'action_rename_custom_field' - => '{memberCreator} renamed the {customField} custom field on {board} (from {name})', - 'action_rename_custom_field@board' - => '{memberCreator} renamed the {customField} custom field on this board (from {name})', - 'action_renamed_card' - => '{memberCreator} renamed {card} (from {name})', - 'action_renamed_card@card' - => '{memberCreator} renamed this card (from {name})', - 'action_renamed_checkitem' - => '{memberCreator} renamed {checkitem} (from {name})', - 'action_renamed_checklist' - => '{memberCreator} renamed {checklist} (from {name})', - 'action_renamed_list' - => '{memberCreator} renamed list {list} (from {name})', - 'action_reopened_board' - => '{memberCreator} re-opened {board}', - 'action_reopened_board@board' - => '{memberCreator} re-opened this board', - 'action_sent_card_to_board' - => '{memberCreator} sent {card} to the board', - 'action_sent_card_to_board@card' - => '{memberCreator} sent this card to the board', - 'action_sent_list_to_board' - => '{memberCreator} sent list {list} to the board', - 'action_set_card_aging_mode_pirate' - => '{memberCreator} changed card aging to pirate mode', - 'action_set_card_aging_mode_regular' - => '{memberCreator} changed card aging to regular mode', - 'action_update_board_desc' - => '{memberCreator} changed description of {board}', - 'action_update_board_desc@board' - => '{memberCreator} changed description of this board', - 'action_update_board_name' - => '{memberCreator} renamed {board} (from {name})', - 'action_update_board_name@board' - => '{memberCreator} renamed this board (from {name})', - 'action_update_custom_field' - => '{memberCreator} updated the {customField} custom field on {board}', - 'action_update_custom_field@board' - => '{memberCreator} updated the {customField} custom field on this board', - 'action_update_custom_field_item' - => '{memberCreator} updated the value for the {customFieldItem} custom field on {card}', - 'action_update_custom_field_item@card' - => '{memberCreator} updated the value for the {customFieldItem} custom field on this card', - 'action_updated_their_bio' - => '{memberCreator} updated their bio', - 'action_updated_their_display_name' - => '{memberCreator} updated their display name', - 'action_updated_their_initials' - => '{memberCreator} updated their initials', - 'action_updated_their_username' - => '{memberCreator} updated their username', - 'action_vote_on_card' - => '{memberCreator} voted for {card}', - 'action_vote_on_card@card' - => '{memberCreator} voted for this card', - 'action_voting' - => 'voting', - 'action_withdraw_enterprise_join_request' - => '{memberCreator} withdrew a request to add team {organization} to the enterprise {enterprise}' - ); +class TrelloBridge extends BridgeAbstract +{ + const NAME = 'Trello Bridge'; + const URI = 'https://trello.com/'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Returns activity on Trello boards or cards'; + const MAINTAINER = 'Roliga'; + const PARAMETERS = [ + 'Board' => [ + 'b' => [ + 'name' => 'Board ID', + 'required' => true, + 'exampleValue' => 'g9mdhdzg', + 'title' => 'Taken from Trello URL, e.g. trello.com/b/[Board ID]' + ] + ], + 'Card' => [ + 'c' => [ + 'name' => 'Card ID', + 'required' => true, + 'exampleValue' => '8vddc9pE', + 'title' => 'Taken from Trello URL, e.g. trello.com/c/[Card ID]' + ] + ] + ]; - const REQUEST_ACTIONS_BOARDS = array( - 'addAttachmentToCard', - 'addChecklistToCard', - 'addMemberToCard', - 'commentCard', - 'copyCommentCard', - 'convertToCardFromCheckItem', - 'createCard', - 'copyCard', - 'deleteAttachmentFromCard', - 'emailCard', - 'moveCardFromBoard', - 'moveCardToBoard', - 'removeChecklistFromCard', - 'removeMemberFromCard', - 'updateCard:idList', - 'updateCard:closed', - 'updateCard:due', - 'updateCard:dueComplete', - 'updateCheckItemStateOnCard', - 'updateCustomFieldItem', - 'addMemberToBoard', - 'addToOrganizationBoard', - 'copyBoard', - 'createBoard', - 'createCustomField', - 'createList', - 'deleteCard', - 'deleteCustomField', - 'disablePlugin', - 'disablePowerUp', - 'enablePlugin', - 'enablePowerUp', - 'makeAdminOfBoard', - 'makeNormalMemberOfBoard', - 'makeObserverOfBoard', - 'moveListFromBoard', - 'moveListToBoard', - 'removeFromOrganizationBoard', - 'unconfirmedBoardInvitation', - 'unconfirmedOrganizationInvitation', - 'updateBoard', - 'updateCustomField', - 'updateList:closed' - ); + /* + * This was extracted from webpack on a Trello page, e.g. trello.com/b/g9mdhdzg + * In the browser's inspector/debugger go to the Debugger (Firefox) or + * Sources (Chromium) tab, these values can be found at: + * webpack:///resources/strings/actions/en.json + */ + const ACTION_TEXTS = [ + 'action_accept_enterprise_join_request' + => '{memberCreator} added team {organization} to the enterprise {enterprise}', + 'action_add_attachment_to_card' + => '{memberCreator} attached {attachment} to {card} {attachmentPreview}', + 'action_add_attachment_to_card@card' + => '{memberCreator} attached {attachment} to this card {attachmentPreview}', + 'action_add_checklist_to_card' + => '{memberCreator} added {checklist} to {card}', + 'action_add_checklist_to_card@card' + => '{memberCreator} added {checklist} to this card', + 'action_add_label_to_card' + => '{memberCreator} added the {label} label to {card}', + 'action_add_label_to_card@card' + => '{memberCreator} added the {label} label to this card', + 'action_add_organization_to_enterprise' + => '{memberCreator} added team {organization} to the enterprise {enterprise}', + 'action_add_to_organization_board' + => '{memberCreator} added {board} to {organization}', + 'action_add_to_organization_board@board' + => '{memberCreator} added this board to {organization}', + 'action_added_a_due_date' + => '{memberCreator} set {card} to be due {date}', + 'action_added_a_due_date@card' + => '{memberCreator} set this card to be due {date}', + 'action_added_list_to_board' + => '{memberCreator} added list {list} to {board}', + 'action_added_list_to_board@board' + => '{memberCreator} added {list} to this board', + 'action_added_member_to_board' + => '{memberCreator} added {member} to {board}', + 'action_added_member_to_board@board' + => '{memberCreator} added {member} to this board', + 'action_added_member_to_board_as_admin' + => '{memberCreator} added {member} to {board} as an admin', + 'action_added_member_to_board_as_admin@board' + => '{memberCreator} added {member} to this board as an admin', + 'action_added_member_to_board_as_observer' + => '{memberCreator} added {member} to {board} as an observer', + 'action_added_member_to_board_as_observer@board' + => '{memberCreator} added {member} to this board as an observer', + 'action_added_member_to_card' + => '{memberCreator} added {member} to {card}', + 'action_added_member_to_card@card' + => '{memberCreator} added {member} to this card', + 'action_added_member_to_organization' + => '{memberCreator} added {member} to {organization}', + 'action_added_member_to_organization_as_admin' + => '{memberCreator} added {member} to {organization} as an admin', + 'action_admins_visibility' + => 'its admins', + 'action_another_board' + => 'another board', + 'action_archived_card' + => '{memberCreator} archived {card}', + 'action_archived_card@card' + => '{memberCreator} archived this card', + 'action_archived_list' + => '{memberCreator} archived list {list}', + 'action_became_a_normal_user_in_organization' + => '{memberCreator} became a normal user in {organization}', + 'action_became_a_normal_user_on' + => '{memberCreator} became a normal user on {board}', + 'action_became_a_normal_user_on@board' + => '{memberCreator} became a normal user on this board', + 'action_became_an_admin_of_organization' + => '{memberCreator} became an admin of {organization}', + 'action_board_perm_level' + => '{memberCreator} made {board} visible to {level}', + 'action_board_perm_level@board' + => '{memberCreator} made this board visible to {level}', + 'action_calendar' + => 'calendar', + 'action_cardAging' + => 'card aging', + 'action_changed_a_due_date' + => '{memberCreator} changed the due date of {card} to {date}', + 'action_changed_a_due_date@card' + => '{memberCreator} changed the due date of this card to {date}', + 'action_changed_board_background' + => '{memberCreator} changed the background of {board}', + 'action_changed_board_background@board' + => '{memberCreator} changed the background of this board', + 'action_changed_description_of_card' + => '{memberCreator} changed description of {card}', + 'action_changed_description_of_card@card' + => '{memberCreator} changed description of this card', + 'action_changed_description_of_organization' + => '{memberCreator} changed description of {organization}', + 'action_changed_display_name_of_organization' + => '{memberCreator} changed display name of {organization}', + 'action_changed_name_of_organization' + => '{memberCreator} changed name of {organization}', + 'action_changed_website_of_organization' + => '{memberCreator} changed website of {organization}', + 'action_closed_board' + => '{memberCreator} closed {board}', + 'action_closed_board@board' + => '{memberCreator} closed this board', + 'action_comment_on_card' + => '{memberCreator} {contextOn} {card} {comment}', + 'action_comment_on_card@card' + => '{memberCreator} {comment}', + 'action_completed_checkitem' + => '{memberCreator} completed {checkitem} on {card}', + 'action_completed_checkitem@card' + => '{memberCreator} completed {checkitem} on this card', + 'action_convert_to_card_from_checkitem' + => '{memberCreator} converted {card} from a checklist item on {cardSource}', + 'action_convert_to_card_from_checkitem@card' + => '{memberCreator} converted this card from a checklist item on {cardSource}', + 'action_convert_to_card_from_checkitem@cardSource' + => '{memberCreator} converted {card} from a checklist item on this card', + 'action_copy_board' + => '{memberCreator} copied this board from {board}', + 'action_copy_card' + => '{memberCreator} copied {card} from {cardSource} in list {list}', + 'action_copy_card@card' + => '{memberCreator} copied this card from {cardSource} in list {list}', + 'action_copy_comment_from_card' + => '{memberCreator} copied comment by {member} from card {card} {comment}', + 'action_create_board' + => '{memberCreator} created {board}', + 'action_create_board@board' + => '{memberCreator} created this board', + 'action_create_card' + => '{memberCreator} added {card} to {list}', + 'action_create_card@card' + => '{memberCreator} added this card to {list}', + 'action_create_custom_field' + => '{memberCreator} created the {customField} custom field on {board}', + 'action_create_custom_field@board' + => '{memberCreator} created the {customField} custom field on this board', + 'action_create_enterprise_join_request' + => '{memberCreator} requested to add team {organization} to the enterprise {enterprise}', + 'action_created_an_invitation_to_board' + => '{memberCreator} created an invitation to {board}', + 'action_created_an_invitation_to_board@board' + => '{memberCreator} created an invitation to this board', + 'action_created_an_invitation_to_organization' + => '{memberCreator} created an invitation to {organization}', + 'action_created_checklist_on_board' + => '{memberCreator} created {checklist} on {board}', + 'action_created_checklist_on_board@board' + => '{memberCreator} created {checklist} on this board', + 'action_created_organization' + => '{memberCreator} created {organization}', + 'action_decline_enterprise_join_request' + => '{memberCreator} declined the request to add team {organization} to the enterprise {enterprise}', + 'action_delete_attachment_from_card' + => '{memberCreator} deleted the {attachment} attachment from {card}', + 'action_delete_attachment_from_card@card' + => '{memberCreator} deleted the {attachment} attachment from this card', + 'action_delete_card' + => '{memberCreator} deleted card #{idCard} from {list}', + 'action_delete_custom_field' + => '{memberCreator} deleted the {customField} custom field from {board}', + 'action_delete_custom_field@board' + => '{memberCreator} deleted the {customField} custom field from this board', + 'action_deleted_account' + => '[deleted account]', + 'action_deleted_an_invitation_to_board' + => '{memberCreator} deleted an invitation to {board}', + 'action_deleted_an_invitation_to_board@board' + => '{memberCreator} deleted an invitation to this board', + 'action_deleted_an_invitation_to_organization' + => '{memberCreator} deleted an invitation to {organization}', + 'action_deleted_checkitem' + => '{memberCreator} deleted task {checkitem} on {checklist}', + 'action_disabled_calendar_feed' + => '{memberCreator} disabled the iCalendar feed on {board}', + 'action_disabled_calendar_feed@board' + => '{memberCreator} disabled the iCalendar feed on this board', + 'action_disabled_card_covers' + => '{memberCreator} disabled card cover images on {board}', + 'action_disabled_card_covers@board' + => '{memberCreator} disabled card cover images on this board', + 'action_disabled_commenting' + => '{memberCreator} disabled commenting on {board}', + 'action_disabled_commenting@board' + => '{memberCreator} disabled commenting on this board', + 'action_disabled_inviting' + => '{memberCreator} disabled inviting on {board}', + 'action_disabled_inviting@board' + => '{memberCreator} disabled inviting on this board', + 'action_disabled_plugin' + => '{memberCreator} disabled the {plugin} Power-Up', + 'action_disabled_powerup' + => '{memberCreator} disabled the {powerup} Power-Up', + 'action_disabled_self_join' + => '{memberCreator} disabled self join on {board}', + 'action_disabled_self_join@board' + => '{memberCreator} disabled self join on this board', + 'action_disabled_voting' + => '{memberCreator} disabled voting on {board}', + 'action_disabled_voting@board' + => '{memberCreator} disabled voting on this board', + 'action_due_date_change' + => '{memberCreator}', + 'action_email_card' + => '{memberCreator} emailed {card} to {list}', + 'action_email_card@card' + => '{memberCreator} emailed this card to {list}', + 'action_email_card_from' + => '{memberCreator} emailed {card} to {list} from {from}', + 'action_email_card_from@card' + => '{memberCreator} emailed this card to {list} from {from}', + 'action_enabled_calendar_feed' + => '{memberCreator} enabled the iCalendar feed on {board}', + 'action_enabled_calendar_feed@board' + => '{memberCreator} enabled the iCalendar feed on this board', + 'action_enabled_card_covers' + => '{memberCreator} enabled card cover images on {board}', + 'action_enabled_card_covers@board' + => '{memberCreator} enabled card cover images on this board', + 'action_enabled_plugin' + => '{memberCreator} enabled the {plugin} Power-Up', + 'action_enabled_powerup' + => '{memberCreator} enabled the {powerup} Power-Up', + 'action_enabled_self_join' + => '{memberCreator} enabled self join on {board}', + 'action_enabled_self_join@board' + => '{memberCreator} enabled self join on this board', + 'action_hid_board' + => '{memberCreator} hid {board}', + 'action_hid_board@board' + => '{memberCreator} hid this board', + 'action_invited_an_unconfirmed_member_to_board' + => '{memberCreator} invited an unconfirmed member to {board}', + 'action_invited_an_unconfirmed_member_to_board@board' + => '{memberCreator} invited an unconfirmed member to this board', + 'action_invited_an_unconfirmed_member_to_organization' + => '{memberCreator} invited an unconfirmed member to {organization}', + 'action_joined_board' + => '{memberCreator} joined {board}', + 'action_joined_board@board' + => '{memberCreator} joined this board', + 'action_joined_board_by_invitation_link' + => '{memberCreator} joined {board} with an invitation link from {memberInviter}', + 'action_joined_board_by_invitation_link@board' + => '{memberCreator} joined this board with an invitation link from {memberInviter}', + 'action_joined_organization' + => '{memberCreator} joined {organization}', + 'action_joined_organization_by_invitation_link' + => '{memberCreator} joined {organization} with an invitation link from {memberInviter}', + 'action_left_board' + => '{memberCreator} left {board}', + 'action_left_board@board' + => '{memberCreator} left this board', + 'action_left_organization' + => '{memberCreator} left {organization}', + 'action_made_a_normal_user_in_organization' + => '{memberCreator} made {member} a normal user in {organization}', + 'action_made_a_normal_user_on' + => '{memberCreator} made {member} a normal user on {board}', + 'action_made_a_normal_user_on@board' + => '{memberCreator} made {member} a normal user on this board', + 'action_made_admin_of_board' + => '{memberCreator} made {member} an admin of {board}', + 'action_made_admin_of_board@board' + => '{memberCreator} made {member} an admin of this board', + 'action_made_an_admin_of_organization' + => '{memberCreator} made {member} an admin of {organization}', + 'action_made_commenting_on' + => '{memberCreator} made commenting on {board} available to {level}', + 'action_made_commenting_on@board' + => '{memberCreator} made commenting on this board available to {level}', + 'action_made_inviting_on' + => '{memberCreator} made inviting on {board} available to {level}', + 'action_made_inviting_on@board' + => '{memberCreator} made inviting on this board available to {level}', + 'action_made_observer_of_board' + => '{memberCreator} made {member} an observer of {board}', + 'action_made_observer_of_board@board' + => '{memberCreator} made {member} an observer of this board', + 'action_made_self_admin_of_board' + => '{memberCreator} made themselves an admin of {board}', + 'action_made_self_admin_of_board@board' + => '{memberCreator} made themselves an admin of this board', + 'action_made_self_observer_of_board' + => '{memberCreator} became an observer of {board}', + 'action_made_self_observer_of_board@board' + => '{memberCreator} became an observer of this board', + 'action_made_voting_on' + => '{memberCreator} made voting on {board} available to {level}', + 'action_made_voting_on@board' + => '{memberCreator} made voting on this board available to {level}', + 'action_marked_checkitem_incomplete' + => '{memberCreator} marked {checkitem} incomplete on {card}', + 'action_marked_checkitem_incomplete@card' + => '{memberCreator} marked {checkitem} incomplete on this card', + 'action_marked_the_due_date_complete' + => '{memberCreator} marked the due date on {card} complete', + 'action_marked_the_due_date_complete@card' + => '{memberCreator} marked the due date complete', + 'action_marked_the_due_date_incomplete' + => '{memberCreator} marked the due date on {card} incomplete', + 'action_marked_the_due_date_incomplete@card' + => '{memberCreator} marked the due date incomplete', + 'action_member_joined_card' + => '{memberCreator} joined {card}', + 'action_member_joined_card@card' + => '{memberCreator} joined this card', + 'action_member_left_card' + => '{memberCreator} left {card}', + 'action_member_left_card@card' + => '{memberCreator} left this card', + 'action_members_visibility' + => 'its members', + 'action_move_card_from_board' + => '{memberCreator} transferred {card} to {board}', + 'action_move_card_from_board@card' + => '{memberCreator} transferred this card to {board}', + 'action_move_card_from_list_to_list' + => '{memberCreator} moved {card} from {listBefore} to {listAfter}', + 'action_move_card_from_list_to_list@card' + => '{memberCreator} moved this card from {listBefore} to {listAfter}', + 'action_move_card_to_board' + => '{memberCreator} transferred {card} from {board}', + 'action_move_card_to_board@card' + => '{memberCreator} transferred this card from {board}', + 'action_move_list_from_board' + => '{memberCreator} transferred {list} to {board}', + 'action_move_list_to_board' + => '{memberCreator} transferred {list} from {board}', + 'action_moved_card_higher' + => '{memberCreator} moved {card} higher', + 'action_moved_card_higher@card' + => '{memberCreator} moved this card higher', + 'action_moved_card_lower' + => '{memberCreator} moved {card} lower', + 'action_moved_card_lower@card' + => '{memberCreator} moved this card lower', + 'action_moved_checkitem_higher' + => '{memberCreator} moved {checkitem} higher in the checklist {checklist}', + 'action_moved_checkitem_lower' + => '{memberCreator} moved {checkitem} higher in the checklist {checklist}', + 'action_moved_list_left' + => '{memberCreator} moved list {list} left on {board}', + 'action_moved_list_left@board' + => '{memberCreator} moved {list} left on this board', + 'action_moved_list_right' + => '{memberCreator} moved list {list} right on {board}', + 'action_moved_list_right@board' + => '{memberCreator} moved {list} right on this board', + 'action_observers_visibility' + => 'members and observers', + 'action_on' + => 'on', + 'action_org_visibility' + => 'members of its team', + 'action_public_visibility' + => 'the public', + 'action_remove_checklist_from_card' + => '{memberCreator} removed {checklist} from {card}', + 'action_remove_checklist_from_card@card' + => '{memberCreator} removed {checklist} from this card', + 'action_remove_from_organization_board' + => '{memberCreator} removed {board} from {organization}', + 'action_remove_from_organization_board@board' + => '{memberCreator} removed this board from {organization}', + 'action_remove_label_from_card' + => '{memberCreator} removed the {label} label from {card}', + 'action_remove_label_from_card@card' + => '{memberCreator} removed the {label} label from this card', + 'action_remove_organization_from_enterprise' + => '{memberCreator} removed team {organization} from the enterprise {enterprise}', + 'action_removed_a_due_date' + => '{memberCreator} removed the due date from {card}', + 'action_removed_a_due_date@card' + => '{memberCreator} removed the due date from this card', + 'action_removed_from_board' + => '{memberCreator} removed {member} from {board}', + 'action_removed_from_board@board' + => '{memberCreator} removed {member} from this board', + 'action_removed_member_from_card' + => '{memberCreator} removed {member} from {card}', + 'action_removed_member_from_card@card' + => '{memberCreator} removed {member} from this card', + 'action_removed_member_from_organization' + => '{memberCreator} removed {member} from {organization}', + 'action_removed_vote_for_card' + => '{memberCreator} removed vote for {card}', + 'action_removed_vote_for_card@card' + => '{memberCreator} removed vote for this card', + 'action_rename_custom_field' + => '{memberCreator} renamed the {customField} custom field on {board} (from {name})', + 'action_rename_custom_field@board' + => '{memberCreator} renamed the {customField} custom field on this board (from {name})', + 'action_renamed_card' + => '{memberCreator} renamed {card} (from {name})', + 'action_renamed_card@card' + => '{memberCreator} renamed this card (from {name})', + 'action_renamed_checkitem' + => '{memberCreator} renamed {checkitem} (from {name})', + 'action_renamed_checklist' + => '{memberCreator} renamed {checklist} (from {name})', + 'action_renamed_list' + => '{memberCreator} renamed list {list} (from {name})', + 'action_reopened_board' + => '{memberCreator} re-opened {board}', + 'action_reopened_board@board' + => '{memberCreator} re-opened this board', + 'action_sent_card_to_board' + => '{memberCreator} sent {card} to the board', + 'action_sent_card_to_board@card' + => '{memberCreator} sent this card to the board', + 'action_sent_list_to_board' + => '{memberCreator} sent list {list} to the board', + 'action_set_card_aging_mode_pirate' + => '{memberCreator} changed card aging to pirate mode', + 'action_set_card_aging_mode_regular' + => '{memberCreator} changed card aging to regular mode', + 'action_update_board_desc' + => '{memberCreator} changed description of {board}', + 'action_update_board_desc@board' + => '{memberCreator} changed description of this board', + 'action_update_board_name' + => '{memberCreator} renamed {board} (from {name})', + 'action_update_board_name@board' + => '{memberCreator} renamed this board (from {name})', + 'action_update_custom_field' + => '{memberCreator} updated the {customField} custom field on {board}', + 'action_update_custom_field@board' + => '{memberCreator} updated the {customField} custom field on this board', + 'action_update_custom_field_item' + => '{memberCreator} updated the value for the {customFieldItem} custom field on {card}', + 'action_update_custom_field_item@card' + => '{memberCreator} updated the value for the {customFieldItem} custom field on this card', + 'action_updated_their_bio' + => '{memberCreator} updated their bio', + 'action_updated_their_display_name' + => '{memberCreator} updated their display name', + 'action_updated_their_initials' + => '{memberCreator} updated their initials', + 'action_updated_their_username' + => '{memberCreator} updated their username', + 'action_vote_on_card' + => '{memberCreator} voted for {card}', + 'action_vote_on_card@card' + => '{memberCreator} voted for this card', + 'action_voting' + => 'voting', + 'action_withdraw_enterprise_join_request' + => '{memberCreator} withdrew a request to add team {organization} to the enterprise {enterprise}' + ]; - const REQUEST_ACTIONS_CARDS = array( - 'addAttachmentToCard', - 'addChecklistToCard', - 'addMemberToCard', - 'commentCard', - 'copyCommentCard', - 'convertToCardFromCheckItem', - 'createCard', - 'copyCard', - 'deleteAttachmentFromCard', - 'emailCard', - 'moveCardFromBoard', - 'moveCardToBoard', - 'removeChecklistFromCard', - 'removeMemberFromCard', - 'updateCard:idList', - 'updateCard:closed', - 'updateCard:due', - 'updateCard:dueComplete', - 'updateCheckItemStateOnCard', - 'updateCustomFieldItem' - ); + const REQUEST_ACTIONS_BOARDS = [ + 'addAttachmentToCard', + 'addChecklistToCard', + 'addMemberToCard', + 'commentCard', + 'copyCommentCard', + 'convertToCardFromCheckItem', + 'createCard', + 'copyCard', + 'deleteAttachmentFromCard', + 'emailCard', + 'moveCardFromBoard', + 'moveCardToBoard', + 'removeChecklistFromCard', + 'removeMemberFromCard', + 'updateCard:idList', + 'updateCard:closed', + 'updateCard:due', + 'updateCard:dueComplete', + 'updateCheckItemStateOnCard', + 'updateCustomFieldItem', + 'addMemberToBoard', + 'addToOrganizationBoard', + 'copyBoard', + 'createBoard', + 'createCustomField', + 'createList', + 'deleteCard', + 'deleteCustomField', + 'disablePlugin', + 'disablePowerUp', + 'enablePlugin', + 'enablePowerUp', + 'makeAdminOfBoard', + 'makeNormalMemberOfBoard', + 'makeObserverOfBoard', + 'moveListFromBoard', + 'moveListToBoard', + 'removeFromOrganizationBoard', + 'unconfirmedBoardInvitation', + 'unconfirmedOrganizationInvitation', + 'updateBoard', + 'updateCustomField', + 'updateList:closed' + ]; - private $feedName = ''; - private $feedURI = ''; + const REQUEST_ACTIONS_CARDS = [ + 'addAttachmentToCard', + 'addChecklistToCard', + 'addMemberToCard', + 'commentCard', + 'copyCommentCard', + 'convertToCardFromCheckItem', + 'createCard', + 'copyCard', + 'deleteAttachmentFromCard', + 'emailCard', + 'moveCardFromBoard', + 'moveCardToBoard', + 'removeChecklistFromCard', + 'removeMemberFromCard', + 'updateCard:idList', + 'updateCard:closed', + 'updateCard:due', + 'updateCard:dueComplete', + 'updateCheckItemStateOnCard', + 'updateCustomFieldItem' + ]; - private function queryAPI($path, $params = array()) { - $data = json_decode(getContents('https://trello.com/1/' - . $path - . '?' - . http_build_query($params))); - return $data; - } + private $feedName = ''; + private $feedURI = ''; - private function renderAction($action, $textOnly = false) { - if(!array_key_exists($action->display->translationKey, self::ACTION_TEXTS)) { - return ''; - } + private function queryAPI($path, $params = []) + { + $data = json_decode(getContents('https://trello.com/1/' + . $path + . '?' + . http_build_query($params))); + return $data; + } - $strings = array(); - $entities = (array)$action->display->entities; + private function renderAction($action, $textOnly = false) + { + if (!array_key_exists($action->display->translationKey, self::ACTION_TEXTS)) { + return ''; + } - foreach($entities as $entity_name => $entity) { - $type = $entity->type; - if($type === 'attachmentPreview' - && !$textOnly - && isset($entity->originalUrl)) { - $string = '<p><a href="' - . $entity->originalUrl - . '"><img src="' - . $entity->previewUrl - . '"></a></p>'; - } elseif($type === 'card' && !$textOnly) { - $string = '<a href="https://trello.com/c/' - . $entity->shortLink - . '">' - . $entity->text - . '</a>'; - } elseif($type === 'member' && !$textOnly) { - $string = '<a href="https://trello.com/' - . $entity->username - . '">' - . $entity->text - . '</a>'; - } elseif($type === 'date') { - $string = gmdate('M j, Y \a\t g:i A T', strtotime($entity->date)); - } elseif($type === 'translatable') { - $string = self::ACTION_TEXTS[$entity->translationKey]; - } else { - if(isset($entity->text)) { - $string = $entity->text; - } else { - $string = ''; - } - } - $strings['{' . $entity_name . '}'] = $string; - } + $strings = []; + $entities = (array)$action->display->entities; - return str_replace(array_keys($strings), - array_values($strings), - self::ACTION_TEXTS[$action->display->translationKey]); - } + foreach ($entities as $entity_name => $entity) { + $type = $entity->type; + if ( + $type === 'attachmentPreview' + && !$textOnly + && isset($entity->originalUrl) + ) { + $string = '<p><a href="' + . $entity->originalUrl + . '"><img src="' + . $entity->previewUrl + . '"></a></p>'; + } elseif ($type === 'card' && !$textOnly) { + $string = '<a href="https://trello.com/c/' + . $entity->shortLink + . '">' + . $entity->text + . '</a>'; + } elseif ($type === 'member' && !$textOnly) { + $string = '<a href="https://trello.com/' + . $entity->username + . '">' + . $entity->text + . '</a>'; + } elseif ($type === 'date') { + $string = gmdate('M j, Y \a\t g:i A T', strtotime($entity->date)); + } elseif ($type === 'translatable') { + $string = self::ACTION_TEXTS[$entity->translationKey]; + } else { + if (isset($entity->text)) { + $string = $entity->text; + } else { + $string = ''; + } + } + $strings['{' . $entity_name . '}'] = $string; + } - public function collectData() { - $apiParams = array( - 'actions_display' => 'true', - 'fields' => 'name,url' - ); - switch($this->queriedContext) { - case 'Board': - $apiParams['actions'] = implode(',', self::REQUEST_ACTIONS_BOARDS); - $data = $this->queryAPI('boards/' . $this->getInput('b'), $apiParams); - break; - case 'Card': - $apiParams['actions'] = implode(',', self::REQUEST_ACTIONS_CARDS); - $data = $this->queryAPI('cards/' . $this->getInput('c'), $apiParams); - break; - default: - returnClientError('Invalid context'); - } + return str_replace( + array_keys($strings), + array_values($strings), + self::ACTION_TEXTS[$action->display->translationKey] + ); + } - $this->feedName = $data->name; - $this->feedURI = $data->url; + public function collectData() + { + $apiParams = [ + 'actions_display' => 'true', + 'fields' => 'name,url' + ]; + switch ($this->queriedContext) { + case 'Board': + $apiParams['actions'] = implode(',', self::REQUEST_ACTIONS_BOARDS); + $data = $this->queryAPI('boards/' . $this->getInput('b'), $apiParams); + break; + case 'Card': + $apiParams['actions'] = implode(',', self::REQUEST_ACTIONS_CARDS); + $data = $this->queryAPI('cards/' . $this->getInput('c'), $apiParams); + break; + default: + returnClientError('Invalid context'); + } - foreach($data->actions as $action) { - $item = array(); + $this->feedName = $data->name; + $this->feedURI = $data->url; - $item['title'] = $this->renderAction($action, true); - $item['timestamp'] = strtotime($action->date); - $item['author'] = $action->memberCreator->fullName; - $item['categories'] = array( - 'trello', - $action->data->board->name, - $action->type - ); - if(isset($action->data->card)) { - $item['categories'][] = $action->data->card->name; - $item['uri'] = 'https://trello.com/c/' - . $action->data->card->shortLink - . '#action-' - . $action->id; - } else { - $item['uri'] = 'https://trello.com/b/' - . $action->data->board->shortLink; - } - $item['content'] = $this->renderAction($action, false); - if(isset($action->data->attachment->url)) { - $item['enclosures'] = array($action->data->attachment->url); - } + foreach ($data->actions as $action) { + $item = []; - $this->items[] = $item; - } - } + $item['title'] = $this->renderAction($action, true); + $item['timestamp'] = strtotime($action->date); + $item['author'] = $action->memberCreator->fullName; + $item['categories'] = [ + 'trello', + $action->data->board->name, + $action->type + ]; + if (isset($action->data->card)) { + $item['categories'][] = $action->data->card->name; + $item['uri'] = 'https://trello.com/c/' + . $action->data->card->shortLink + . '#action-' + . $action->id; + } else { + $item['uri'] = 'https://trello.com/b/' + . $action->data->board->shortLink; + } + $item['content'] = $this->renderAction($action, false); + if (isset($action->data->attachment->url)) { + $item['enclosures'] = [$action->data->attachment->url]; + } - public function detectParameters($url) { - $regex = '/^(https?:\/\/)?trello\.com\/([bc])\/([^\/?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - return array($matches[2] => $matches[3]); - } else { - return null; - } - } + $this->items[] = $item; + } + } - public function getURI() { - switch($this->queriedContext) { - case 'Board': - case 'Card': - return $this->feedURI; - default: return parent::getURI(); - } - } + public function detectParameters($url) + { + $regex = '/^(https?:\/\/)?trello\.com\/([bc])\/([^\/?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + return [$matches[2] => $matches[3]]; + } else { + return null; + } + } - public function getName() { - switch($this->queriedContext) { - case 'Board': - case 'Card': - return $this->feedName; - default: return parent::getName(); - } - } + public function getURI() + { + switch ($this->queriedContext) { + case 'Board': + case 'Card': + return $this->feedURI; + default: + return parent::getURI(); + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Board': + case 'Card': + return $this->feedName; + default: + return parent::getName(); + } + } } diff --git a/bridges/TwitScoopBridge.php b/bridges/TwitScoopBridge.php index 4a85dcfc..8865e06c 100644 --- a/bridges/TwitScoopBridge.php +++ b/bridges/TwitScoopBridge.php @@ -1,120 +1,123 @@ <?php -class TwitScoopBridge extends BridgeAbstract { - const NAME = 'TwitScoop Bridge'; - const URI = 'https://www.twitscoop.com'; - const DESCRIPTION = 'Returns trending Twitter topics by country'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - array( - 'country' => array( - 'name' => 'Country', - 'type' => 'list', - 'values' => array( - 'Worldwide' => 'worldwide', - 'Algeria' => 'algeria', - 'Argentina' => 'argentina', - 'Australia' => 'australia', - 'Austria' => 'austria', - 'Bahrain' => 'bahrain', - 'Belarus' => 'belarus', - 'Belgium' => 'belgium', - 'Brazil' => 'brazil', - 'Canada' => 'canada', - 'Chile' => 'chile', - 'Colombia' => 'colombia', - 'Denmark' => 'denmark', - 'Dominican Republic' => 'dominican-republic', - 'Ecuador' => 'ecuador', - 'Egypt' => 'egypt', - 'France' => 'france', - 'Germany' => 'germany', - 'Ghana' => 'ghana', - 'Greece' => 'greece', - 'Guatemala' => 'guatemala', - 'India' => 'india', - 'Indonesia' => 'indonesia', - 'Ireland' => 'ireland', - 'Israel' => 'israel', - 'Italy' => 'italy', - 'Japan' => 'japan', - 'Jordan' => 'jordan', - 'Kenya' => 'kenya', - 'Korea' => 'korea', - 'Kuwait' => 'kuwait', - 'Latvia' => 'latvia', - 'Lebanon' => 'lebanon', - 'Malaysia' => 'malaysia', - 'Mexico' => 'mexico', - 'Netherlands' => 'netherlands', - 'New Zealand' => 'new-zealand', - 'Nigeria' => 'nigeria', - 'Norway' => 'norway', - 'Oman' => 'oman', - 'Pakistan' => 'pakistan', - 'Panama' => 'panama', - 'Peru' => 'peru', - 'Philippines' => 'philippines', - 'Poland' => 'poland', - 'Portugal' => 'portugal', - 'Puerto Rico' => 'puerto-rico', - 'Qatar' => 'qatar', - 'Russia' => 'russia', - 'Saudi Arabia' => 'saudi-arabia', - 'Singapore' => 'singapore', - 'South Africa' => 'south-africa', - 'Spain' => 'spain', - 'Sweden' => 'sweden', - 'Switzerland' => 'switzerland', - 'Thailand' => 'thailand', - 'Turkey' => 'turkey', - 'Ukraine' => 'ukraine', - 'United Arab Emirates' => 'united-arab-emirates', - 'United Kingdom' => 'united-kingdom', - 'United States' => 'united-states', - 'Venezuela' => 'venezuela', - 'Vietnam' => 'vietnam', - ) - ), - 'limit' => array( - 'name' => 'Topics', - 'type' => 'number', - 'title' => 'Number of trending topics to return. Max 50', - 'defaultValue' => 20, - ) - ) - ); - - const CACHE_TIMEOUT = 900; // 15 mins - - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI()); - - $updated = $html->find('time', 0)->datetime; - $trends = $html->find('div.trends', 0); - - $limit = $this->getInput('limit'); - - if ($limit > 50 || $limit < 1) { - $limit = 50; - } - - foreach($trends->find('ol.items > li') as $index => $li) { - $number = $index + 1; - - $item = array(); - - $name = rtrim($li->find('span.trend.name', 0)->plaintext, ' '); - $tweets = str_replace(' tweets', '', $li->find('span.tweets', 0)->plaintext); - $tweets = str_replace('<', '', $tweets); - - $item['title'] = '#' . $number . ' - ' . $name . ' (' . $tweets . ' tweets)'; - $item['uri'] = 'https://twitter.com/search?q=' . rawurlencode($name); - - if ($tweets === '10K') { - $tweets = 'less than 10K'; - } - - $item['content'] = <<<EOD + +class TwitScoopBridge extends BridgeAbstract +{ + const NAME = 'TwitScoop Bridge'; + const URI = 'https://www.twitscoop.com'; + const DESCRIPTION = 'Returns trending Twitter topics by country'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + [ + 'country' => [ + 'name' => 'Country', + 'type' => 'list', + 'values' => [ + 'Worldwide' => 'worldwide', + 'Algeria' => 'algeria', + 'Argentina' => 'argentina', + 'Australia' => 'australia', + 'Austria' => 'austria', + 'Bahrain' => 'bahrain', + 'Belarus' => 'belarus', + 'Belgium' => 'belgium', + 'Brazil' => 'brazil', + 'Canada' => 'canada', + 'Chile' => 'chile', + 'Colombia' => 'colombia', + 'Denmark' => 'denmark', + 'Dominican Republic' => 'dominican-republic', + 'Ecuador' => 'ecuador', + 'Egypt' => 'egypt', + 'France' => 'france', + 'Germany' => 'germany', + 'Ghana' => 'ghana', + 'Greece' => 'greece', + 'Guatemala' => 'guatemala', + 'India' => 'india', + 'Indonesia' => 'indonesia', + 'Ireland' => 'ireland', + 'Israel' => 'israel', + 'Italy' => 'italy', + 'Japan' => 'japan', + 'Jordan' => 'jordan', + 'Kenya' => 'kenya', + 'Korea' => 'korea', + 'Kuwait' => 'kuwait', + 'Latvia' => 'latvia', + 'Lebanon' => 'lebanon', + 'Malaysia' => 'malaysia', + 'Mexico' => 'mexico', + 'Netherlands' => 'netherlands', + 'New Zealand' => 'new-zealand', + 'Nigeria' => 'nigeria', + 'Norway' => 'norway', + 'Oman' => 'oman', + 'Pakistan' => 'pakistan', + 'Panama' => 'panama', + 'Peru' => 'peru', + 'Philippines' => 'philippines', + 'Poland' => 'poland', + 'Portugal' => 'portugal', + 'Puerto Rico' => 'puerto-rico', + 'Qatar' => 'qatar', + 'Russia' => 'russia', + 'Saudi Arabia' => 'saudi-arabia', + 'Singapore' => 'singapore', + 'South Africa' => 'south-africa', + 'Spain' => 'spain', + 'Sweden' => 'sweden', + 'Switzerland' => 'switzerland', + 'Thailand' => 'thailand', + 'Turkey' => 'turkey', + 'Ukraine' => 'ukraine', + 'United Arab Emirates' => 'united-arab-emirates', + 'United Kingdom' => 'united-kingdom', + 'United States' => 'united-states', + 'Venezuela' => 'venezuela', + 'Vietnam' => 'vietnam', + ] + ], + 'limit' => [ + 'name' => 'Topics', + 'type' => 'number', + 'title' => 'Number of trending topics to return. Max 50', + 'defaultValue' => 20, + ] + ] + ]; + + const CACHE_TIMEOUT = 900; // 15 mins + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + $updated = $html->find('time', 0)->datetime; + $trends = $html->find('div.trends', 0); + + $limit = $this->getInput('limit'); + + if ($limit > 50 || $limit < 1) { + $limit = 50; + } + + foreach ($trends->find('ol.items > li') as $index => $li) { + $number = $index + 1; + + $item = []; + + $name = rtrim($li->find('span.trend.name', 0)->plaintext, ' '); + $tweets = str_replace(' tweets', '', $li->find('span.tweets', 0)->plaintext); + $tweets = str_replace('<', '', $tweets); + + $item['title'] = '#' . $number . ' - ' . $name . ' (' . $tweets . ' tweets)'; + $item['uri'] = 'https://twitter.com/search?q=' . rawurlencode($name); + + if ($tweets === '10K') { + $tweets = 'less than 10K'; + } + + $item['content'] = <<<EOD <strong>Rank</strong><br> <p>{$number}</p> <Strong>Topic</strong><br> @@ -122,33 +125,34 @@ class TwitScoopBridge extends BridgeAbstract { <Strong>Tweets</strong><br> <p>{$tweets}</p> EOD; - $item['timestamp'] = $updated; - - $this->items[] = $item; + $item['timestamp'] = $updated; - if (count($this->items) >= $limit) { - break; - } - } + $this->items[] = $item; - } + if (count($this->items) >= $limit) { + break; + } + } + } - public function getURI() { - if (!is_null($this->getInput('country'))) { - return self::URI . '/' . $this->getInput('country'); - } + public function getURI() + { + if (!is_null($this->getInput('country'))) { + return self::URI . '/' . $this->getInput('country'); + } - return parent::getURI(); - } + return parent::getURI(); + } - public function getName() { - if (!is_null($this->getInput('country'))) { - $parameters = $this->getParameters(); - $values = array_flip($parameters[0]['country']['values']); + public function getName() + { + if (!is_null($this->getInput('country'))) { + $parameters = $this->getParameters(); + $values = array_flip($parameters[0]['country']['values']); - return $values[$this->getInput('country')] . ' - TwitScoop'; - } + return $values[$this->getInput('country')] . ' - TwitScoop'; + } - return parent::getName(); - } + return parent::getName(); + } } diff --git a/bridges/TwitchBridge.php b/bridges/TwitchBridge.php index a0e089dd..c26dafc6 100644 --- a/bridges/TwitchBridge.php +++ b/bridges/TwitchBridge.php @@ -1,58 +1,60 @@ <?php -class TwitchBridge extends BridgeAbstract { - const MAINTAINER = 'Roliga'; - const NAME = 'Twitch Bridge'; - const URI = 'https://twitch.tv/'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Twitch channel videos'; - const PARAMETERS = array( array( - 'channel' => array( - 'name' => 'Channel', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'criticalrole', - 'title' => 'Lowercase channel name as seen in channel URL' - ), - 'type' => array( - 'name' => 'Type', - 'type' => 'list', - 'values' => array( - 'All' => 'all', - 'Archive' => 'archive', - 'Highlights' => 'highlight', - 'Uploads' => 'upload', - 'Past Premieres' => 'past_premiere', - 'Premiere Uploads' => 'premiere_upload' - ), - 'defaultValue' => 'archive' - ) - )); +class TwitchBridge extends BridgeAbstract +{ + const MAINTAINER = 'Roliga'; + const NAME = 'Twitch Bridge'; + const URI = 'https://twitch.tv/'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Twitch channel videos'; + const PARAMETERS = [ [ + 'channel' => [ + 'name' => 'Channel', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'criticalrole', + 'title' => 'Lowercase channel name as seen in channel URL' + ], + 'type' => [ + 'name' => 'Type', + 'type' => 'list', + 'values' => [ + 'All' => 'all', + 'Archive' => 'archive', + 'Highlights' => 'highlight', + 'Uploads' => 'upload', + 'Past Premieres' => 'past_premiere', + 'Premiere Uploads' => 'premiere_upload' + ], + 'defaultValue' => 'archive' + ] + ]]; - /* - * Official instructions for obtaining your own client ID can be found here: - * https://dev.twitch.tv/docs/v5/#getting-a-client-id - */ - const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; + /* + * Official instructions for obtaining your own client ID can be found here: + * https://dev.twitch.tv/docs/v5/#getting-a-client-id + */ + const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; - const API_ENDPOINT = 'https://gql.twitch.tv/gql'; - const BROADCAST_TYPES = array( - 'all' => array( - 'ARCHIVE', - 'HIGHLIGHT', - 'UPLOAD', - 'PAST_PREMIERE', - 'PREMIERE_UPLOAD' - ), - 'archive' => 'ARCHIVE', - 'highlight' => 'HIGHLIGHT', - 'upload' => 'UPLOAD', - 'past_premiere' => 'PAST_PREMIERE', - 'premiere_upload' => 'PREMIERE_UPLOAD' - ); + const API_ENDPOINT = 'https://gql.twitch.tv/gql'; + const BROADCAST_TYPES = [ + 'all' => [ + 'ARCHIVE', + 'HIGHLIGHT', + 'UPLOAD', + 'PAST_PREMIERE', + 'PREMIERE_UPLOAD' + ], + 'archive' => 'ARCHIVE', + 'highlight' => 'HIGHLIGHT', + 'upload' => 'UPLOAD', + 'past_premiere' => 'PAST_PREMIERE', + 'premiere_upload' => 'PREMIERE_UPLOAD' + ]; - public function collectData(){ - $query = <<<'EOD' + public function collectData() + { + $query = <<<'EOD' query VODList($channel: String!, $types: [BroadcastType!]) { user(login: $channel) { displayName @@ -89,176 +91,189 @@ query VODList($channel: String!, $types: [BroadcastType!]) { } } EOD; - $variables = array( - 'channel' => $this->getInput('channel'), - 'types' => self::BROADCAST_TYPES[$this->getInput('type')] - ); - $data = $this->apiRequest($query, $variables); + $variables = [ + 'channel' => $this->getInput('channel'), + 'types' => self::BROADCAST_TYPES[$this->getInput('type')] + ]; + $data = $this->apiRequest($query, $variables); - $user = $data->user; - foreach($user->videos->edges as $edge) { - $video = $edge->node; + $user = $data->user; + foreach ($user->videos->edges as $edge) { + $video = $edge->node; - $url = 'https://www.twitch.tv/videos/' . $video->id; + $url = 'https://www.twitch.tv/videos/' . $video->id; - $item = array( - 'uri' => $url, - 'title' => $video->title, - 'timestamp' => $video->publishedAt, - 'author' => $user->displayName, - ); + $item = [ + 'uri' => $url, + 'title' => $video->title, + 'timestamp' => $video->publishedAt, + 'author' => $user->displayName, + ]; - // Add categories for tags and played game - $item['categories'] = $video->tags; - if(!is_null($video->game)) - $item['categories'][] = $video->game->displayName; - foreach($video->contentTags as $tag) - if(!$tag->isLanguageTag) - $item['categories'][] = $tag->localizedName; + // Add categories for tags and played game + $item['categories'] = $video->tags; + if (!is_null($video->game)) { + $item['categories'][] = $video->game->displayName; + } + foreach ($video->contentTags as $tag) { + if (!$tag->isLanguageTag) { + $item['categories'][] = $tag->localizedName; + } + } - // Add enclosures for thumbnails from a few points in the video - // Thumbnail list has duplicate entries sometimes so remove those - $item['enclosures'] = array_unique($video->thumbnailURLs); + // Add enclosures for thumbnails from a few points in the video + // Thumbnail list has duplicate entries sometimes so remove those + $item['enclosures'] = array_unique($video->thumbnailURLs); - /* - * Content format example: - * - * [Preview Image] - * - * Some optional video description. - * - * Duration: 1:23:45 - * Views: 123 - * - * Played games: - * * 00:00:00 Game 1 - * * 00:12:34 Game 2 - * - */ - $item['content'] = '<p><a href="' - . $url - . '"><img src="' - . $video->previewThumbnailURL - . '" /></a></p><p>' - . $video->description // in markdown format - . '</p><p><b>Duration:</b> ' - . $this->formatTimestampTime($video->lengthSeconds) - . '<br/><b>Views:</b> ' - . $video->viewCount - . '</p>'; + /* + * Content format example: + * + * [Preview Image] + * + * Some optional video description. + * + * Duration: 1:23:45 + * Views: 123 + * + * Played games: + * * 00:00:00 Game 1 + * * 00:12:34 Game 2 + * + */ + $item['content'] = '<p><a href="' + . $url + . '"><img src="' + . $video->previewThumbnailURL + . '" /></a></p><p>' + . $video->description // in markdown format + . '</p><p><b>Duration:</b> ' + . $this->formatTimestampTime($video->lengthSeconds) + . '<br/><b>Views:</b> ' + . $video->viewCount + . '</p>'; - // Add played games list to content - $item['content'] .= '<p><b>Played games:</b><ul>'; - if(count($video->moments->edges) > 0) { - foreach($video->moments->edges as $edge) { - $moment = $edge->node; + // Add played games list to content + $item['content'] .= '<p><b>Played games:</b><ul>'; + if (count($video->moments->edges) > 0) { + foreach ($video->moments->edges as $edge) { + $moment = $edge->node; - $item['categories'][] = $moment->description; - $item['content'] .= '<li><a href="' - . $url - . '?t=' - . $this->formatQueryTime($moment->positionMilliseconds / 1000) - . '">' - . $this->formatTimestampTime($moment->positionMilliseconds / 1000) - . '</a> - ' - . $moment->description - . '</li>'; - } - } else { - $item['content'] .= '<li><a href="' - . $url - . '">00:00:00</a> - ' - . ($video->game ? $video->game->displayName : 'No Game') - . '</li>'; - } - $item['content'] .= '</ul></p>'; + $item['categories'][] = $moment->description; + $item['content'] .= '<li><a href="' + . $url + . '?t=' + . $this->formatQueryTime($moment->positionMilliseconds / 1000) + . '">' + . $this->formatTimestampTime($moment->positionMilliseconds / 1000) + . '</a> - ' + . $moment->description + . '</li>'; + } + } else { + $item['content'] .= '<li><a href="' + . $url + . '">00:00:00</a> - ' + . ($video->game ? $video->game->displayName : 'No Game') + . '</li>'; + } + $item['content'] .= '</ul></p>'; - $item['categories'] = array_unique($item['categories']); + $item['categories'] = array_unique($item['categories']); - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - // e.g. 01:53:27 - private function formatTimestampTime($seconds) { - return sprintf('%02d:%02d:%02d', - floor($seconds / 3600), - ($seconds / 60) % 60, - $seconds % 60); - } + // e.g. 01:53:27 + private function formatTimestampTime($seconds) + { + return sprintf( + '%02d:%02d:%02d', + floor($seconds / 3600), + ($seconds / 60) % 60, + $seconds % 60 + ); + } - // e.g. 01h53m27s - private function formatQueryTime($seconds) { - return sprintf('%02dh%02dm%02ds', - floor($seconds / 3600), - ($seconds / 60) % 60, - $seconds % 60); - } + // e.g. 01h53m27s + private function formatQueryTime($seconds) + { + return sprintf( + '%02dh%02dm%02ds', + floor($seconds / 3600), + ($seconds / 60) % 60, + $seconds % 60 + ); + } - // GraphQL: https://graphql.org/ - // Tool for developing/testing queries: https://github.com/skevy/graphiql-app - private function apiRequest($query, $variables) { - $request = array( - 'query' => $query, - 'variables' => $variables - ); - $header = array( - 'Client-ID: ' . self::CLIENT_ID - ); - $opts = array( - CURLOPT_CUSTOMREQUEST => 'POST', - CURLOPT_POSTFIELDS => json_encode($request) - ); + // GraphQL: https://graphql.org/ + // Tool for developing/testing queries: https://github.com/skevy/graphiql-app + private function apiRequest($query, $variables) + { + $request = [ + 'query' => $query, + 'variables' => $variables + ]; + $header = [ + 'Client-ID: ' . self::CLIENT_ID + ]; + $opts = [ + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => json_encode($request) + ]; - Debug::log("Sending GraphQL query:\n" . $query); - Debug::log("Sending GraphQL variables:\n" - . json_encode($variables, JSON_PRETTY_PRINT)); + Debug::log("Sending GraphQL query:\n" . $query); + Debug::log("Sending GraphQL variables:\n" + . json_encode($variables, JSON_PRETTY_PRINT)); - $response = json_decode(getContents(self::API_ENDPOINT, $header, $opts)); + $response = json_decode(getContents(self::API_ENDPOINT, $header, $opts)); - Debug::log("Got GraphQL response:\n" - . json_encode($response, JSON_PRETTY_PRINT)); + Debug::log("Got GraphQL response:\n" + . json_encode($response, JSON_PRETTY_PRINT)); - if(isset($response->errors)) { - $messages = array_column($response->errors, 'message'); - returnServerError('API error(s): ' . implode("\n", $messages)); - } + if (isset($response->errors)) { + $messages = array_column($response->errors, 'message'); + returnServerError('API error(s): ' . implode("\n", $messages)); + } - return $response->data; - } + return $response->data; + } - public function getName(){ - if(!is_null($this->getInput('channel'))) { - return $this->getInput('channel') . ' twitch videos'; - } + public function getName() + { + if (!is_null($this->getInput('channel'))) { + return $this->getInput('channel') . ' twitch videos'; + } - return parent::getName(); - } + return parent::getName(); + } - public function getURI(){ - if(!is_null($this->getInput('channel'))) { - return self::URI . $this->getInput('channel'); - } + public function getURI() + { + if (!is_null($this->getInput('channel'))) { + return self::URI . $this->getInput('channel'); + } - return parent::getURI(); - } + return parent::getURI(); + } - public function detectParameters($url){ - $params = array(); + public function detectParameters($url) + { + $params = []; - // Matches e.g. https://www.twitch.tv/someuser/videos?filter=archives - $regex = '/^(https?:\/\/)? + // Matches e.g. https://www.twitch.tv/someuser/videos?filter=archives + $regex = '/^(https?:\/\/)? (www\.)? twitch\.tv\/ ([^\/&?\n]+) \/videos\?.*filter= (all|archive|highlight|upload)/x'; - if(preg_match($regex, $url, $matches) > 0) { - $params['channel'] = urldecode($matches[3]); - $params['type'] = $matches[4]; - return $params; - } + if (preg_match($regex, $url, $matches) > 0) { + $params['channel'] = urldecode($matches[3]); + $params['type'] = $matches[4]; + return $params; + } - return null; - } + return null; + } } diff --git a/bridges/TwitterBridge.php b/bridges/TwitterBridge.php index 71ac52ba..743843da 100644 --- a/bridges/TwitterBridge.php +++ b/bridges/TwitterBridge.php @@ -1,37 +1,39 @@ <?php -class TwitterBridge extends BridgeAbstract { - const NAME = 'Twitter Bridge'; - const URI = 'https://twitter.com/'; - const API_URI = 'https://api.twitter.com'; - const GUEST_TOKEN_USES = 100; - const GUEST_TOKEN_EXPIRY = 10800; // 3hrs - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'returns tweets'; - const MAINTAINER = 'arnd-s'; - const PARAMETERS = array( - 'global' => array( - 'nopic' => array( - 'name' => 'Hide profile pictures', - 'type' => 'checkbox', - 'title' => 'Activate to hide profile pictures in content' - ), - 'noimg' => array( - 'name' => 'Hide images in tweets', - 'type' => 'checkbox', - 'title' => 'Activate to hide images in tweets' - ), - 'noimgscaling' => array( - 'name' => 'Disable image scaling', - 'type' => 'checkbox', - 'title' => 'Activate to disable image scaling in tweets (keeps original image)' - ) - ), - 'By keyword or hashtag' => array( - 'q' => array( - 'name' => 'Keyword or #hashtag', - 'required' => true, - 'exampleValue' => 'rss-bridge OR rssbridge', - 'title' => <<<EOD + +class TwitterBridge extends BridgeAbstract +{ + const NAME = 'Twitter Bridge'; + const URI = 'https://twitter.com/'; + const API_URI = 'https://api.twitter.com'; + const GUEST_TOKEN_USES = 100; + const GUEST_TOKEN_EXPIRY = 10800; // 3hrs + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'returns tweets'; + const MAINTAINER = 'arnd-s'; + const PARAMETERS = [ + 'global' => [ + 'nopic' => [ + 'name' => 'Hide profile pictures', + 'type' => 'checkbox', + 'title' => 'Activate to hide profile pictures in content' + ], + 'noimg' => [ + 'name' => 'Hide images in tweets', + 'type' => 'checkbox', + 'title' => 'Activate to hide images in tweets' + ], + 'noimgscaling' => [ + 'name' => 'Disable image scaling', + 'type' => 'checkbox', + 'title' => 'Activate to disable image scaling in tweets (keeps original image)' + ] + ], + 'By keyword or hashtag' => [ + 'q' => [ + 'name' => 'Keyword or #hashtag', + 'required' => true, + 'exampleValue' => 'rss-bridge OR rssbridge', + 'title' => <<<EOD * To search for multiple words (must contain all of these words), put a space between them. Example: `rss-bridge release`. @@ -56,328 +58,340 @@ Example: `#rss-bridge OR #rssbridge` Example: `#rss-bridge OR #rssbridge -release` EOD - ) - ), - 'By username' => array( - 'u' => array( - 'name' => 'username', - 'required' => true, - 'exampleValue' => 'sebsauvage', - 'title' => 'Insert a user name' - ), - 'norep' => array( - 'name' => 'Without replies', - 'type' => 'checkbox', - 'title' => 'Only return initial tweets' - ), - 'noretweet' => array( - 'name' => 'Without retweets', - 'required' => false, - 'type' => 'checkbox', - 'title' => 'Hide retweets' - ), - 'nopinned' => array( - 'name' => 'Without pinned tweet', - 'required' => false, - 'type' => 'checkbox', - 'title' => 'Hide pinned tweet' - ) - ), - 'By list' => array( - 'user' => array( - 'name' => 'User', - 'required' => true, - 'exampleValue' => 'Scobleizer', - 'title' => 'Insert a user name' - ), - 'list' => array( - 'name' => 'List', - 'required' => true, - 'exampleValue' => 'Tech-News', - 'title' => 'Insert the list name' - ), - 'filter' => array( - 'name' => 'Filter', - 'exampleValue' => '#rss-bridge', - 'required' => false, - 'title' => 'Specify term to search for' - ) - ), - 'By list ID' => array( - 'listid' => array( - 'name' => 'List ID', - 'exampleValue' => '31748', - 'required' => true, - 'title' => 'Insert the list id' - ), - 'filter' => array( - 'name' => 'Filter', - 'exampleValue' => '#rss-bridge', - 'required' => false, - 'title' => 'Specify term to search for' - ) - ) - ); - - private $apiKey = null; - private $guestToken = null; - private $authHeader = array(); - - public function detectParameters($url){ - $params = array(); - - // By keyword or hashtag (search) - $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/search.*(\?|&)q=([^\/&?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['q'] = urldecode($matches[4]); - return $params; - } - - // By hashtag - $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/hashtag\/([^\/?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['q'] = urldecode($matches[3]); - return $params; - } - - // By list - $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)\/lists\/([^\/?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['user'] = urldecode($matches[3]); - $params['list'] = urldecode($matches[4]); - return $params; - } - - // By username - $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)/'; - if(preg_match($regex, $url, $matches) > 0) { - $params['u'] = urldecode($matches[3]); - return $params; - } - - return null; - } - - public function getName(){ - switch($this->queriedContext) { - case 'By keyword or hashtag': - $specific = 'search '; - $param = 'q'; - break; - case 'By username': - $specific = '@'; - $param = 'u'; - break; - case 'By list': - return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user'); - case 'By list ID': - return 'Twitter List #' . $this->getInput('listid'); - default: return parent::getName(); - } - return 'Twitter ' . $specific . $this->getInput($param); - } - - public function getURI(){ - switch($this->queriedContext) { - case 'By keyword or hashtag': - return self::URI - . 'search?q=' - . urlencode($this->getInput('q')) - . '&f=tweets'; - case 'By username': - return self::URI - . urlencode($this->getInput('u')); - // Always return without replies! - // . ($this->getInput('norep') ? '' : '/with_replies'); - case 'By list': - return self::URI - . urlencode($this->getInput('user')) - . '/lists/' - . str_replace(' ', '-', strtolower($this->getInput('list'))); - case 'By list ID': - return self::URI - . 'i/lists/' - . urlencode($this->getInput('listid')); - default: return parent::getURI(); - } - } - - public function collectData(){ - // $data will contain an array of all found tweets (unfiltered) - $data = null; - // Contains user data (when in by username context) - $user = null; - // Array of all found tweets - $tweets = array(); - - // Get authentication information - $this->getApiKey(); - - // Try to get all tweets - switch($this->queriedContext) { - case 'By username': - $user = $this->makeApiCall('/1.1/users/show.json', array('screen_name' => $this->getInput('u'))); - if (!$user) { - returnServerError('Requested username can\'t be found.'); - } - - $params = array( - 'user_id' => $user->id_str, - 'tweet_mode' => 'extended' - ); - - $data = $this->makeApiCall('/1.1/statuses/user_timeline.json', $params); - break; - - case 'By keyword or hashtag': - $params = array( - 'q' => urlencode($this->getInput('q')), - 'tweet_mode' => 'extended', - 'tweet_search_mode' => 'live', - ); - - $data = $this->makeApiCall('/1.1/search/tweets.json', $params)->statuses; - break; - - case 'By list': - $params = array( - 'slug' => strtolower($this->getInput('list')), - 'owner_screen_name' => strtolower($this->getInput('user')), - 'tweet_mode' => 'extended', - ); - - $data = $this->makeApiCall('/1.1/lists/statuses.json', $params); - break; - - case 'By list ID': - $params = array( - 'list_id' => $this->getInput('listid'), - 'tweet_mode' => 'extended', - ); - - $data = $this->makeApiCall('/1.1/lists/statuses.json', $params); - break; - - default: - returnServerError('Invalid query context !'); - } - - if(!$data) { - switch($this->queriedContext) { - case 'By keyword or hashtag': - returnServerError('No results for this query.'); - // fall-through - case 'By username': - returnServerError('Requested username can\'t be found.'); - // fall-through - case 'By list': - returnServerError('Requested username or list can\'t be found'); - } - } - - // Filter out unwanted tweets - foreach ($data as $tweet) { - // Filter out retweets to remove possible duplicates of original tweet - switch($this->queriedContext) { - case 'By keyword or hashtag': - if (isset($tweet->retweeted_status) && substr($tweet->full_text, 0, 4) === 'RT @') { - continue 2; - } - break; - } - $tweets[] = $tweet; - } - - $hidePictures = $this->getInput('nopic'); - - $hidePinned = $this->getInput('nopinned'); - if ($hidePinned) { - $pinnedTweetId = null; - if ($user && $user->pinned_tweet_ids_str) { - $pinnedTweetId = $user->pinned_tweet_ids_str; - } - } - - foreach($tweets as $tweet) { - - // Skip own Retweets... - if (isset($tweet->retweeted_status) && $tweet->retweeted_status->user->id_str === $tweet->user->id_str) { - continue; - } - - // Skip pinned tweet - if ($hidePinned && $tweet->id_str === $pinnedTweetId) { - continue; - } - - switch($this->queriedContext) { - case 'By username': - if ($this->getInput('norep') && isset($tweet->in_reply_to_status_id)) - continue 2; - break; - } - - $item = array(); - - $realtweet = $tweet; - if (isset($tweet->retweeted_status)) { - // Tweet is a Retweet, so set author based on original tweet and set realtweet for reference to the right content - $realtweet = $tweet->retweeted_status; - } - - $item['username'] = $realtweet->user->screen_name; - $item['fullname'] = $realtweet->user->name; - $item['avatar'] = $realtweet->user->profile_image_url_https; - $item['timestamp'] = $realtweet->created_at; - $item['id'] = $realtweet->id_str; - $item['uri'] = self::URI . $item['username'] . '/status/' . $item['id']; - $item['author'] = (isset($tweet->retweeted_status) ? 'RT: ' : '' ) - . $item['fullname'] - . ' (@' - . $item['username'] . ')'; - - // Convert plain text URLs into HTML hyperlinks - $fulltext = $realtweet->full_text; - $cleanedTweet = $fulltext; - - $foundUrls = false; - - if (substr($cleanedTweet, 0, 4) === 'RT @') { - $cleanedTweet = substr($cleanedTweet, 3); - } - - if (isset($realtweet->entities->media)) { - foreach($realtweet->entities->media as $media) { - $cleanedTweet = str_replace($media->url, - '<a href="' . $media->expanded_url . '">' . $media->display_url . '</a>', - $cleanedTweet); - $foundUrls = true; - } - } - if (isset($realtweet->entities->urls)) { - foreach($realtweet->entities->urls as $url) { - $cleanedTweet = str_replace($url->url, - '<a href="' . $url->expanded_url . '">' . $url->display_url . '</a>', - $cleanedTweet); - $foundUrls = true; - } - } - if ($foundUrls === false) { - // fallback to regex'es - $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/'; - if(preg_match($reg_ex, $realtweet->full_text, $url)) { - $cleanedTweet = preg_replace($reg_ex, - "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ", - $cleanedTweet); - } - } - // generate the title - $item['title'] = strip_tags($cleanedTweet); - - // Add avatar - $picture_html = ''; - if(!$hidePictures) { - $picture_html = <<<EOD + ] + ], + 'By username' => [ + 'u' => [ + 'name' => 'username', + 'required' => true, + 'exampleValue' => 'sebsauvage', + 'title' => 'Insert a user name' + ], + 'norep' => [ + 'name' => 'Without replies', + 'type' => 'checkbox', + 'title' => 'Only return initial tweets' + ], + 'noretweet' => [ + 'name' => 'Without retweets', + 'required' => false, + 'type' => 'checkbox', + 'title' => 'Hide retweets' + ], + 'nopinned' => [ + 'name' => 'Without pinned tweet', + 'required' => false, + 'type' => 'checkbox', + 'title' => 'Hide pinned tweet' + ] + ], + 'By list' => [ + 'user' => [ + 'name' => 'User', + 'required' => true, + 'exampleValue' => 'Scobleizer', + 'title' => 'Insert a user name' + ], + 'list' => [ + 'name' => 'List', + 'required' => true, + 'exampleValue' => 'Tech-News', + 'title' => 'Insert the list name' + ], + 'filter' => [ + 'name' => 'Filter', + 'exampleValue' => '#rss-bridge', + 'required' => false, + 'title' => 'Specify term to search for' + ] + ], + 'By list ID' => [ + 'listid' => [ + 'name' => 'List ID', + 'exampleValue' => '31748', + 'required' => true, + 'title' => 'Insert the list id' + ], + 'filter' => [ + 'name' => 'Filter', + 'exampleValue' => '#rss-bridge', + 'required' => false, + 'title' => 'Specify term to search for' + ] + ] + ]; + + private $apiKey = null; + private $guestToken = null; + private $authHeader = []; + + public function detectParameters($url) + { + $params = []; + + // By keyword or hashtag (search) + $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/search.*(\?|&)q=([^\/&?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['q'] = urldecode($matches[4]); + return $params; + } + + // By hashtag + $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/hashtag\/([^\/?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['q'] = urldecode($matches[3]); + return $params; + } + + // By list + $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)\/lists\/([^\/?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['user'] = urldecode($matches[3]); + $params['list'] = urldecode($matches[4]); + return $params; + } + + // By username + $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)/'; + if (preg_match($regex, $url, $matches) > 0) { + $params['u'] = urldecode($matches[3]); + return $params; + } + + return null; + } + + public function getName() + { + switch ($this->queriedContext) { + case 'By keyword or hashtag': + $specific = 'search '; + $param = 'q'; + break; + case 'By username': + $specific = '@'; + $param = 'u'; + break; + case 'By list': + return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user'); + case 'By list ID': + return 'Twitter List #' . $this->getInput('listid'); + default: + return parent::getName(); + } + return 'Twitter ' . $specific . $this->getInput($param); + } + + public function getURI() + { + switch ($this->queriedContext) { + case 'By keyword or hashtag': + return self::URI + . 'search?q=' + . urlencode($this->getInput('q')) + . '&f=tweets'; + case 'By username': + return self::URI + . urlencode($this->getInput('u')); + // Always return without replies! + // . ($this->getInput('norep') ? '' : '/with_replies'); + case 'By list': + return self::URI + . urlencode($this->getInput('user')) + . '/lists/' + . str_replace(' ', '-', strtolower($this->getInput('list'))); + case 'By list ID': + return self::URI + . 'i/lists/' + . urlencode($this->getInput('listid')); + default: + return parent::getURI(); + } + } + + public function collectData() + { + // $data will contain an array of all found tweets (unfiltered) + $data = null; + // Contains user data (when in by username context) + $user = null; + // Array of all found tweets + $tweets = []; + + // Get authentication information + $this->getApiKey(); + + // Try to get all tweets + switch ($this->queriedContext) { + case 'By username': + $user = $this->makeApiCall('/1.1/users/show.json', ['screen_name' => $this->getInput('u')]); + if (!$user) { + returnServerError('Requested username can\'t be found.'); + } + + $params = [ + 'user_id' => $user->id_str, + 'tweet_mode' => 'extended' + ]; + + $data = $this->makeApiCall('/1.1/statuses/user_timeline.json', $params); + break; + + case 'By keyword or hashtag': + $params = [ + 'q' => urlencode($this->getInput('q')), + 'tweet_mode' => 'extended', + 'tweet_search_mode' => 'live', + ]; + + $data = $this->makeApiCall('/1.1/search/tweets.json', $params)->statuses; + break; + + case 'By list': + $params = [ + 'slug' => strtolower($this->getInput('list')), + 'owner_screen_name' => strtolower($this->getInput('user')), + 'tweet_mode' => 'extended', + ]; + + $data = $this->makeApiCall('/1.1/lists/statuses.json', $params); + break; + + case 'By list ID': + $params = [ + 'list_id' => $this->getInput('listid'), + 'tweet_mode' => 'extended', + ]; + + $data = $this->makeApiCall('/1.1/lists/statuses.json', $params); + break; + + default: + returnServerError('Invalid query context !'); + } + + if (!$data) { + switch ($this->queriedContext) { + case 'By keyword or hashtag': + returnServerError('No results for this query.'); + // fall-through + case 'By username': + returnServerError('Requested username can\'t be found.'); + // fall-through + case 'By list': + returnServerError('Requested username or list can\'t be found'); + } + } + + // Filter out unwanted tweets + foreach ($data as $tweet) { + // Filter out retweets to remove possible duplicates of original tweet + switch ($this->queriedContext) { + case 'By keyword or hashtag': + if (isset($tweet->retweeted_status) && substr($tweet->full_text, 0, 4) === 'RT @') { + continue 2; + } + break; + } + $tweets[] = $tweet; + } + + $hidePictures = $this->getInput('nopic'); + + $hidePinned = $this->getInput('nopinned'); + if ($hidePinned) { + $pinnedTweetId = null; + if ($user && $user->pinned_tweet_ids_str) { + $pinnedTweetId = $user->pinned_tweet_ids_str; + } + } + + foreach ($tweets as $tweet) { + // Skip own Retweets... + if (isset($tweet->retweeted_status) && $tweet->retweeted_status->user->id_str === $tweet->user->id_str) { + continue; + } + + // Skip pinned tweet + if ($hidePinned && $tweet->id_str === $pinnedTweetId) { + continue; + } + + switch ($this->queriedContext) { + case 'By username': + if ($this->getInput('norep') && isset($tweet->in_reply_to_status_id)) { + continue 2; + } + break; + } + + $item = []; + + $realtweet = $tweet; + if (isset($tweet->retweeted_status)) { + // Tweet is a Retweet, so set author based on original tweet and set realtweet for reference to the right content + $realtweet = $tweet->retweeted_status; + } + + $item['username'] = $realtweet->user->screen_name; + $item['fullname'] = $realtweet->user->name; + $item['avatar'] = $realtweet->user->profile_image_url_https; + $item['timestamp'] = $realtweet->created_at; + $item['id'] = $realtweet->id_str; + $item['uri'] = self::URI . $item['username'] . '/status/' . $item['id']; + $item['author'] = (isset($tweet->retweeted_status) ? 'RT: ' : '' ) + . $item['fullname'] + . ' (@' + . $item['username'] . ')'; + + // Convert plain text URLs into HTML hyperlinks + $fulltext = $realtweet->full_text; + $cleanedTweet = $fulltext; + + $foundUrls = false; + + if (substr($cleanedTweet, 0, 4) === 'RT @') { + $cleanedTweet = substr($cleanedTweet, 3); + } + + if (isset($realtweet->entities->media)) { + foreach ($realtweet->entities->media as $media) { + $cleanedTweet = str_replace( + $media->url, + '<a href="' . $media->expanded_url . '">' . $media->display_url . '</a>', + $cleanedTweet + ); + $foundUrls = true; + } + } + if (isset($realtweet->entities->urls)) { + foreach ($realtweet->entities->urls as $url) { + $cleanedTweet = str_replace( + $url->url, + '<a href="' . $url->expanded_url . '">' . $url->display_url . '</a>', + $cleanedTweet + ); + $foundUrls = true; + } + } + if ($foundUrls === false) { + // fallback to regex'es + $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/'; + if (preg_match($reg_ex, $realtweet->full_text, $url)) { + $cleanedTweet = preg_replace( + $reg_ex, + "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ", + $cleanedTweet + ); + } + } + // generate the title + $item['title'] = strip_tags($cleanedTweet); + + // Add avatar + $picture_html = ''; + if (!$hidePictures) { + $picture_html = <<<EOD <a href="https://twitter.com/{$item['username']}"> <img style="align:top; width:75px; border:1px solid black;" @@ -386,20 +400,20 @@ EOD title="{$item['fullname']}" /> </a> EOD; - } - - // Get images - $media_html = ''; - if(isset($realtweet->extended_entities->media) && !$this->getInput('noimg')) { - foreach($realtweet->extended_entities->media as $media) { - switch($media->type) { - case 'photo': - $image = $media->media_url_https . '?name=orig'; - $display_image = $media->media_url_https; - // add enclosures - $item['enclosures'][] = $image; - - $media_html .= <<<EOD + } + + // Get images + $media_html = ''; + if (isset($realtweet->extended_entities->media) && !$this->getInput('noimg')) { + foreach ($realtweet->extended_entities->media as $media) { + switch ($media->type) { + case 'photo': + $image = $media->media_url_https . '?name=orig'; + $display_image = $media->media_url_https; + // add enclosures + $item['enclosures'][] = $image; + + $media_html .= <<<EOD <a href="{$image}"> <img style="align:top; max-width:558px; border:1px solid black;" @@ -407,61 +421,61 @@ EOD; src="{$display_image}" /> </a> EOD; - break; - case 'video': - case 'animated_gif': - if(isset($media->video_info)) { - $link = $media->expanded_url; - $poster = $media->media_url_https; - $video = null; - $maxBitrate = -1; - foreach($media->video_info->variants as $variant) { - $bitRate = isset($variant->bitrate) ? $variant->bitrate : -100; - if ($bitRate > $maxBitrate) { - $maxBitrate = $bitRate; - $video = $variant->url; - } - } - if(!is_null($video)) { - // add enclosures - $item['enclosures'][] = $video; - $item['enclosures'][] = $poster; - - $media_html .= <<<EOD + break; + case 'video': + case 'animated_gif': + if (isset($media->video_info)) { + $link = $media->expanded_url; + $poster = $media->media_url_https; + $video = null; + $maxBitrate = -1; + foreach ($media->video_info->variants as $variant) { + $bitRate = isset($variant->bitrate) ? $variant->bitrate : -100; + if ($bitRate > $maxBitrate) { + $maxBitrate = $bitRate; + $video = $variant->url; + } + } + if (!is_null($video)) { + // add enclosures + $item['enclosures'][] = $video; + $item['enclosures'][] = $poster; + + $media_html .= <<<EOD <a href="{$link}">Video</a> <video style="align:top; max-width:558px; border:1px solid black;" referrerpolicy="no-referrer" src="{$video}" poster="{$poster}" /> EOD; - } - } - break; - default: - Debug::log('Missing support for media type: ' . $media->type); - } - } - } - - switch($this->queriedContext) { - case 'By list': - case 'By list ID': - // Check if filter applies to list (using raw content) - if($this->getInput('filter')) { - if(stripos($cleanedTweet, $this->getInput('filter')) === false) { - continue 2; // switch + for-loop! - } - } - break; - case 'By username': - if ($this->getInput('noretweet') && strtolower($item['username']) != strtolower($this->getInput('u'))) { - continue 2; // switch + for-loop! - } - break; - default: - } - - $item['content'] = <<<EOD + } + } + break; + default: + Debug::log('Missing support for media type: ' . $media->type); + } + } + } + + switch ($this->queriedContext) { + case 'By list': + case 'By list ID': + // Check if filter applies to list (using raw content) + if ($this->getInput('filter')) { + if (stripos($cleanedTweet, $this->getInput('filter')) === false) { + continue 2; // switch + for-loop! + } + } + break; + case 'By username': + if ($this->getInput('noretweet') && strtolower($item['username']) != strtolower($this->getInput('u'))) { + continue 2; // switch + for-loop! + } + break; + default: + } + + $item['content'] = <<<EOD <div style="display: inline-block; vertical-align: top;"> {$picture_html} </div> @@ -473,173 +487,178 @@ EOD; </div> EOD; - // put out - $this->items[] = $item; - } - - usort($this->items, array('TwitterBridge', 'compareTweetId')); - } - - private static function compareTweetId($tweet1, $tweet2) { - return (intval($tweet1['id']) < intval($tweet2['id']) ? 1 : -1); - } - - //The aim of this function is to get an API key and a guest token - //This function takes 2 requests, and therefore is cached - private function getApiKey($forceNew = 0) { - - $cacheFac = new CacheFactory(); - - $r_cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $r_cache->setScope(get_called_class()); - $r_cache->setKey(array('refresh')); - $data = $r_cache->loadData(); - - $refresh = null; - if($data === null) { - $refresh = time(); - $r_cache->saveData($refresh); - } else { - $refresh = $data; - } - - $cacheFac = new CacheFactory(); - - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope(get_called_class()); - $cache->setKey(array('api_key')); - $data = $cache->loadData(); - - $apiKey = null; - if($forceNew || $data === null || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) { - $twitterPage = getContents('https://twitter.com'); - - $jsLink = false; - $jsMainRegexArray = array( - '/(https:\/\/abs\.twimg\.com\/responsive-web\/web\/main\.[^\.]+\.js)/m', - '/(https:\/\/abs\.twimg\.com\/responsive-web\/web_legacy\/main\.[^\.]+\.js)/m', - '/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web\/main\.[^\.]+\.js)/m', - '/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web-legacy\/main\.[^\.]+\.js)/m', - ); - foreach ($jsMainRegexArray as $jsMainRegex) { - if (preg_match_all($jsMainRegex, $twitterPage, $jsMainMatches, PREG_SET_ORDER, 0)) { - $jsLink = $jsMainMatches[0][0]; - break; - } - } - if (!$jsLink) { - returnServerError('Could not locate main.js link'); - } - - $jsContent = getContents($jsLink); - $apiKeyRegex = '/([a-zA-Z0-9]{59}%[a-zA-Z0-9]{44})/m'; - preg_match_all($apiKeyRegex, $jsContent, $apiKeyMatches, PREG_SET_ORDER, 0); - $apiKey = $apiKeyMatches[0][0]; - $cache->saveData($apiKey); - } else { - $apiKey = $data; - } - - $cacheFac2 = new CacheFactory(); - - $gt_cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $gt_cache->setScope(get_called_class()); - $gt_cache->setKey(array('guest_token')); - $guestTokenUses = $gt_cache->loadData(); - - $guestToken = null; - if($forceNew || $guestTokenUses === null || !is_array($guestTokenUses) || count($guestTokenUses) != 2 - || $guestTokenUses[0] <= 0 || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) { - $guestToken = $this->getGuestToken($apiKey); - if ($guestToken === null) { - if($guestTokenUses === null) { - returnServerError('Could not parse guest token'); - } else { - $guestToken = $guestTokenUses[1]; - } - } else { - $gt_cache->saveData(array(self::GUEST_TOKEN_USES, $guestToken)); - $r_cache->saveData(time()); - } - } else { - $guestTokenUses[0] -= 1; - $gt_cache->saveData($guestTokenUses); - $guestToken = $guestTokenUses[1]; - } - - $this->apiKey = $apiKey; - $this->guestToken = $guestToken; - $this->authHeaders = array( - 'authorization: Bearer ' . $apiKey, - 'x-guest-token: ' . $guestToken, - ); - - return array($apiKey, $guestToken); - } - - // Get a guest token. This is different to an API key, - // and it seems to change more regularly than the API key. - private function getGuestToken($apiKey) { - $headers = array( - 'authorization: Bearer ' . $apiKey, - ); - $opts = array( - CURLOPT_POST => 1, - ); - - try { - $pageContent = getContents('https://api.twitter.com/1.1/guest/activate.json', $headers, $opts, true); - $guestToken = json_decode($pageContent['content'])->guest_token; - } catch (Exception $e) { - $guestToken = null; - } - return $guestToken; - } - - /** - * Tries to make an API call to twitter. - * @param $api string API entry point - * @param $params array additional URI parmaeters - * @return object json data - */ - private function makeApiCall($api, $params) { - $uri = self::API_URI . $api . '?' . http_build_query($params); - - $retries = 1; - $retry = 0; - do { - $retry = 0; - - try { - $result = getContents($uri, $this->authHeaders, array(), true); - } catch (HttpException $e) { - switch ($e->getCode()) { - case 401: - // fall-through - case 403: - if ($retries) { - $retries--; - $retry = 1; - $this->getApiKey(1); - continue 2; - } - // fall-through - default: - $code = $e->getCode(); - $data = $e->getMessage(); - returnServerError(<<<EOD + // put out + $this->items[] = $item; + } + + usort($this->items, ['TwitterBridge', 'compareTweetId']); + } + + private static function compareTweetId($tweet1, $tweet2) + { + return (intval($tweet1['id']) < intval($tweet2['id']) ? 1 : -1); + } + + //The aim of this function is to get an API key and a guest token + //This function takes 2 requests, and therefore is cached + private function getApiKey($forceNew = 0) + { + $cacheFac = new CacheFactory(); + + $r_cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $r_cache->setScope(get_called_class()); + $r_cache->setKey(['refresh']); + $data = $r_cache->loadData(); + + $refresh = null; + if ($data === null) { + $refresh = time(); + $r_cache->saveData($refresh); + } else { + $refresh = $data; + } + + $cacheFac = new CacheFactory(); + + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope(get_called_class()); + $cache->setKey(['api_key']); + $data = $cache->loadData(); + + $apiKey = null; + if ($forceNew || $data === null || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) { + $twitterPage = getContents('https://twitter.com'); + + $jsLink = false; + $jsMainRegexArray = [ + '/(https:\/\/abs\.twimg\.com\/responsive-web\/web\/main\.[^\.]+\.js)/m', + '/(https:\/\/abs\.twimg\.com\/responsive-web\/web_legacy\/main\.[^\.]+\.js)/m', + '/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web\/main\.[^\.]+\.js)/m', + '/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web-legacy\/main\.[^\.]+\.js)/m', + ]; + foreach ($jsMainRegexArray as $jsMainRegex) { + if (preg_match_all($jsMainRegex, $twitterPage, $jsMainMatches, PREG_SET_ORDER, 0)) { + $jsLink = $jsMainMatches[0][0]; + break; + } + } + if (!$jsLink) { + returnServerError('Could not locate main.js link'); + } + + $jsContent = getContents($jsLink); + $apiKeyRegex = '/([a-zA-Z0-9]{59}%[a-zA-Z0-9]{44})/m'; + preg_match_all($apiKeyRegex, $jsContent, $apiKeyMatches, PREG_SET_ORDER, 0); + $apiKey = $apiKeyMatches[0][0]; + $cache->saveData($apiKey); + } else { + $apiKey = $data; + } + + $cacheFac2 = new CacheFactory(); + + $gt_cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $gt_cache->setScope(get_called_class()); + $gt_cache->setKey(['guest_token']); + $guestTokenUses = $gt_cache->loadData(); + + $guestToken = null; + if ( + $forceNew || $guestTokenUses === null || !is_array($guestTokenUses) || count($guestTokenUses) != 2 + || $guestTokenUses[0] <= 0 || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY + ) { + $guestToken = $this->getGuestToken($apiKey); + if ($guestToken === null) { + if ($guestTokenUses === null) { + returnServerError('Could not parse guest token'); + } else { + $guestToken = $guestTokenUses[1]; + } + } else { + $gt_cache->saveData([self::GUEST_TOKEN_USES, $guestToken]); + $r_cache->saveData(time()); + } + } else { + $guestTokenUses[0] -= 1; + $gt_cache->saveData($guestTokenUses); + $guestToken = $guestTokenUses[1]; + } + + $this->apiKey = $apiKey; + $this->guestToken = $guestToken; + $this->authHeaders = [ + 'authorization: Bearer ' . $apiKey, + 'x-guest-token: ' . $guestToken, + ]; + + return [$apiKey, $guestToken]; + } + + // Get a guest token. This is different to an API key, + // and it seems to change more regularly than the API key. + private function getGuestToken($apiKey) + { + $headers = [ + 'authorization: Bearer ' . $apiKey, + ]; + $opts = [ + CURLOPT_POST => 1, + ]; + + try { + $pageContent = getContents('https://api.twitter.com/1.1/guest/activate.json', $headers, $opts, true); + $guestToken = json_decode($pageContent['content'])->guest_token; + } catch (Exception $e) { + $guestToken = null; + } + return $guestToken; + } + + /** + * Tries to make an API call to twitter. + * @param $api string API entry point + * @param $params array additional URI parmaeters + * @return object json data + */ + private function makeApiCall($api, $params) + { + $uri = self::API_URI . $api . '?' . http_build_query($params); + + $retries = 1; + $retry = 0; + do { + $retry = 0; + + try { + $result = getContents($uri, $this->authHeaders, [], true); + } catch (HttpException $e) { + switch ($e->getCode()) { + case 401: + // fall-through + case 403: + if ($retries) { + $retries--; + $retry = 1; + $this->getApiKey(1); + continue 2; + } + // fall-through + default: + $code = $e->getCode(); + $data = $e->getMessage(); + returnServerError(<<<EOD Failed to make api call: $api HTTP Status: $code Errormessage: $data EOD - ); - break; - } - } - } while ($retry); + ); + break; + } + } + } while ($retry); - $data = json_decode($result['content']); + $data = json_decode($result['content']); - return $data; - } + return $data; + } } diff --git a/bridges/TwitterEngineeringBridge.php b/bridges/TwitterEngineeringBridge.php index f11caaa1..7c450013 100644 --- a/bridges/TwitterEngineeringBridge.php +++ b/bridges/TwitterEngineeringBridge.php @@ -1,62 +1,67 @@ <?php -class TwitterEngineeringBridge extends FeedExpander { - - const MAINTAINER = 'corenting'; - const NAME = 'Twitter Engineering Blog'; - const URI = 'https://blog.twitter.com/engineering/'; - const DESCRIPTION = 'Returns the newest articles.'; - const CACHE_TIMEOUT = 21600; // 6h - - protected function parseItem($item){ - $item = parent::parseItem($item); - - $article_html = getSimpleHTMLDOMCached($item['uri']); - if(!$article_html) { - $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; - return $item; - } - $article_html = defaultLinkTo($article_html, $this->getURI()); - - $article_body = $article_html->find('div.column.column-6', 0); - - // Remove elements that are not part of article content - $unwanted_selector = 'div.bl02-blog-post-text-masthead, div.tweet-error-text, div.bl13-tweet-template'; - foreach($article_body->find($unwanted_selector) as $found) { - $found->outertext = ''; - } - - // Set src for images - foreach($article_body->find('img') as $found) { - $found->setAttribute('src', $found->getAttribute('data-src')); - } - - $item['content'] = $article_body; - $item['timestamp'] = strtotime($article_html->find('span.b02-blog-post-no-masthead__date', 0)->innertext); - $item['categories'] = self::getCategoriesFromTags($article_html); - - return $item; - } - - private function getCategoriesFromTags($article_html){ - $tags_list_items = array($article_html->find('.post__tags > ul > li')); - $categories = array(); - - foreach($tags_list_items as $tag_list_item) { - foreach($tag_list_item as $tag) { - $categories[] = trim($tag->plaintext); - } - } - - return $categories; - } - - public function collectData(){ - $feed = static::URI . 'en_us/blog.rss'; - $this->collectExpandableDatas($feed); - } - - public function getName(){ - // Else the original feed returns "English (US)" as the title - return 'Twitter Engineering Blog'; - } + +class TwitterEngineeringBridge extends FeedExpander +{ + const MAINTAINER = 'corenting'; + const NAME = 'Twitter Engineering Blog'; + const URI = 'https://blog.twitter.com/engineering/'; + const DESCRIPTION = 'Returns the newest articles.'; + const CACHE_TIMEOUT = 21600; // 6h + + protected function parseItem($item) + { + $item = parent::parseItem($item); + + $article_html = getSimpleHTMLDOMCached($item['uri']); + if (!$article_html) { + $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; + return $item; + } + $article_html = defaultLinkTo($article_html, $this->getURI()); + + $article_body = $article_html->find('div.column.column-6', 0); + + // Remove elements that are not part of article content + $unwanted_selector = 'div.bl02-blog-post-text-masthead, div.tweet-error-text, div.bl13-tweet-template'; + foreach ($article_body->find($unwanted_selector) as $found) { + $found->outertext = ''; + } + + // Set src for images + foreach ($article_body->find('img') as $found) { + $found->setAttribute('src', $found->getAttribute('data-src')); + } + + $item['content'] = $article_body; + $item['timestamp'] = strtotime($article_html->find('span.b02-blog-post-no-masthead__date', 0)->innertext); + $item['categories'] = self::getCategoriesFromTags($article_html); + + return $item; + } + + private function getCategoriesFromTags($article_html) + { + $tags_list_items = [$article_html->find('.post__tags > ul > li')]; + $categories = []; + + foreach ($tags_list_items as $tag_list_item) { + foreach ($tag_list_item as $tag) { + $categories[] = trim($tag->plaintext); + } + } + + return $categories; + } + + public function collectData() + { + $feed = static::URI . 'en_us/blog.rss'; + $this->collectExpandableDatas($feed); + } + + public function getName() + { + // Else the original feed returns "English (US)" as the title + return 'Twitter Engineering Blog'; + } } diff --git a/bridges/TwitterV2Bridge.php b/bridges/TwitterV2Bridge.php index 1636139d..cad88598 100644 --- a/bridges/TwitterV2Bridge.php +++ b/bridges/TwitterV2Bridge.php @@ -1,93 +1,95 @@ <?php + /** * TwitterV2Bridge leverages Twitter API v2, and requires * a unique API Bearer Token, which requires creation of * a Twitter Dev account. Link to instructions in DESCRIPTION. */ -class TwitterV2Bridge extends BridgeAbstract { - const NAME = 'Twitter V2 Bridge'; - const URI = 'https://twitter.com/'; - const API_URI = 'https://api.twitter.com/2'; - const DESCRIPTION = 'Returns tweets (using Twitter API v2). See the +class TwitterV2Bridge extends BridgeAbstract +{ + const NAME = 'Twitter V2 Bridge'; + const URI = 'https://twitter.com/'; + const API_URI = 'https://api.twitter.com/2'; + const DESCRIPTION = 'Returns tweets (using Twitter API v2). See the <a href="https://rss-bridge.github.io/rss-bridge/Bridge_Specific/TwitterV2.html"> Configuration Instructions</a>.'; - const MAINTAINER = 'quickwick'; - const CONFIGURATION = array( - 'twitterv2apitoken' => array( - 'required' => true, - ) - ); - const PARAMETERS = array( - 'global' => array( - 'filter' => array( - 'name' => 'Filter', - 'exampleValue' => 'rss-bridge', - 'required' => false, - 'title' => 'Specify a single term to search for' - ), - 'norep' => array( - 'name' => 'Without replies', - 'type' => 'checkbox', - 'title' => 'Activate to exclude reply tweets' - ), - 'noretweet' => array( - 'name' => 'Without retweets', - 'required' => false, - 'type' => 'checkbox', - 'title' => 'Activate to exclude retweets' - ), - 'nopinned' => array( - 'name' => 'Without pinned tweet', - 'required' => false, - 'type' => 'checkbox', - 'title' => 'Activate to exclude pinned tweets' - ), - 'maxresults' => array( - 'name' => 'Maximum results', - 'required' => false, - 'exampleValue' => '20', - 'title' => 'Maximum number of tweets to retrieve (limit is 100)' - ), - 'imgonly' => array( - 'name' => 'Only media tweets', - 'type' => 'checkbox', - 'title' => 'Activate to show only tweets with media (photo/video)' - ), - 'nopic' => array( - 'name' => 'Hide profile pictures', - 'type' => 'checkbox', - 'title' => 'Activate to hide profile pictures in content' - ), - 'noimg' => array( - 'name' => 'Hide images in tweets', - 'type' => 'checkbox', - 'title' => 'Activate to hide images in tweets' - ), - 'noimgscaling' => array( - 'name' => 'Disable image scaling', - 'type' => 'checkbox', - 'title' => 'Activate to display original sized images (no thumbnails)' - ), - 'idastitle' => array( - 'name' => 'Use tweet id as title', - 'type' => 'checkbox', - 'title' => 'Activate to use tweet id as title (instead of tweet text)' - ) - ), - 'By username' => array( - 'u' => array( - 'name' => 'username', - 'required' => true, - 'exampleValue' => 'sebsauvage', - 'title' => 'Insert a user name' - ) - ), - 'By keyword or hashtag' => array( - 'query' => array( - 'name' => 'Keyword or #hashtag', - 'required' => true, - 'exampleValue' => 'rss-bridge OR #rss-bridge', - 'title' => <<<EOD + const MAINTAINER = 'quickwick'; + const CONFIGURATION = [ + 'twitterv2apitoken' => [ + 'required' => true, + ] + ]; + const PARAMETERS = [ + 'global' => [ + 'filter' => [ + 'name' => 'Filter', + 'exampleValue' => 'rss-bridge', + 'required' => false, + 'title' => 'Specify a single term to search for' + ], + 'norep' => [ + 'name' => 'Without replies', + 'type' => 'checkbox', + 'title' => 'Activate to exclude reply tweets' + ], + 'noretweet' => [ + 'name' => 'Without retweets', + 'required' => false, + 'type' => 'checkbox', + 'title' => 'Activate to exclude retweets' + ], + 'nopinned' => [ + 'name' => 'Without pinned tweet', + 'required' => false, + 'type' => 'checkbox', + 'title' => 'Activate to exclude pinned tweets' + ], + 'maxresults' => [ + 'name' => 'Maximum results', + 'required' => false, + 'exampleValue' => '20', + 'title' => 'Maximum number of tweets to retrieve (limit is 100)' + ], + 'imgonly' => [ + 'name' => 'Only media tweets', + 'type' => 'checkbox', + 'title' => 'Activate to show only tweets with media (photo/video)' + ], + 'nopic' => [ + 'name' => 'Hide profile pictures', + 'type' => 'checkbox', + 'title' => 'Activate to hide profile pictures in content' + ], + 'noimg' => [ + 'name' => 'Hide images in tweets', + 'type' => 'checkbox', + 'title' => 'Activate to hide images in tweets' + ], + 'noimgscaling' => [ + 'name' => 'Disable image scaling', + 'type' => 'checkbox', + 'title' => 'Activate to display original sized images (no thumbnails)' + ], + 'idastitle' => [ + 'name' => 'Use tweet id as title', + 'type' => 'checkbox', + 'title' => 'Activate to use tweet id as title (instead of tweet text)' + ] + ], + 'By username' => [ + 'u' => [ + 'name' => 'username', + 'required' => true, + 'exampleValue' => 'sebsauvage', + 'title' => 'Insert a user name' + ] + ], + 'By keyword or hashtag' => [ + 'query' => [ + 'name' => 'Keyword or #hashtag', + 'required' => true, + 'exampleValue' => 'rss-bridge OR #rss-bridge', + 'title' => <<<EOD * To search for multiple words (must contain all of these words), put a space between them. Example: `rss-bridge release`. @@ -112,353 +114,362 @@ Example: `#rss-bridge OR #rssbridge` Example: `#rss-bridge OR #rssbridge -release` EOD - ) - ), - 'By list ID' => array( - 'listid' => array( - 'name' => 'List ID', - 'exampleValue' => '31748', - 'required' => true, - 'title' => 'Enter a list id' - ) - ) - ); - - // $Item variable needs to be accessible from multiple functions without passing - private $item = array(); - - public function getName() { - switch($this->queriedContext) { - case 'By keyword or hashtag': - $specific = 'search '; - $param = 'query'; - break; - case 'By username': - $specific = '@'; - $param = 'u'; - break; - case 'By list ID': - return 'Twitter List #' . $this->getInput('listid'); - default: - return parent::getName(); - } - return 'Twitter ' . $specific . $this->getInput($param); - } - - public function collectData() { - // $data will contain an array of all found tweets - $data = null; - // Contains user data (when in by username context) - $user = null; - // Array of all found tweets - $tweets = array(); - - $hideProfilePic = $this->getInput('nopic'); - $hideImages = $this->getInput('noimg'); - $hideReplies = $this->getInput('norep'); - $hideRetweets = $this->getInput('noretweet'); - $hidePinned = $this->getInput('nopinned'); - $tweetFilter = $this->getInput('filter'); - $maxResults = $this->getInput('maxresults'); - if ($maxResults > 100) { - $maxResults = 100; - } - $idAsTitle = $this->getInput('idastitle'); - $onlyMediaTweets = $this->getInput('imgonly'); - - // Read API token from config.ini.php, put into Header - $apiToken = $this->getOption('twitterv2apitoken'); - $authHeaders = array( - 'authorization: Bearer ' . $apiToken, - ); - - // Try to get all tweets - switch($this->queriedContext) { - case 'By username': - //Get id from username - $params = array( - 'user.fields' => 'pinned_tweet_id,profile_image_url' - ); - $user = $this->makeApiCall('/users/by/username/' - . $this->getInput('u'), $authHeaders, $params); - - if(isset($user->errors)) { - Debug::log('User JSON: ' . json_encode($user)); - returnServerError('Requested username can\'t be found.'); - } - - // Set default params - $params = array( - 'max_results' => (empty($maxResults) ? '10' : $maxResults ), - 'tweet.fields' - => 'created_at,referenced_tweets,entities,attachments', - 'user.fields' => 'pinned_tweet_id', - 'expansions' - => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys', - 'media.fields' => 'type,url,preview_image_url' - ); - - // Set params to filter out replies and/or retweets - if($hideReplies && $hideRetweets) { - $params['exclude'] = 'replies,retweets'; - } elseif($hideReplies) { - $params['exclude'] = 'replies'; - } elseif($hideRetweets) { - $params['exclude'] = 'retweets'; - } - - // Get the tweets - $data = $this->makeApiCall('/users/' . $user->data->id - . '/tweets', $authHeaders, $params); - break; - - case 'By keyword or hashtag': - $params = array( - 'query' => $this->getInput('query'), - 'max_results' => (empty($maxResults) ? '10' : $maxResults ), - 'tweet.fields' - => 'created_at,referenced_tweets,entities,attachments', - 'expansions' - => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys', - 'media.fields' => 'type,url,preview_image_url' - ); - - // Set params to filter out replies and/or retweets - if($hideReplies) { - $params['query'] = $params['query'] . ' -is:reply'; - } - if($hideRetweets) { - $params['query'] = $params['query'] . ' -is:retweet'; - } - - $data = $this->makeApiCall('/tweets/search/recent', $authHeaders, $params); - break; - - case 'By list ID': - // Set default params - $params = array( - 'max_results' => (empty($maxResults) ? '10' : $maxResults ), - 'tweet.fields' - => 'created_at,referenced_tweets,entities,attachments', - 'expansions' - => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys', - 'media.fields' => 'type,url,preview_image_url' - ); - - $data = $this->makeApiCall('/lists/' . $this->getInput('listid') . - '/tweets', $authHeaders, $params); - break; - - default: - returnServerError('Invalid query context !'); - } - - if((isset($data->errors) && !isset($data->data)) || - (isset($data->meta) && $data->meta->result_count === 0)) { - Debug::log('Data JSON: ' . json_encode($data)); - switch($this->queriedContext) { - case 'By keyword or hashtag': - returnServerError('No results for this query.'); - // fall-through - case 'By username': - returnServerError('Requested username cannnot be found.'); - // fall-through - case 'By list ID': - returnServerError('Requested list cannnot be found'); - // fall-through - } - } - - // figure out the Pinned Tweet Id - if($hidePinned) { - $pinnedTweetId = null; - if(isset($user) && isset($user->data->pinned_tweet_id)) { - $pinnedTweetId = $user->data->pinned_tweet_id; - } - } - - // Extract Media data into array - isset($data->includes->media) ? $includesMedia = $data->includes->media : $includesMedia = null; - - // Extract additional Users data into array - isset($data->includes->users) ? $includesUsers = $data->includes->users : $includesUsers = null; - - // Extract additional Tweets data into array - isset($data->includes->tweets) ? $includesTweets = $data->includes->tweets : $includesTweets = null; - - // Extract main Tweets data into array - $tweets = $data->data; - - // Make another API call to get user and media info for retweets - // Is there some way to get this info included in original API call? - $retweetedData = null; - $retweetedMedia = null; - $retweetedUsers = null; - if(!$hideImages && isset($includesTweets)) { - // There has to be a better PHP way to extract the tweet Ids? - $includesTweetsIds = array(); - foreach($includesTweets as $includesTweet) { - $includesTweetsIds[] = $includesTweet->id; - } - Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds)); - - // Set default params for API query - $params = array( - 'ids' => join(',', $includesTweetsIds), - 'tweet.fields' => 'entities,attachments', - 'expansions' => 'author_id,attachments.media_keys', - 'media.fields' => 'type,url,preview_image_url', - 'user.fields' => 'id,profile_image_url' - ); - - // Get the retweeted tweets - $retweetedData = $this->makeApiCall('/tweets', $authHeaders, $params); - - // Extract retweets Media data into array - isset($retweetedData->includes->media) ? $retweetedMedia - = $retweetedData->includes->media : $retweetedMedia = null; - - // Extract retweets additional Users data into array - isset($retweetedData->includes->users) ? $retweetedUsers - = $retweetedData->includes->users : $retweetedUsers = null; - } - - // Create output array with all required elements for each tweet - foreach($tweets as $tweet) { - //Debug::log('Tweet JSON: ' . json_encode($tweet)); - - // Skip pinned tweet (if selected) - if($hidePinned && $tweet->id === $pinnedTweetId) { - continue; - } - - // Check if tweet is Retweet, Quote or Reply - $isRetweet = false; - $isReply = false; - $isQuote = false; - - if(isset($tweet->referenced_tweets)) { - switch($tweet->referenced_tweets[0]->type) { - case 'retweeted': - $isRetweet = true; break; - case 'quoted': - $isQuote = true; break; - case 'replied_to': - $isReply = true; break; - } - } - - // Skip replies and/or retweets (if selected). This check is primarily for lists - // These should already be pre-filtered for username and keyword queries - if (($hideRetweets && $isRetweet) || ($hideReplies && $isReply)) { - continue; - } - - $cleanedTweet = nl2br($tweet->text); - //Debug::log('cleanedTweet: ' . $cleanedTweet); - - // Perform optional keyword filtering (only keep tweet if keyword is found) - if (! empty($tweetFilter)) { - if(stripos($cleanedTweet, $this->getInput('filter')) === false) { - continue; - } - } - - // Initialize empty array to hold feed item values - $this->item = array(); - - // Start getting and setting values needed for HTML output - $quotedTweet = null; - $cleanedQuotedTweet = null; - $quotedUser = null; - if ($isQuote) { - Debug::log('Tweet is quote'); - foreach($includesTweets as $includesTweet) { - if($includesTweet->id === $tweet->referenced_tweets[0]->id) { - $quotedTweet = $includesTweet; - $cleanedQuotedTweet = nl2br($quotedTweet->text); - //Debug::log('Found quoted tweet'); - break; - } - } - - $quotedUser = $this->getTweetUser($quotedTweet, $retweetedUsers, $includesUsers); - } - if($isRetweet || is_null($user)) { - Debug::log('Tweet is retweet, or $user is null'); - // Replace tweet object with original retweeted object - if($isRetweet) { - foreach($includesTweets as $includesTweet) { - if($includesTweet->id === $tweet->referenced_tweets[0]->id) { - $tweet = $includesTweet; - break; - } - } - } - - // Skip self-Retweets (can cause duplicate entries in output) - if(isset($user) && $tweet->author_id === $user->data->id) { - continue; - } - - // Get user object for retweeted tweet - $originalUser = $this->getTweetUser($tweet, $retweetedUsers, $includesUsers); - - $this->item['username'] = $originalUser->username; - $this->item['fullname'] = $originalUser->name; - if(isset($originalUser->profile_image_url)) { - $this->item['avatar'] = $originalUser->profile_image_url; - } else{ - $this->item['avatar'] = null; - } - } else{ - $this->item['username'] = $user->data->username; - $this->item['fullname'] = $user->data->name; - $this->item['avatar'] = $user->data->profile_image_url; - } - $this->item['id'] = $tweet->id; - $this->item['timestamp'] = $tweet->created_at; - $this->item['uri'] - = self::URI . $this->item['username'] . '/status/' . $this->item['id']; - $this->item['author'] = ($isRetweet ? 'RT: ' : '' ) - . $this->item['fullname'] - . ' (@' - . $this->item['username'] . ')'; - - // (Optional) Skip non-media tweet - // This check must wait until after retweets are identified - if ($onlyMediaTweets && !isset($tweet->attachments->media_keys) && - (($isQuote && !isset($quotedTweet->attachments->media_keys)) || !$isQuote)) { - // There is no media in current tweet or quoted tweet, skip to next - continue; - } - - // Search for and replace URLs in Tweet text - $cleanedTweet = $this->replaceTweetURLs($tweet, $cleanedTweet); - if (isset($cleanedQuotedTweet)) { - Debug::log('Replacing URLs in Quoted Tweet text'); - $cleanedQuotedTweet = $this->replaceTweetURLs($quotedTweet, $cleanedQuotedTweet); - } - - // Generate Title text - if ($idAsTitle) { - $titleText = $tweet->id; - } else{ - $titleText = strip_tags($cleanedTweet); - } - - if($isRetweet && substr($titleText, 0, 4) === 'RT @') { - $titleText = substr_replace($titleText, ':', 2, 0 ); - } elseif ($isReply && !$idAsTitle) { - $titleText = 'R: ' . $titleText; - } - - $this->item['title'] = $titleText; - - // Generate Avatar HTML block - $picture_html = ''; - if(!$hideProfilePic && isset($this->item['avatar'])) { - $picture_html = <<<EOD + ] + ], + 'By list ID' => [ + 'listid' => [ + 'name' => 'List ID', + 'exampleValue' => '31748', + 'required' => true, + 'title' => 'Enter a list id' + ] + ] + ]; + + // $Item variable needs to be accessible from multiple functions without passing + private $item = []; + + public function getName() + { + switch ($this->queriedContext) { + case 'By keyword or hashtag': + $specific = 'search '; + $param = 'query'; + break; + case 'By username': + $specific = '@'; + $param = 'u'; + break; + case 'By list ID': + return 'Twitter List #' . $this->getInput('listid'); + default: + return parent::getName(); + } + return 'Twitter ' . $specific . $this->getInput($param); + } + + public function collectData() + { + // $data will contain an array of all found tweets + $data = null; + // Contains user data (when in by username context) + $user = null; + // Array of all found tweets + $tweets = []; + + $hideProfilePic = $this->getInput('nopic'); + $hideImages = $this->getInput('noimg'); + $hideReplies = $this->getInput('norep'); + $hideRetweets = $this->getInput('noretweet'); + $hidePinned = $this->getInput('nopinned'); + $tweetFilter = $this->getInput('filter'); + $maxResults = $this->getInput('maxresults'); + if ($maxResults > 100) { + $maxResults = 100; + } + $idAsTitle = $this->getInput('idastitle'); + $onlyMediaTweets = $this->getInput('imgonly'); + + // Read API token from config.ini.php, put into Header + $apiToken = $this->getOption('twitterv2apitoken'); + $authHeaders = [ + 'authorization: Bearer ' . $apiToken, + ]; + + // Try to get all tweets + switch ($this->queriedContext) { + case 'By username': + //Get id from username + $params = [ + 'user.fields' => 'pinned_tweet_id,profile_image_url' + ]; + $user = $this->makeApiCall('/users/by/username/' + . $this->getInput('u'), $authHeaders, $params); + + if (isset($user->errors)) { + Debug::log('User JSON: ' . json_encode($user)); + returnServerError('Requested username can\'t be found.'); + } + + // Set default params + $params = [ + 'max_results' => (empty($maxResults) ? '10' : $maxResults ), + 'tweet.fields' + => 'created_at,referenced_tweets,entities,attachments', + 'user.fields' => 'pinned_tweet_id', + 'expansions' + => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys', + 'media.fields' => 'type,url,preview_image_url' + ]; + + // Set params to filter out replies and/or retweets + if ($hideReplies && $hideRetweets) { + $params['exclude'] = 'replies,retweets'; + } elseif ($hideReplies) { + $params['exclude'] = 'replies'; + } elseif ($hideRetweets) { + $params['exclude'] = 'retweets'; + } + + // Get the tweets + $data = $this->makeApiCall('/users/' . $user->data->id + . '/tweets', $authHeaders, $params); + break; + + case 'By keyword or hashtag': + $params = [ + 'query' => $this->getInput('query'), + 'max_results' => (empty($maxResults) ? '10' : $maxResults ), + 'tweet.fields' + => 'created_at,referenced_tweets,entities,attachments', + 'expansions' + => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys', + 'media.fields' => 'type,url,preview_image_url' + ]; + + // Set params to filter out replies and/or retweets + if ($hideReplies) { + $params['query'] = $params['query'] . ' -is:reply'; + } + if ($hideRetweets) { + $params['query'] = $params['query'] . ' -is:retweet'; + } + + $data = $this->makeApiCall('/tweets/search/recent', $authHeaders, $params); + break; + + case 'By list ID': + // Set default params + $params = [ + 'max_results' => (empty($maxResults) ? '10' : $maxResults ), + 'tweet.fields' + => 'created_at,referenced_tweets,entities,attachments', + 'expansions' + => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys', + 'media.fields' => 'type,url,preview_image_url' + ]; + + $data = $this->makeApiCall('/lists/' . $this->getInput('listid') . + '/tweets', $authHeaders, $params); + break; + + default: + returnServerError('Invalid query context !'); + } + + if ( + (isset($data->errors) && !isset($data->data)) || + (isset($data->meta) && $data->meta->result_count === 0) + ) { + Debug::log('Data JSON: ' . json_encode($data)); + switch ($this->queriedContext) { + case 'By keyword or hashtag': + returnServerError('No results for this query.'); + // fall-through + case 'By username': + returnServerError('Requested username cannnot be found.'); + // fall-through + case 'By list ID': + returnServerError('Requested list cannnot be found'); + // fall-through + } + } + + // figure out the Pinned Tweet Id + if ($hidePinned) { + $pinnedTweetId = null; + if (isset($user) && isset($user->data->pinned_tweet_id)) { + $pinnedTweetId = $user->data->pinned_tweet_id; + } + } + + // Extract Media data into array + isset($data->includes->media) ? $includesMedia = $data->includes->media : $includesMedia = null; + + // Extract additional Users data into array + isset($data->includes->users) ? $includesUsers = $data->includes->users : $includesUsers = null; + + // Extract additional Tweets data into array + isset($data->includes->tweets) ? $includesTweets = $data->includes->tweets : $includesTweets = null; + + // Extract main Tweets data into array + $tweets = $data->data; + + // Make another API call to get user and media info for retweets + // Is there some way to get this info included in original API call? + $retweetedData = null; + $retweetedMedia = null; + $retweetedUsers = null; + if (!$hideImages && isset($includesTweets)) { + // There has to be a better PHP way to extract the tweet Ids? + $includesTweetsIds = []; + foreach ($includesTweets as $includesTweet) { + $includesTweetsIds[] = $includesTweet->id; + } + Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds)); + + // Set default params for API query + $params = [ + 'ids' => join(',', $includesTweetsIds), + 'tweet.fields' => 'entities,attachments', + 'expansions' => 'author_id,attachments.media_keys', + 'media.fields' => 'type,url,preview_image_url', + 'user.fields' => 'id,profile_image_url' + ]; + + // Get the retweeted tweets + $retweetedData = $this->makeApiCall('/tweets', $authHeaders, $params); + + // Extract retweets Media data into array + isset($retweetedData->includes->media) ? $retweetedMedia + = $retweetedData->includes->media : $retweetedMedia = null; + + // Extract retweets additional Users data into array + isset($retweetedData->includes->users) ? $retweetedUsers + = $retweetedData->includes->users : $retweetedUsers = null; + } + + // Create output array with all required elements for each tweet + foreach ($tweets as $tweet) { + //Debug::log('Tweet JSON: ' . json_encode($tweet)); + + // Skip pinned tweet (if selected) + if ($hidePinned && $tweet->id === $pinnedTweetId) { + continue; + } + + // Check if tweet is Retweet, Quote or Reply + $isRetweet = false; + $isReply = false; + $isQuote = false; + + if (isset($tweet->referenced_tweets)) { + switch ($tweet->referenced_tweets[0]->type) { + case 'retweeted': + $isRetweet = true; + break; + case 'quoted': + $isQuote = true; + break; + case 'replied_to': + $isReply = true; + break; + } + } + + // Skip replies and/or retweets (if selected). This check is primarily for lists + // These should already be pre-filtered for username and keyword queries + if (($hideRetweets && $isRetweet) || ($hideReplies && $isReply)) { + continue; + } + + $cleanedTweet = nl2br($tweet->text); + //Debug::log('cleanedTweet: ' . $cleanedTweet); + + // Perform optional keyword filtering (only keep tweet if keyword is found) + if (! empty($tweetFilter)) { + if (stripos($cleanedTweet, $this->getInput('filter')) === false) { + continue; + } + } + + // Initialize empty array to hold feed item values + $this->item = []; + + // Start getting and setting values needed for HTML output + $quotedTweet = null; + $cleanedQuotedTweet = null; + $quotedUser = null; + if ($isQuote) { + Debug::log('Tweet is quote'); + foreach ($includesTweets as $includesTweet) { + if ($includesTweet->id === $tweet->referenced_tweets[0]->id) { + $quotedTweet = $includesTweet; + $cleanedQuotedTweet = nl2br($quotedTweet->text); + //Debug::log('Found quoted tweet'); + break; + } + } + + $quotedUser = $this->getTweetUser($quotedTweet, $retweetedUsers, $includesUsers); + } + if ($isRetweet || is_null($user)) { + Debug::log('Tweet is retweet, or $user is null'); + // Replace tweet object with original retweeted object + if ($isRetweet) { + foreach ($includesTweets as $includesTweet) { + if ($includesTweet->id === $tweet->referenced_tweets[0]->id) { + $tweet = $includesTweet; + break; + } + } + } + + // Skip self-Retweets (can cause duplicate entries in output) + if (isset($user) && $tweet->author_id === $user->data->id) { + continue; + } + + // Get user object for retweeted tweet + $originalUser = $this->getTweetUser($tweet, $retweetedUsers, $includesUsers); + + $this->item['username'] = $originalUser->username; + $this->item['fullname'] = $originalUser->name; + if (isset($originalUser->profile_image_url)) { + $this->item['avatar'] = $originalUser->profile_image_url; + } else { + $this->item['avatar'] = null; + } + } else { + $this->item['username'] = $user->data->username; + $this->item['fullname'] = $user->data->name; + $this->item['avatar'] = $user->data->profile_image_url; + } + $this->item['id'] = $tweet->id; + $this->item['timestamp'] = $tweet->created_at; + $this->item['uri'] + = self::URI . $this->item['username'] . '/status/' . $this->item['id']; + $this->item['author'] = ($isRetweet ? 'RT: ' : '' ) + . $this->item['fullname'] + . ' (@' + . $this->item['username'] . ')'; + + // (Optional) Skip non-media tweet + // This check must wait until after retweets are identified + if ( + $onlyMediaTweets && !isset($tweet->attachments->media_keys) && + (($isQuote && !isset($quotedTweet->attachments->media_keys)) || !$isQuote) + ) { + // There is no media in current tweet or quoted tweet, skip to next + continue; + } + + // Search for and replace URLs in Tweet text + $cleanedTweet = $this->replaceTweetURLs($tweet, $cleanedTweet); + if (isset($cleanedQuotedTweet)) { + Debug::log('Replacing URLs in Quoted Tweet text'); + $cleanedQuotedTweet = $this->replaceTweetURLs($quotedTweet, $cleanedQuotedTweet); + } + + // Generate Title text + if ($idAsTitle) { + $titleText = $tweet->id; + } else { + $titleText = strip_tags($cleanedTweet); + } + + if ($isRetweet && substr($titleText, 0, 4) === 'RT @') { + $titleText = substr_replace($titleText, ':', 2, 0); + } elseif ($isReply && !$idAsTitle) { + $titleText = 'R: ' . $titleText; + } + + $this->item['title'] = $titleText; + + // Generate Avatar HTML block + $picture_html = ''; + if (!$hideProfilePic && isset($this->item['avatar'])) { + $picture_html = <<<EOD <a href="https://twitter.com/{$this->item['username']}"> <img style="margin-right: 10px; margin-bottom: 10px;" @@ -467,24 +478,24 @@ EOD title="{$this->item['fullname']}" /> </a> EOD; - } - - // Generate media HTML block - $media_html = ''; - $quoted_media_html = ''; - if(!$hideImages) { - if (isset($tweet->attachments->media_keys)) { - Debug::log('Generating HTML for tweet media'); - $media_html = $this->createTweetMediaHTML($tweet, $includesMedia, $retweetedMedia); - } - if (isset($quotedTweet->attachments->media_keys)) { - Debug::log('Generating HTML for quoted tweet media'); - $quoted_media_html = $this->createTweetMediaHTML($quotedTweet, $includesMedia, $retweetedMedia); - } - } - - // Generate the HTML for Item content - $this->item['content'] = <<<EOD + } + + // Generate media HTML block + $media_html = ''; + $quoted_media_html = ''; + if (!$hideImages) { + if (isset($tweet->attachments->media_keys)) { + Debug::log('Generating HTML for tweet media'); + $media_html = $this->createTweetMediaHTML($tweet, $includesMedia, $retweetedMedia); + } + if (isset($quotedTweet->attachments->media_keys)) { + Debug::log('Generating HTML for quoted tweet media'); + $quoted_media_html = $this->createTweetMediaHTML($quotedTweet, $includesMedia, $retweetedMedia); + } + } + + // Generate the HTML for Item content + $this->item['content'] = <<<EOD <div style="float: left;"> {$picture_html} </div> @@ -495,10 +506,10 @@ EOD; {$media_html} EOD; - // Add Quoted Tweet HTML, if relevant - if (isset($quotedTweet)) { - $quotedTweetURI = self::URI . $quotedUser->username . '/status/' . $quotedTweet->id; - $quote_html = <<<QUOTE + // Add Quoted Tweet HTML, if relevant + if (isset($quotedTweet)) { + $quotedTweetURI = self::URI . $quotedUser->username . '/status/' . $quotedTweet->id; + $quote_html = <<<QUOTE <div style="display: table; border-style: solid; border-width: 1px; border-radius: 5px; padding: 5px;"> <p><b>$quotedUser->name</b> @$quotedUser->username · @@ -507,183 +518,200 @@ EOD; $quoted_media_html </div> QUOTE; - $this->item['content'] .= $quote_html; - } - - $this->item['content'] = htmlspecialchars_decode($this->item['content'], ENT_QUOTES); - - // Add current Item to Items array - $this->items[] = $this->item; - } - - // Sort all tweets in array by date - usort($this->items, array('TwitterV2Bridge', 'compareTweetDate')); - } - - private static function compareTweetDate($tweet1, $tweet2) { - return (strtotime($tweet1['timestamp']) < strtotime($tweet2['timestamp']) ? 1 : -1); - } - - /** - * Tries to make an API call to Twitter. - * @param $api string API entry point - * @param $params array additional URI parmaeters - * @return object json data - */ - private function makeApiCall($api, $authHeaders, $params) { - $uri = self::API_URI . $api . '?' . http_build_query($params); - $result = getContents($uri, $authHeaders, array(), false); - $data = json_decode($result); - return $data; - } - - /** - * Change format of URLs in tweet text - * @param $tweetObject object current Tweet JSON - * @param $tweetText string current Tweet text - * @return string modified tweet text - */ - private function replaceTweetURLs($tweetObject, $tweetText) { - $foundUrls = false; - // Rewrite URL links, based on URL list in tweet object - if(isset($tweetObject->entities->urls)) { - foreach($tweetObject->entities->urls as $url) { - $tweetText = str_replace($url->url, - '<a href="' . $url->expanded_url - . '">' . $url->display_url . '</a>', - $tweetText); - } - $foundUrls = true; - } - // Regex fallback for rewriting URL links. Should never trigger? - if($foundUrls === false) { - $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/'; - if(preg_match($reg_ex, $tweetText, $url)) { - $tweetText = preg_replace($reg_ex, - "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ", - $tweetText); - } - } - // Fix back-to-back URLs by adding a <br> - $reg_ex = '/\/a>\s*<a/'; - $tweetText = preg_replace($reg_ex, '/a><br><a', $tweetText); - - return $tweetText; - } - - /** - * Find User object for Retweeted/Quoted tweet - * @param $tweetObject object current Tweet JSON - * @param $retweetedUsers - * @param $includesUsers - * @return object found User - */ - private function getTweetUser($tweetObject, $retweetedUsers, $includesUsers) { - $originalUser = new stdClass(); // make the linters stop complaining - if(isset($retweetedUsers)) { - Debug::log('Searching for tweet author_id in $retweetedUsers'); - foreach($retweetedUsers as $retweetedUser) { - if($retweetedUser->id === $tweetObject->author_id) { - $matchedUser = $retweetedUser; - Debug::log('Found author_id match in $retweetedUsers'); - break; - } - } - } - if(!isset($matchedUser->username) && isset($includesUsers)) { - Debug::log('Searching for tweet author_id in $includesUsers'); - foreach($includesUsers as $includesUser) { - if($includesUser->id === $tweetObject->author_id) { - $matchedUser = $includesUser; - Debug::log('Found author_id match in $includesUsers'); - break; - } - } - } - return $matchedUser; - } - - /** - * Generates HTML for embedded media - * @param $tweetObject object current Tweet JSON - * @param $includesMedia - * @param $retweetedMedia - * @return string modified tweet text - */ - private function createTweetMediaHTML($tweetObject, $includesMedia, $retweetedMedia){ - $media_html = ''; - // Match media_keys in tweet to media list from, put matches into new array - $tweetMedia = array(); - // Start by checking the original list of tweet Media includes - if(isset($includesMedia)) { - Debug::log('Searching for media_key in $includesMedia'); - foreach($includesMedia as $includesMedium) { - if(in_array ($includesMedium->media_key, - $tweetObject->attachments->media_keys)) { - Debug::log('Found media_key in $includesMedia'); - $tweetMedia[] = $includesMedium; - } - } - } - // If no matches found, check the retweet Media includes - if(empty($tweetMedia) && isset($retweetedMedia)) { - Debug::log('Searching for media_key in $retweetedMedia'); - foreach($retweetedMedia as $retweetedMedium) { - if(in_array ($retweetedMedium->media_key, - $tweetObject->attachments->media_keys)) { - Debug::log('Found media_key in $retweetedMedia'); - $tweetMedia[] = $retweetedMedium; - } - } - } - - foreach($tweetMedia as $media) { - switch($media->type) { - case 'photo': - if ($this->getInput('noimgscaling')) { - $image = $media->url; - $display_image = $media->url; - } else{ - $image = $media->url . '?name=orig'; - $display_image = $media->url; - } - // add enclosures - $this->item['enclosures'][] = $image; - - $media_html .= <<<EOD + $this->item['content'] .= $quote_html; + } + + $this->item['content'] = htmlspecialchars_decode($this->item['content'], ENT_QUOTES); + + // Add current Item to Items array + $this->items[] = $this->item; + } + + // Sort all tweets in array by date + usort($this->items, ['TwitterV2Bridge', 'compareTweetDate']); + } + + private static function compareTweetDate($tweet1, $tweet2) + { + return (strtotime($tweet1['timestamp']) < strtotime($tweet2['timestamp']) ? 1 : -1); + } + + /** + * Tries to make an API call to Twitter. + * @param $api string API entry point + * @param $params array additional URI parmaeters + * @return object json data + */ + private function makeApiCall($api, $authHeaders, $params) + { + $uri = self::API_URI . $api . '?' . http_build_query($params); + $result = getContents($uri, $authHeaders, [], false); + $data = json_decode($result); + return $data; + } + + /** + * Change format of URLs in tweet text + * @param $tweetObject object current Tweet JSON + * @param $tweetText string current Tweet text + * @return string modified tweet text + */ + private function replaceTweetURLs($tweetObject, $tweetText) + { + $foundUrls = false; + // Rewrite URL links, based on URL list in tweet object + if (isset($tweetObject->entities->urls)) { + foreach ($tweetObject->entities->urls as $url) { + $tweetText = str_replace( + $url->url, + '<a href="' . $url->expanded_url + . '">' . $url->display_url . '</a>', + $tweetText + ); + } + $foundUrls = true; + } + // Regex fallback for rewriting URL links. Should never trigger? + if ($foundUrls === false) { + $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/'; + if (preg_match($reg_ex, $tweetText, $url)) { + $tweetText = preg_replace( + $reg_ex, + "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ", + $tweetText + ); + } + } + // Fix back-to-back URLs by adding a <br> + $reg_ex = '/\/a>\s*<a/'; + $tweetText = preg_replace($reg_ex, '/a><br><a', $tweetText); + + return $tweetText; + } + + /** + * Find User object for Retweeted/Quoted tweet + * @param $tweetObject object current Tweet JSON + * @param $retweetedUsers + * @param $includesUsers + * @return object found User + */ + private function getTweetUser($tweetObject, $retweetedUsers, $includesUsers) + { + $originalUser = new stdClass(); // make the linters stop complaining + if (isset($retweetedUsers)) { + Debug::log('Searching for tweet author_id in $retweetedUsers'); + foreach ($retweetedUsers as $retweetedUser) { + if ($retweetedUser->id === $tweetObject->author_id) { + $matchedUser = $retweetedUser; + Debug::log('Found author_id match in $retweetedUsers'); + break; + } + } + } + if (!isset($matchedUser->username) && isset($includesUsers)) { + Debug::log('Searching for tweet author_id in $includesUsers'); + foreach ($includesUsers as $includesUser) { + if ($includesUser->id === $tweetObject->author_id) { + $matchedUser = $includesUser; + Debug::log('Found author_id match in $includesUsers'); + break; + } + } + } + return $matchedUser; + } + + /** + * Generates HTML for embedded media + * @param $tweetObject object current Tweet JSON + * @param $includesMedia + * @param $retweetedMedia + * @return string modified tweet text + */ + private function createTweetMediaHTML($tweetObject, $includesMedia, $retweetedMedia) + { + $media_html = ''; + // Match media_keys in tweet to media list from, put matches into new array + $tweetMedia = []; + // Start by checking the original list of tweet Media includes + if (isset($includesMedia)) { + Debug::log('Searching for media_key in $includesMedia'); + foreach ($includesMedia as $includesMedium) { + if ( + in_array( + $includesMedium->media_key, + $tweetObject->attachments->media_keys + ) + ) { + Debug::log('Found media_key in $includesMedia'); + $tweetMedia[] = $includesMedium; + } + } + } + // If no matches found, check the retweet Media includes + if (empty($tweetMedia) && isset($retweetedMedia)) { + Debug::log('Searching for media_key in $retweetedMedia'); + foreach ($retweetedMedia as $retweetedMedium) { + if ( + in_array( + $retweetedMedium->media_key, + $tweetObject->attachments->media_keys + ) + ) { + Debug::log('Found media_key in $retweetedMedia'); + $tweetMedia[] = $retweetedMedium; + } + } + } + + foreach ($tweetMedia as $media) { + switch ($media->type) { + case 'photo': + if ($this->getInput('noimgscaling')) { + $image = $media->url; + $display_image = $media->url; + } else { + $image = $media->url . '?name=orig'; + $display_image = $media->url; + } + // add enclosures + $this->item['enclosures'][] = $image; + + $media_html .= <<<EOD <a href="{$image}"> <img referrerpolicy="no-referrer" src="{$display_image}" /> </a> EOD; - break; - case 'video': - // To Do: Is there a way to easily match this - // to a direct Video URL? - $display_image = $media->preview_image_url; + break; + case 'video': + // To Do: Is there a way to easily match this + // to a direct Video URL? + $display_image = $media->preview_image_url; - $media_html .= <<<EOD + $media_html .= <<<EOD <p>Video:</p><a href="{$this->item['uri']}"> <img referrerpolicy="no-referrer" src="{$display_image}" /></a> EOD; - break; - case 'animated_gif': - // To Do: Is there a way to easily match this to a - // direct animated Gif URL? - $display_image = $media->preview_image_url; + break; + case 'animated_gif': + // To Do: Is there a way to easily match this to a + // direct animated Gif URL? + $display_image = $media->preview_image_url; - $media_html .= <<<EOD + $media_html .= <<<EOD <p>Animated Gif:</p><a href="{$this->item['uri']}"> <img referrerpolicy="no-referrer" src="{$display_image}" /></a> EOD; - break; - default: - Debug::log('Missing support for media type: ' - . $media->type); - } - } - - return $media_html; - } + break; + default: + Debug::log('Missing support for media type: ' + . $media->type); + } + } + + return $media_html; + } } diff --git a/bridges/UberNewsroomBridge.php b/bridges/UberNewsroomBridge.php index 560998cd..333200cd 100644 --- a/bridges/UberNewsroomBridge.php +++ b/bridges/UberNewsroomBridge.php @@ -1,179 +1,185 @@ <?php -class UberNewsroomBridge extends BridgeAbstract { - const NAME = 'Uber Newsroom Bridge'; - const URI = 'https://www.uber.com'; - const URI_API_DATA = 'https://newsroomapi.uber.com/wp-json/newsroom/v1/data?locale='; - const URI_API_POST = 'https://newsroomapi.uber.com/wp-json/wp/v2/posts/'; - const DESCRIPTION = 'Returns news posts'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(array( - 'region' => array( - 'name' => 'Region', - 'type' => 'list', - 'values' => array( - 'Africa' => array( - 'Egypt' => 'ar-EG', - 'Ghana' => 'en-GH', - 'Kenya' => 'en-KE', - 'Morocco' => 'fr-MA', - 'Nigeria' => 'en-NG', - 'South Africa' => 'en-ZA', - 'Tanzania' => 'en-TZ', - 'Uganda' => 'en-UG', - ), - 'Asia' => array( - 'Bangladesh' => 'en-BD', - 'Cambodia' => 'km-KH', - 'China' => 'zh-CN', - 'Hong Kong' => 'zh-HK', - 'India' => 'en-IN', - 'Indonesia' => 'en-ID', - 'Japan' => 'ja-JP', - 'Korea' => 'ko-KR', - 'Macau' => 'zh-MO', - 'Malaysia' => 'en-MY', - 'Myanmar' => 'en-MM', - 'Philippines' => 'en-PH', - 'Singapore' => 'en-SG', - 'Sri Lanka' => 'en-LK', - 'Taiwan' => 'zh-TW', - 'Thailand' => 'th-TH', - 'Vietnam' => 'vi-VN', - ), - 'Central America' => array( - 'Costa Rica' => 'es-CR', - 'Dominican Republic' => 'es-DO', - 'El Salvador' => 'es-SV', - 'Guatemala' => 'es-GT', - 'Honduras' => 'es-HN', - 'Mexico' => 'es-MX', - 'Nicaragua' => 'es-NI', - 'Panama' => 'es-PA', - 'Puerto Rico' => 'es-PR', - ), - 'Europe' => array( - 'Austria' => 'de-AT', - 'Azerbaijan' => 'az', - 'Belarus' => 'ru-BY', - 'Belgium' => 'fr-BE', - 'Bulgaria' => 'bg', - 'Croatia' => 'hr', - 'Czech Republic' => 'cs-CZ', - 'Denmark' => 'da-DK', - 'Estonia' => 'et-EE', - 'Finland' => 'fi', - 'France' => 'fr', - 'Germany' => 'de', - 'Greece' => 'el-GR', - 'Hungary' => 'hu', - 'Ireland' => 'en-IE', - 'Italy' => 'it', - 'Kazakhstan' => 'ru-KZ', - 'Lithuania' => 'lt', - 'Netherlands' => 'nl', - 'Norway' => 'nb-NO', - 'Poland' => 'pl', - 'Portugal' => 'pt', - 'Romania' => 'ro', - 'Russia' => 'ru', - 'Slovakia' => 'sk', - 'Spain' => 'es-ES', - 'Sweden' => 'sv-SE', - 'Switzerland' => 'fr-CH', - 'Turkey' => 'tr', - 'Ukraine' => 'uk-UA', - 'United Kingdom' => 'en-GB', - ), - 'Middle East' => array( - 'Bahrain' => 'en-BH', - 'Israel' => 'he-IL', - 'Jordan' => 'en-JO', - 'Kuwait' => 'en-KW', - 'Lebanon' => 'en-LB', - 'Pakistan' => 'en-PK', - 'Qatar' => 'en-QA', - 'Saudi Arabia' => 'ar-SA', - 'United Arab Emirates' => 'en-AE', - ), - 'North America' => array( - 'Canada' => 'en-CA', - 'United States' => 'en-US', - ), - 'Pacific' => array( - 'Australia' => 'en-AU', - 'New Zealand' => 'en-NZ', - ), - 'South America' => array( - 'Argentina' => 'es-AR', - 'Bolivia' => 'es-BO', - 'Brazil' => 'pt-BR', - 'Chile' => 'es-CL', - 'Colombia' => 'es-CO', - 'Ecuador' => 'es-EC', - 'Paraguay' => 'es-PY', - 'Peru' => 'es-PE', - 'Trinidad & Tobago' => 'en-TT', - 'Uruguay' => 'es-UY', - 'Venezuela' => 'es-VE', - ), - ), - 'defaultValue' => 'en-US', - ) - )); - - const CACHE_TIMEOUT = 3600; - - private $regionName = ''; - - public function collectData() { - $json = getContents(self::URI_API_DATA . $this->getInput('region')); - $data = json_decode($json); - - $this->regionName = $data->region->name; - - foreach ($data->articles as $article) { - $json = getContents(self::URI_API_POST . $article->id); - $post = json_decode($json); - - $item = array(); - $item['title'] = $post->title->rendered; - $item['timestamp'] = $post->date; - $item['uri'] = $post->link; - $item['content'] = $this->formatContent($post->content->rendered); - $item['enclosures'][] = $article->image_full; - - $this->items[] = $item; - } - } - - public function getURI() { - if (is_null($this->getInput('region')) === false) { - return self::URI . '/' . $this->getInput('region') . '/newsroom'; - } - - return parent::getURI() . '/newsroom'; - } - - public function getName() { - if (is_null($this->getInput('region')) === false) { - return $this->regionName . ' - Uber Newsroom'; - } - - return parent::getName(); - } - - private function formatContent($html) { - $html = str_get_html($html); - - foreach ($html->find('div.wp-video') as $div) { - $div->style = ''; - } - - foreach ($html->find('video') as $video) { - $video->width = '100%'; - $video->height = ''; - } - - return $html; - } + +class UberNewsroomBridge extends BridgeAbstract +{ + const NAME = 'Uber Newsroom Bridge'; + const URI = 'https://www.uber.com'; + const URI_API_DATA = 'https://newsroomapi.uber.com/wp-json/newsroom/v1/data?locale='; + const URI_API_POST = 'https://newsroomapi.uber.com/wp-json/wp/v2/posts/'; + const DESCRIPTION = 'Returns news posts'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [[ + 'region' => [ + 'name' => 'Region', + 'type' => 'list', + 'values' => [ + 'Africa' => [ + 'Egypt' => 'ar-EG', + 'Ghana' => 'en-GH', + 'Kenya' => 'en-KE', + 'Morocco' => 'fr-MA', + 'Nigeria' => 'en-NG', + 'South Africa' => 'en-ZA', + 'Tanzania' => 'en-TZ', + 'Uganda' => 'en-UG', + ], + 'Asia' => [ + 'Bangladesh' => 'en-BD', + 'Cambodia' => 'km-KH', + 'China' => 'zh-CN', + 'Hong Kong' => 'zh-HK', + 'India' => 'en-IN', + 'Indonesia' => 'en-ID', + 'Japan' => 'ja-JP', + 'Korea' => 'ko-KR', + 'Macau' => 'zh-MO', + 'Malaysia' => 'en-MY', + 'Myanmar' => 'en-MM', + 'Philippines' => 'en-PH', + 'Singapore' => 'en-SG', + 'Sri Lanka' => 'en-LK', + 'Taiwan' => 'zh-TW', + 'Thailand' => 'th-TH', + 'Vietnam' => 'vi-VN', + ], + 'Central America' => [ + 'Costa Rica' => 'es-CR', + 'Dominican Republic' => 'es-DO', + 'El Salvador' => 'es-SV', + 'Guatemala' => 'es-GT', + 'Honduras' => 'es-HN', + 'Mexico' => 'es-MX', + 'Nicaragua' => 'es-NI', + 'Panama' => 'es-PA', + 'Puerto Rico' => 'es-PR', + ], + 'Europe' => [ + 'Austria' => 'de-AT', + 'Azerbaijan' => 'az', + 'Belarus' => 'ru-BY', + 'Belgium' => 'fr-BE', + 'Bulgaria' => 'bg', + 'Croatia' => 'hr', + 'Czech Republic' => 'cs-CZ', + 'Denmark' => 'da-DK', + 'Estonia' => 'et-EE', + 'Finland' => 'fi', + 'France' => 'fr', + 'Germany' => 'de', + 'Greece' => 'el-GR', + 'Hungary' => 'hu', + 'Ireland' => 'en-IE', + 'Italy' => 'it', + 'Kazakhstan' => 'ru-KZ', + 'Lithuania' => 'lt', + 'Netherlands' => 'nl', + 'Norway' => 'nb-NO', + 'Poland' => 'pl', + 'Portugal' => 'pt', + 'Romania' => 'ro', + 'Russia' => 'ru', + 'Slovakia' => 'sk', + 'Spain' => 'es-ES', + 'Sweden' => 'sv-SE', + 'Switzerland' => 'fr-CH', + 'Turkey' => 'tr', + 'Ukraine' => 'uk-UA', + 'United Kingdom' => 'en-GB', + ], + 'Middle East' => [ + 'Bahrain' => 'en-BH', + 'Israel' => 'he-IL', + 'Jordan' => 'en-JO', + 'Kuwait' => 'en-KW', + 'Lebanon' => 'en-LB', + 'Pakistan' => 'en-PK', + 'Qatar' => 'en-QA', + 'Saudi Arabia' => 'ar-SA', + 'United Arab Emirates' => 'en-AE', + ], + 'North America' => [ + 'Canada' => 'en-CA', + 'United States' => 'en-US', + ], + 'Pacific' => [ + 'Australia' => 'en-AU', + 'New Zealand' => 'en-NZ', + ], + 'South America' => [ + 'Argentina' => 'es-AR', + 'Bolivia' => 'es-BO', + 'Brazil' => 'pt-BR', + 'Chile' => 'es-CL', + 'Colombia' => 'es-CO', + 'Ecuador' => 'es-EC', + 'Paraguay' => 'es-PY', + 'Peru' => 'es-PE', + 'Trinidad & Tobago' => 'en-TT', + 'Uruguay' => 'es-UY', + 'Venezuela' => 'es-VE', + ], + ], + 'defaultValue' => 'en-US', + ] + ]]; + + const CACHE_TIMEOUT = 3600; + + private $regionName = ''; + + public function collectData() + { + $json = getContents(self::URI_API_DATA . $this->getInput('region')); + $data = json_decode($json); + + $this->regionName = $data->region->name; + + foreach ($data->articles as $article) { + $json = getContents(self::URI_API_POST . $article->id); + $post = json_decode($json); + + $item = []; + $item['title'] = $post->title->rendered; + $item['timestamp'] = $post->date; + $item['uri'] = $post->link; + $item['content'] = $this->formatContent($post->content->rendered); + $item['enclosures'][] = $article->image_full; + + $this->items[] = $item; + } + } + + public function getURI() + { + if (is_null($this->getInput('region')) === false) { + return self::URI . '/' . $this->getInput('region') . '/newsroom'; + } + + return parent::getURI() . '/newsroom'; + } + + public function getName() + { + if (is_null($this->getInput('region')) === false) { + return $this->regionName . ' - Uber Newsroom'; + } + + return parent::getName(); + } + + private function formatContent($html) + { + $html = str_get_html($html); + + foreach ($html->find('div.wp-video') as $div) { + $div->style = ''; + } + + foreach ($html->find('video') as $video) { + $video->width = '100%'; + $video->height = ''; + } + + return $html; + } } diff --git a/bridges/UnogsBridge.php b/bridges/UnogsBridge.php index f03555b4..021b75ed 100644 --- a/bridges/UnogsBridge.php +++ b/bridges/UnogsBridge.php @@ -1,191 +1,196 @@ <?php -class UnogsBridge extends BridgeAbstract { - - const MAINTAINER = 'csisoap'; - const NAME = 'uNoGS Bridge'; - const URI = 'https://unogs.com'; - const DESCRIPTION = 'Return what\'s new or removal on Netflix'; - - const PARAMETERS = array( - 'global' => array( - 'feed' => array( - 'name' => 'feed', - 'type' => 'list', - 'title' => 'Choose whether you want latest movies or removal on Netflix', - 'values' => array( - 'What\'s New' => 'new last 7 days', - 'Expiring' => 'expiring' - ) - ), - 'limit' => self::LIMIT, - ), - 'Global' => array(), - 'Country' => array( - 'country_code' => array( - 'name' => 'Country', - 'type' => 'list', - 'title' => 'Choose your preferred country', - 'values' => array( - 'Argentina' => 21, - 'Australia' => 23, - 'Belgium' => 26, - 'Brazil' => 29, - 'Canada' => 33, - 'Colombia' => 36, - 'Czech Republic' => 307, - 'France' => 45, - 'Germany' => 39, - 'Greece' => 327, - 'Hong Kong' => 331, - 'Hungary' => 334, - 'Iceland' => 265, - 'India' => 337, - 'Israel' => 336, - 'Italy' => 269, - 'Japan' => 267, - 'Lithuania' => 357, - 'Malaysia' => 378, - 'Mexico' => 65, - 'Netherlands' => 67, - 'Philippines' => 390, - 'Poland' => 392, - 'Portugal' => 268, - 'Romania' => 400, - 'Russia' => 402, - 'Singapore' => 408, - 'Slovakia' => 412, - 'South Africa' => 447, - 'South Korea' => 348, - 'Spain' => 270, - 'Sweden' => 73, - 'Switzerland' => 34, - 'Thailand' => 425, - 'Turkey' => 432, - 'Ukraine' => 436, - 'United Kingdom' => 46, - 'United States' => 78 - ) - ) - ) - ); - - public function getName() { - $feedName = ''; - if($this->queriedContext == 'Global') { - $feedName .= 'Netflix Global - '; - } elseif($this->queriedContext == 'Country') { - $feedName .= 'Netflix ' . $this->getParametersKey('country_code') . ' - '; - } - if($this->getInput('feed') == 'expiring') { - $feedName .= 'Expiring title'; - } elseif($this->getInput('feed') == 'new last 7 days') { - $feedName .= 'What\'s New'; - } else { - $feedName = self::NAME; - } - return $feedName; - } - - private function getParametersKey($input = '') { - $params = $this->getParameters(); - $tab = 'Country'; - if (!isset($params[$tab][$input])) { - return ''; - } - - return array_search( - $this->getInput($input), - $params[$tab][$input]['values'] - ); - - } - - private function getJSON($url) { - $header = array( - 'Referer: https://unogs.com/', - 'referrer: http://unogs.com' - ); - - $raw = getContents($url, $header); - return json_decode($raw, true); - } - - private function getImage($nfid) { - $url = self::URI . '/api/title/bgimages?netflixid=' . $nfid; - $json = $this->getJSON($url); - $image_wrapper = ''; - if(isset($json['bo1280x448'])) { - $image_wrapper = 'bo1280x448'; - } else { - $image_wrapper = 'bo665x375'; - } - end($json[$image_wrapper]); - $position = key($json[$image_wrapper]); - $image_link = $json[$image_wrapper][$position]['url']; - return $image_link; - } - - private function handleData($data) { - $item = array(); - $item['title'] = $data['title'] . ' - ' . $data['year']; - $item['timestamp'] = $data['titledate']; - $netflix_id = $data['nfid']; - $item['uri'] = 'https://www.netflix.com/title/' . $netflix_id; - $image_url = $this->getImage($netflix_id); - $netflix_synopsis = $data['synopsis']; - $expired_warning = ''; - if(isset($data['expires'])) { - $expired_warning .= '<p><b>Expired on: ' . $data['expires'] . '</b></p>'; - $item['timestamp'] = $data['expires']; - } - $unogs_url = self::URI . '/title/' . $netflix_id; - - $item['content'] = <<<EOD +class UnogsBridge extends BridgeAbstract +{ + const MAINTAINER = 'csisoap'; + const NAME = 'uNoGS Bridge'; + const URI = 'https://unogs.com'; + const DESCRIPTION = 'Return what\'s new or removal on Netflix'; + + const PARAMETERS = [ + 'global' => [ + 'feed' => [ + 'name' => 'feed', + 'type' => 'list', + 'title' => 'Choose whether you want latest movies or removal on Netflix', + 'values' => [ + 'What\'s New' => 'new last 7 days', + 'Expiring' => 'expiring' + ] + ], + 'limit' => self::LIMIT, + ], + 'Global' => [], + 'Country' => [ + 'country_code' => [ + 'name' => 'Country', + 'type' => 'list', + 'title' => 'Choose your preferred country', + 'values' => [ + 'Argentina' => 21, + 'Australia' => 23, + 'Belgium' => 26, + 'Brazil' => 29, + 'Canada' => 33, + 'Colombia' => 36, + 'Czech Republic' => 307, + 'France' => 45, + 'Germany' => 39, + 'Greece' => 327, + 'Hong Kong' => 331, + 'Hungary' => 334, + 'Iceland' => 265, + 'India' => 337, + 'Israel' => 336, + 'Italy' => 269, + 'Japan' => 267, + 'Lithuania' => 357, + 'Malaysia' => 378, + 'Mexico' => 65, + 'Netherlands' => 67, + 'Philippines' => 390, + 'Poland' => 392, + 'Portugal' => 268, + 'Romania' => 400, + 'Russia' => 402, + 'Singapore' => 408, + 'Slovakia' => 412, + 'South Africa' => 447, + 'South Korea' => 348, + 'Spain' => 270, + 'Sweden' => 73, + 'Switzerland' => 34, + 'Thailand' => 425, + 'Turkey' => 432, + 'Ukraine' => 436, + 'United Kingdom' => 46, + 'United States' => 78 + ] + ] + ] + ]; + + public function getName() + { + $feedName = ''; + if ($this->queriedContext == 'Global') { + $feedName .= 'Netflix Global - '; + } elseif ($this->queriedContext == 'Country') { + $feedName .= 'Netflix ' . $this->getParametersKey('country_code') . ' - '; + } + if ($this->getInput('feed') == 'expiring') { + $feedName .= 'Expiring title'; + } elseif ($this->getInput('feed') == 'new last 7 days') { + $feedName .= 'What\'s New'; + } else { + $feedName = self::NAME; + } + return $feedName; + } + + private function getParametersKey($input = '') + { + $params = $this->getParameters(); + $tab = 'Country'; + if (!isset($params[$tab][$input])) { + return ''; + } + + return array_search( + $this->getInput($input), + $params[$tab][$input]['values'] + ); + } + + private function getJSON($url) + { + $header = [ + 'Referer: https://unogs.com/', + 'referrer: http://unogs.com' + ]; + + $raw = getContents($url, $header); + return json_decode($raw, true); + } + + private function getImage($nfid) + { + $url = self::URI . '/api/title/bgimages?netflixid=' . $nfid; + $json = $this->getJSON($url); + $image_wrapper = ''; + if (isset($json['bo1280x448'])) { + $image_wrapper = 'bo1280x448'; + } else { + $image_wrapper = 'bo665x375'; + } + end($json[$image_wrapper]); + $position = key($json[$image_wrapper]); + $image_link = $json[$image_wrapper][$position]['url']; + return $image_link; + } + + private function handleData($data) + { + $item = []; + $item['title'] = $data['title'] . ' - ' . $data['year']; + $item['timestamp'] = $data['titledate']; + $netflix_id = $data['nfid']; + $item['uri'] = 'https://www.netflix.com/title/' . $netflix_id; + $image_url = $this->getImage($netflix_id); + $netflix_synopsis = $data['synopsis']; + $expired_warning = ''; + if (isset($data['expires'])) { + $expired_warning .= '<p><b>Expired on: ' . $data['expires'] . '</b></p>'; + $item['timestamp'] = $data['expires']; + } + $unogs_url = self::URI . '/title/' . $netflix_id; + + $item['content'] = <<<EOD <img src={$image_url}> $expired_warning <p>$netflix_synopsis</p> <p>Details: <a href={$unogs_url}>$unogs_url</a></p> EOD; - $this->items[] = $item; - } - - public function collectData() { - $feed = $this->getInput('feed'); - $is_global = false; - $country_code = ''; - - switch ($this->queriedContext) { - case 'Country': - $country_code = $this->getInput('country_code'); - break; - } - - $limit = $this->getInput('limit') ?? 30; - - // https://rapidapi.com/unogs/api/unogsng/details - $api_url = sprintf( - '%s/api/search?query=%s%s&limit=%s', - self::URI, - urlencode($feed), - $country_code ? '&countrylist=' . $country_code : '', - $limit - ); - - $json_data = $this->getJSON($api_url); - $movies = $json_data['results']; - - if($this->getInput('feed') == 'expiring') { - /* uNoGS API returns movies/series that going to remove - * today according to the day you fetch the data. - * They put items that going to remove in the future on the last - * so I reverse this to get those items, not to bothers those that already removed today. - */ - $movies = array_reverse($movies); - } - - foreach($movies as $movie) { - $this->handleData($movie); - } - } + $this->items[] = $item; + } + + public function collectData() + { + $feed = $this->getInput('feed'); + $is_global = false; + $country_code = ''; + + switch ($this->queriedContext) { + case 'Country': + $country_code = $this->getInput('country_code'); + break; + } + + $limit = $this->getInput('limit') ?? 30; + + // https://rapidapi.com/unogs/api/unogsng/details + $api_url = sprintf( + '%s/api/search?query=%s%s&limit=%s', + self::URI, + urlencode($feed), + $country_code ? '&countrylist=' . $country_code : '', + $limit + ); + + $json_data = $this->getJSON($api_url); + $movies = $json_data['results']; + + if ($this->getInput('feed') == 'expiring') { + /* uNoGS API returns movies/series that going to remove + * today according to the day you fetch the data. + * They put items that going to remove in the future on the last + * so I reverse this to get those items, not to bothers those that already removed today. + */ + $movies = array_reverse($movies); + } + + foreach ($movies as $movie) { + $this->handleData($movie); + } + } } diff --git a/bridges/UnraidCommunityApplicationsBridge.php b/bridges/UnraidCommunityApplicationsBridge.php index c2cb3ace..5acd5049 100644 --- a/bridges/UnraidCommunityApplicationsBridge.php +++ b/bridges/UnraidCommunityApplicationsBridge.php @@ -1,70 +1,81 @@ <?php -class UnraidCommunityApplicationsBridge extends BridgeAbstract { - const NAME = 'Unraid Community Applications'; - const URI = 'https://forums.unraid.net/topic/38582-plug-in-community-applications/'; - const DESCRIPTION = 'Fetches the latest fifteen new apps/plugins from Unraid Community Applications'; - const MAINTAINER = 'Paroleen'; - const CACHE_TIMEOUT = 3600; - const APPSURI = 'https://raw.githubusercontent.com/Squidly271/AppFeed/master/applicationFeed.json'; +class UnraidCommunityApplicationsBridge extends BridgeAbstract +{ + const NAME = 'Unraid Community Applications'; + const URI = 'https://forums.unraid.net/topic/38582-plug-in-community-applications/'; + const DESCRIPTION = 'Fetches the latest fifteen new apps/plugins from Unraid Community Applications'; + const MAINTAINER = 'Paroleen'; + const CACHE_TIMEOUT = 3600; - private $apps = array(); + const APPSURI = 'https://raw.githubusercontent.com/Squidly271/AppFeed/master/applicationFeed.json'; - private function fetchApps() { - Debug::log('Fetching all applications/plugins'); - $this->apps = getContents(self::APPSURI); - $this->apps = json_decode($this->apps, true)['applist']; - } + private $apps = []; - private function sortApps() { - Debug::log('Sorting applications/plugins'); - usort($this->apps, function($app1, $app2) { - return $app1['FirstSeen'] < $app2['FirstSeen'] ? 1 : -1; - }); - } + private function fetchApps() + { + Debug::log('Fetching all applications/plugins'); + $this->apps = getContents(self::APPSURI); + $this->apps = json_decode($this->apps, true)['applist']; + } - public function collectData() { - $this->fetchApps(); - $this->sortApps(); + private function sortApps() + { + Debug::log('Sorting applications/plugins'); + usort($this->apps, function ($app1, $app2) { + return $app1['FirstSeen'] < $app2['FirstSeen'] ? 1 : -1; + }); + } - Debug::log('Building RSS feed'); - foreach($this->apps as $app) { - if(!array_key_exists('Language', $app)) { - $item = array(); - $item['title'] = $app['Name']; - $item['timestamp'] = $app['FirstSeen']; - $item['author'] = explode('\'', $app['Repo'])[0]; - $item['categories'] = explode(' ', $app['Category']); - $item['content'] = ''; + public function collectData() + { + $this->fetchApps(); + $this->sortApps(); - if(array_key_exists('Icon', $app)) - $item['content'] .= '<img style="width: 64px" src="' - . $app['Icon'] - . '">'; + Debug::log('Building RSS feed'); + foreach ($this->apps as $app) { + if (!array_key_exists('Language', $app)) { + $item = []; + $item['title'] = $app['Name']; + $item['timestamp'] = $app['FirstSeen']; + $item['author'] = explode('\'', $app['Repo'])[0]; + $item['categories'] = explode(' ', $app['Category']); + $item['content'] = ''; - if(array_key_exists('Overview', $app)) - $item['content'] .= '<p>' - . $app['Overview'] - . '</p>'; + if (array_key_exists('Icon', $app)) { + $item['content'] .= '<img style="width: 64px" src="' + . $app['Icon'] + . '">'; + } - if(array_key_exists('Project', $app)) - $item['uri'] = $app['Project']; + if (array_key_exists('Overview', $app)) { + $item['content'] .= '<p>' + . $app['Overview'] + . '</p>'; + } - if(array_key_exists('Registry', $app)) - $item['content'] .= '<br><a href="' - . $app['Registry'] - . '">Docker Hub</a>'; + if (array_key_exists('Project', $app)) { + $item['uri'] = $app['Project']; + } - if(array_key_exists('Support', $app)) - $item['content'] .= '<br><a href="' - . $app['Support'] - . '">Support</a>'; + if (array_key_exists('Registry', $app)) { + $item['content'] .= '<br><a href="' + . $app['Registry'] + . '">Docker Hub</a>'; + } - $this->items[] = $item; + if (array_key_exists('Support', $app)) { + $item['content'] .= '<br><a href="' + . $app['Support'] + . '">Support</a>'; + } - if(count($this->items) >= 15) - break; - } - } - } + $this->items[] = $item; + + if (count($this->items) >= 15) { + break; + } + } + } + } } diff --git a/bridges/UnsplashBridge.php b/bridges/UnsplashBridge.php index 876dfe9d..590d16ab 100644 --- a/bridges/UnsplashBridge.php +++ b/bridges/UnsplashBridge.php @@ -2,112 +2,118 @@ class UnsplashBridge extends BridgeAbstract { - const MAINTAINER = 'nel50n, langfingaz'; - const NAME = 'Unsplash Bridge'; - const URI = 'https://unsplash.com/'; - const CACHE_TIMEOUT = 43200; // 12h - const DESCRIPTION = 'Returns the latest photos from Unsplash'; + const MAINTAINER = 'nel50n, langfingaz'; + const NAME = 'Unsplash Bridge'; + const URI = 'https://unsplash.com/'; + const CACHE_TIMEOUT = 43200; // 12h + const DESCRIPTION = 'Returns the latest photos from Unsplash'; - const PARAMETERS = array(array( - 'u' => array( - 'name' => 'Filter by username (optional)', - 'type' => 'text', - 'defaultValue' => 'unsplash' - ), - 'm' => array( - 'name' => 'Max number of photos', - 'type' => 'number', - 'defaultValue' => 20, - 'required' => true - ), - 'prev_q' => array( - 'name' => 'Preview quality', - 'type' => 'list', - 'values' => array( - 'full' => 'full', - 'regular' => 'regular', - 'small' => 'small', - 'thumb' => 'thumb', - ), - 'defaultValue' => 'regular' - ), - 'w' => array( - 'name' => 'Max download width (optional)', - 'exampleValue' => 1920, - 'type' => 'number', - 'defaultValue' => 1920, - ), - 'jpg_q' => array( - 'name' => 'Max JPEG quality (optional)', - 'exampleValue' => 75, - 'type' => 'number', - 'defaultValue' => 75, - ) - )); + const PARAMETERS = [[ + 'u' => [ + 'name' => 'Filter by username (optional)', + 'type' => 'text', + 'defaultValue' => 'unsplash' + ], + 'm' => [ + 'name' => 'Max number of photos', + 'type' => 'number', + 'defaultValue' => 20, + 'required' => true + ], + 'prev_q' => [ + 'name' => 'Preview quality', + 'type' => 'list', + 'values' => [ + 'full' => 'full', + 'regular' => 'regular', + 'small' => 'small', + 'thumb' => 'thumb', + ], + 'defaultValue' => 'regular' + ], + 'w' => [ + 'name' => 'Max download width (optional)', + 'exampleValue' => 1920, + 'type' => 'number', + 'defaultValue' => 1920, + ], + 'jpg_q' => [ + 'name' => 'Max JPEG quality (optional)', + 'exampleValue' => 75, + 'type' => 'number', + 'defaultValue' => 75, + ] + ]]; - public function collectData() - { - $filteredUser = $this->getInput('u'); - $width = $this->getInput('w'); - $max = $this->getInput('m'); - $previewQuality = $this->getInput('prev_q'); - $jpgQuality = $this->getInput('jpg_q'); + public function collectData() + { + $filteredUser = $this->getInput('u'); + $width = $this->getInput('w'); + $max = $this->getInput('m'); + $previewQuality = $this->getInput('prev_q'); + $jpgQuality = $this->getInput('jpg_q'); - $url = 'https://unsplash.com/napi'; - if (strlen($filteredUser) > 0) $url .= '/users/' . $filteredUser; - $url .= '/photos?page=1&per_page=' . $max; - $api_response = getContents($url); + $url = 'https://unsplash.com/napi'; + if (strlen($filteredUser) > 0) { + $url .= '/users/' . $filteredUser; + } + $url .= '/photos?page=1&per_page=' . $max; + $api_response = getContents($url); - $json = json_decode($api_response, true); + $json = json_decode($api_response, true); - foreach ($json as $json_item) { - $item = array(); + foreach ($json as $json_item) { + $item = []; - // Get image URI - $uri = $json_item['urls']['raw'] . '&fm=jpg'; - if ($jpgQuality > 0) $uri .= '&q=' . $jpgQuality; - if ($width > 0) $uri .= '&w=' . $width . '&fit=max'; - $uri .= '.jpg'; // only for format hint - $item['uri'] = $uri; + // Get image URI + $uri = $json_item['urls']['raw'] . '&fm=jpg'; + if ($jpgQuality > 0) { + $uri .= '&q=' . $jpgQuality; + } + if ($width > 0) { + $uri .= '&w=' . $width . '&fit=max'; + } + $uri .= '.jpg'; // only for format hint + $item['uri'] = $uri; - // Get title from description - if (is_null($json_item['description'])) { - $item['title'] = 'Unsplash picture from ' . $json_item['user']['name']; - } else { - $item['title'] = $json_item['description']; - } + // Get title from description + if (is_null($json_item['description'])) { + $item['title'] = 'Unsplash picture from ' . $json_item['user']['name']; + } else { + $item['title'] = $json_item['description']; + } - $item['timestamp'] = $json_item['created_at']; - $content = 'User: <a href="' - . $json_item['user']['links']['html'] - . '">@' - . $json_item['user']['username'] - . '</a>'; - if (isset($json_item['location']['name'])) { - $content .= ' | Location: ' . $json_item['location']['name']; - } - $content .= ' | Image on <a href="' - . $json_item['links']['html'] - . '">Unsplash</a><br><a href="' - . $uri - . '"><img src="' - . $json_item['urls'][$previewQuality] - . '" alt="Image from ' - . $filteredUser - . '" /></a>'; - $item['content'] = $content; + $item['timestamp'] = $json_item['created_at']; + $content = 'User: <a href="' + . $json_item['user']['links']['html'] + . '">@' + . $json_item['user']['username'] + . '</a>'; + if (isset($json_item['location']['name'])) { + $content .= ' | Location: ' . $json_item['location']['name']; + } + $content .= ' | Image on <a href="' + . $json_item['links']['html'] + . '">Unsplash</a><br><a href="' + . $uri + . '"><img src="' + . $json_item['urls'][$previewQuality] + . '" alt="Image from ' + . $filteredUser + . '" /></a>'; + $item['content'] = $content; - $this->items[] = $item; - } - } + $this->items[] = $item; + } + } - public function getName() - { - $filteredUser = $this->getInput('u') ?? ''; - if (strlen($filteredUser) > 0) { - return $filteredUser . ' - ' . self::NAME; - } else { - return self::NAME; - } - } + public function getName() + { + $filteredUser = $this->getInput('u') ?? ''; + if (strlen($filteredUser) > 0) { + return $filteredUser . ' - ' . self::NAME; + } else { + return self::NAME; + } + } } diff --git a/bridges/UrlebirdBridge.php b/bridges/UrlebirdBridge.php index 98a16aae..429e93f5 100644 --- a/bridges/UrlebirdBridge.php +++ b/bridges/UrlebirdBridge.php @@ -1,72 +1,77 @@ <?php -class UrlebirdBridge extends BridgeAbstract { - const MAINTAINER = 'dotter-ak'; - const NAME = 'urlebird.com'; - const URI = 'https://urlebird.com/'; - const DESCRIPTION = 'Bridge for urlebird.com'; - const CACHE_TIMEOUT = 10; - const PARAMETERS = array( - array( - 'query' => array( - 'name' => '@username or #hashtag', - 'type' => 'text', - 'required' => true, - 'exampleValue' => '@willsmith', - 'title' => '@username or #hashtag' - ) - ) - ); +class UrlebirdBridge extends BridgeAbstract +{ + const MAINTAINER = 'dotter-ak'; + const NAME = 'urlebird.com'; + const URI = 'https://urlebird.com/'; + const DESCRIPTION = 'Bridge for urlebird.com'; + const CACHE_TIMEOUT = 10; + const PARAMETERS = [ + [ + 'query' => [ + 'name' => '@username or #hashtag', + 'type' => 'text', + 'required' => true, + 'exampleValue' => '@willsmith', + 'title' => '@username or #hashtag' + ] + ] + ]; - private $title; + private $title; - private function fixURI($uri) { - $path = parse_url($uri, PHP_URL_PATH); - $encoded_path = array_map('urlencode', explode('/', $path)); - return str_replace($path, implode('/', $encoded_path), $uri); - } + private function fixURI($uri) + { + $path = parse_url($uri, PHP_URL_PATH); + $encoded_path = array_map('urlencode', explode('/', $path)); + return str_replace($path, implode('/', $encoded_path), $uri); + } - public function collectData() { - switch($this->getInput('query')[0]) { - default: - returnServerError('Please, enter valid username or hashtag!'); - break; - case '@': - $url = 'https://urlebird.com/user/' . substr($this->getInput('query'), 1) . '/'; - break; - case '#': - $url = 'https://urlebird.com/hash/' . substr($this->getInput('query'), 1) . '/'; - break; - } + public function collectData() + { + switch ($this->getInput('query')[0]) { + default: + returnServerError('Please, enter valid username or hashtag!'); + break; + case '@': + $url = 'https://urlebird.com/user/' . substr($this->getInput('query'), 1) . '/'; + break; + case '#': + $url = 'https://urlebird.com/hash/' . substr($this->getInput('query'), 1) . '/'; + break; + } - $html = getSimpleHTMLDOM($url); - $this->title = $html->find('title', 0)->innertext; - $articles = $html->find('div.thumb'); - foreach ($articles as $article) { - $item = array(); - $item['uri'] = $this->fixURI($article->find('a', 2)->href); - $article_content = getSimpleHTMLDOM($item['uri']); - $item['author'] = $article->find('img', 0)->alt . ' (' . - $article_content->find('a.user-video', 1)->innertext . ')'; - $item['title'] = $article_content->find('title', 0)->innertext; - $item['enclosures'][] = $article_content->find('video', 0)->poster; - $video = $article_content->find('video', 0); - $video->autoplay = null; - $item['content'] = $video->outertext . '<br>' . - $article_content->find('div.music', 0) . '<br>' . - $article_content->find('div.info2', 0)->innertext . - '<br><br><a href="' . $article_content->find('video', 0)->src . - '">Direct video link</a><br><br><a href="' . $item['uri'] . - '">Post link</a><br><br>'; - $this->items[] = $item; - } - } + $html = getSimpleHTMLDOM($url); + $this->title = $html->find('title', 0)->innertext; + $articles = $html->find('div.thumb'); + foreach ($articles as $article) { + $item = []; + $item['uri'] = $this->fixURI($article->find('a', 2)->href); + $article_content = getSimpleHTMLDOM($item['uri']); + $item['author'] = $article->find('img', 0)->alt . ' (' . + $article_content->find('a.user-video', 1)->innertext . ')'; + $item['title'] = $article_content->find('title', 0)->innertext; + $item['enclosures'][] = $article_content->find('video', 0)->poster; + $video = $article_content->find('video', 0); + $video->autoplay = null; + $item['content'] = $video->outertext . '<br>' . + $article_content->find('div.music', 0) . '<br>' . + $article_content->find('div.info2', 0)->innertext . + '<br><br><a href="' . $article_content->find('video', 0)->src . + '">Direct video link</a><br><br><a href="' . $item['uri'] . + '">Post link</a><br><br>'; + $this->items[] = $item; + } + } - public function getName() { - return $this->title ?: parent::getName(); - } + public function getName() + { + return $this->title ?: parent::getName(); + } - public function getIcon() { - return 'https://urlebird.com/favicon.ico'; - } + public function getIcon() + { + return 'https://urlebird.com/favicon.ico'; + } } diff --git a/bridges/UsbekEtRicaBridge.php b/bridges/UsbekEtRicaBridge.php index d5fd507a..3dd432f0 100644 --- a/bridges/UsbekEtRicaBridge.php +++ b/bridges/UsbekEtRicaBridge.php @@ -1,111 +1,115 @@ <?php -class UsbekEtRicaBridge extends BridgeAbstract { - - const MAINTAINER = 'logmanoriginal'; - const NAME = 'Usbek & Rica Bridge'; - const URI = 'https://usbeketrica.com'; - const DESCRIPTION = 'Returns latest articles from the front page'; - - const PARAMETERS = array( - array( - 'limit' => array( - 'name' => 'Number of articles to return', - 'type' => 'number', - 'required' => false, - 'title' => 'Specifies the maximum number of articles to return', - 'defaultValue' => -1 - ), - 'fullarticle' => array( - 'name' => 'Load full article', - 'type' => 'checkbox', - 'required' => false, - 'title' => 'Activate to load full articles', - ) - ) - ); - - public function collectData(){ - $limit = $this->getInput('limit'); - $fullarticle = $this->getInput('fullarticle'); - $html = getSimpleHTMLDOM($this->getURI()); - - $articles = $html->find('article'); - - foreach($articles as $article) { - $item = array(); - - $title = $article->find('h2', 0); - if($title) { - $item['title'] = $title->plaintext; - } else { - // Sometimes we get rubbish, ignore. - continue; - } - - $author = $article->find('div.author span', 0); - if($author) { - $item['author'] = $author->plaintext; - } - - $u = $article->find('a.card-img', 0); - - $uri = $u->href; - if(substr($uri, 0, 1) === 'h') { // absolute uri - $item['uri'] = $uri; - } else { // relative uri - $item['uri'] = $this->getURI() . $uri; - } - - if($fullarticle) { - $content = $this->loadFullArticle($item['uri']); - } - - if($fullarticle && !is_null($content)) { - $item['content'] = $content; - } else { - $excerpt = $article->find('div.card-excerpt', 0); - if($excerpt) { - $item['content'] = $excerpt->plaintext; - } - } - - $image = $article->find('div.card-img img', 0); - if($image) { - $item['enclosures'] = array( - $image->src - ); - } - - $this->items[] = $item; - - if($limit > 0 && count($this->items) >= $limit) { - break; - } - } - } - - /** - * Loads the full article and returns the contents - * @param $uri The article URI - * @return The article content - */ - private function loadFullArticle($uri){ - $html = getSimpleHTMLDOMCached($uri); - - $content = $html->find('div.rich-text', 1); - if($content) { - return $this->replaceUriInHtmlElement($content); - } - - return null; - } - - /** - * Replaces all relative URIs with absolute ones - * @param $element A simplehtmldom element - * @return The $element->innertext with all URIs replaced - */ - private function replaceUriInHtmlElement($element){ - return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext); - } + +class UsbekEtRicaBridge extends BridgeAbstract +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'Usbek & Rica Bridge'; + const URI = 'https://usbeketrica.com'; + const DESCRIPTION = 'Returns latest articles from the front page'; + + const PARAMETERS = [ + [ + 'limit' => [ + 'name' => 'Number of articles to return', + 'type' => 'number', + 'required' => false, + 'title' => 'Specifies the maximum number of articles to return', + 'defaultValue' => -1 + ], + 'fullarticle' => [ + 'name' => 'Load full article', + 'type' => 'checkbox', + 'required' => false, + 'title' => 'Activate to load full articles', + ] + ] + ]; + + public function collectData() + { + $limit = $this->getInput('limit'); + $fullarticle = $this->getInput('fullarticle'); + $html = getSimpleHTMLDOM($this->getURI()); + + $articles = $html->find('article'); + + foreach ($articles as $article) { + $item = []; + + $title = $article->find('h2', 0); + if ($title) { + $item['title'] = $title->plaintext; + } else { + // Sometimes we get rubbish, ignore. + continue; + } + + $author = $article->find('div.author span', 0); + if ($author) { + $item['author'] = $author->plaintext; + } + + $u = $article->find('a.card-img', 0); + + $uri = $u->href; + if (substr($uri, 0, 1) === 'h') { // absolute uri + $item['uri'] = $uri; + } else { // relative uri + $item['uri'] = $this->getURI() . $uri; + } + + if ($fullarticle) { + $content = $this->loadFullArticle($item['uri']); + } + + if ($fullarticle && !is_null($content)) { + $item['content'] = $content; + } else { + $excerpt = $article->find('div.card-excerpt', 0); + if ($excerpt) { + $item['content'] = $excerpt->plaintext; + } + } + + $image = $article->find('div.card-img img', 0); + if ($image) { + $item['enclosures'] = [ + $image->src + ]; + } + + $this->items[] = $item; + + if ($limit > 0 && count($this->items) >= $limit) { + break; + } + } + } + + /** + * Loads the full article and returns the contents + * @param $uri The article URI + * @return The article content + */ + private function loadFullArticle($uri) + { + $html = getSimpleHTMLDOMCached($uri); + + $content = $html->find('div.rich-text', 1); + if ($content) { + return $this->replaceUriInHtmlElement($content); + } + + return null; + } + + /** + * Replaces all relative URIs with absolute ones + * @param $element A simplehtmldom element + * @return The $element->innertext with all URIs replaced + */ + private function replaceUriInHtmlElement($element) + { + return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext); + } } diff --git a/bridges/UsenixBridge.php b/bridges/UsenixBridge.php index 4f785a0e..659f012d 100644 --- a/bridges/UsenixBridge.php +++ b/bridges/UsenixBridge.php @@ -1,68 +1,69 @@ <?php + declare(strict_types=1); final class UsenixBridge extends BridgeAbstract { - const NAME = 'USENIX'; - const URI = 'https://www.usenix.org/publications'; - const DESCRIPTION = 'Digital publications from USENIX (usenix.org)'; - const MAINTAINER = 'dvikan'; - const PARAMETERS = [ - 'USENIX ;login:' => [ - ], - ]; + const NAME = 'USENIX'; + const URI = 'https://www.usenix.org/publications'; + const DESCRIPTION = 'Digital publications from USENIX (usenix.org)'; + const MAINTAINER = 'dvikan'; + const PARAMETERS = [ + 'USENIX ;login:' => [ + ], + ]; - public function collectData() - { - if ($this->queriedContext === 'USENIX ;login:') { - $this->collectLoginOnlineItems(); - return; - } - returnClientError('Illegal Context'); - } + public function collectData() + { + if ($this->queriedContext === 'USENIX ;login:') { + $this->collectLoginOnlineItems(); + return; + } + returnClientError('Illegal Context'); + } - private function collectLoginOnlineItems(): void - { - $url = 'https://www.usenix.org/publications/loginonline'; - $dom = getSimpleHTMLDOMCached($url); - $items = $dom->find('div.view-content > div'); + private function collectLoginOnlineItems(): void + { + $url = 'https://www.usenix.org/publications/loginonline'; + $dom = getSimpleHTMLDOMCached($url); + $items = $dom->find('div.view-content > div'); - foreach ($items as $item) { - $title = $item->find('.views-field-title > span', 0); - $author = $item->find('.views-field-pseudo-author-list > span.field-content', 0); - $relativeUrl = $item->find('.views-field-nothing-1 > span > a', 0); - $uri = sprintf('https://www.usenix.org%s', $relativeUrl->href); - // June 2, 2022 - $createdAt = $item->find('div.views-field-field-lv2-publication-date > div > span', 0); + foreach ($items as $item) { + $title = $item->find('.views-field-title > span', 0); + $author = $item->find('.views-field-pseudo-author-list > span.field-content', 0); + $relativeUrl = $item->find('.views-field-nothing-1 > span > a', 0); + $uri = sprintf('https://www.usenix.org%s', $relativeUrl->href); + // June 2, 2022 + $createdAt = $item->find('div.views-field-field-lv2-publication-date > div > span', 0); - $item = [ - 'title' => $title->innertext, - 'author' => strstr($author->plaintext, ',', true) ?: $author->plaintext, - 'uri' => $uri, - 'timestamp' => $createdAt->innertext, - ]; + $item = [ + 'title' => $title->innertext, + 'author' => strstr($author->plaintext, ',', true) ?: $author->plaintext, + 'uri' => $uri, + 'timestamp' => $createdAt->innertext, + ]; - $this->items[] = array_merge($item, $this->getItemContent($uri)); - } - } + $this->items[] = array_merge($item, $this->getItemContent($uri)); + } + } - private function getItemContent(string $uri) : array - { - $html = getSimpleHTMLDOMCached($uri); - $content = $html->find('.paragraphs-items-full', 0)->innertext; - $extra = $html->find('fieldset', 0); - if (!empty($extra)) { - $content .= $extra->innertext; - } + private function getItemContent(string $uri): array + { + $html = getSimpleHTMLDOMCached($uri); + $content = $html->find('.paragraphs-items-full', 0)->innertext; + $extra = $html->find('fieldset', 0); + if (!empty($extra)) { + $content .= $extra->innertext; + } - $tags = []; - foreach($html->find('.field-name-field-lv2-tags div.field-item') as $tag) { - $tags[] = $tag->plaintext; - } + $tags = []; + foreach ($html->find('.field-name-field-lv2-tags div.field-item') as $tag) { + $tags[] = $tag->plaintext; + } - return [ - 'content' => $content, - 'categories' => $tags - ]; - } + return [ + 'content' => $content, + 'categories' => $tags + ]; + } } diff --git a/bridges/VarietyBridge.php b/bridges/VarietyBridge.php index 8bc48f46..23d1df3f 100644 --- a/bridges/VarietyBridge.php +++ b/bridges/VarietyBridge.php @@ -1,30 +1,33 @@ <?php -class VarietyBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'Variety Bridge'; - const URI = 'https://variety.com'; - const CACHE_TIMEOUT = 3600; - const DESCRIPTION = 'RSS feed for Variety'; +class VarietyBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'Variety Bridge'; + const URI = 'https://variety.com'; + const CACHE_TIMEOUT = 3600; + const DESCRIPTION = 'RSS feed for Variety'; - public function collectData(){ - $this->collectExpandableDatas('https://feeds.feedburner.com/variety/headlines', 15); - } + public function collectData() + { + $this->collectExpandableDatas('https://feeds.feedburner.com/variety/headlines', 15); + } - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); - // Remove Script tags - foreach($articlePage->find('script') as $script_tag) { - $script_tag->remove(); - } - $article = $articlePage->find('div.c-featured-media', 0); - $article = $article . $articlePage->find('.c-content', 0); + // Remove Script tags + foreach ($articlePage->find('script') as $script_tag) { + $script_tag->remove(); + } + $article = $articlePage->find('div.c-featured-media', 0); + $article = $article . $articlePage->find('.c-content', 0); - $item['content'] = $article; + $item['content'] = $article; - return $item; - } + return $item; + } } diff --git a/bridges/ViadeoCompanyBridge.php b/bridges/ViadeoCompanyBridge.php index fd1a29b6..3b147c41 100644 --- a/bridges/ViadeoCompanyBridge.php +++ b/bridges/ViadeoCompanyBridge.php @@ -1,40 +1,43 @@ <?php -class ViadeoCompanyBridge extends BridgeAbstract { - const MAINTAINER = 'regisenguehard'; - const NAME = 'Viadeo Company'; - const URI = 'https://www.viadeo.com/'; - const CACHE_TIMEOUT = 21600; // 6h - const DESCRIPTION = 'Returns most recent actus from Company on Viadeo. +class ViadeoCompanyBridge extends BridgeAbstract +{ + const MAINTAINER = 'regisenguehard'; + const NAME = 'Viadeo Company'; + const URI = 'https://www.viadeo.com/'; + const CACHE_TIMEOUT = 21600; // 6h + const DESCRIPTION = 'Returns most recent actus from Company on Viadeo. (http://www.viadeo.com/fr/company/<strong style="font-weight:bold;">apple</strong>)'; - const PARAMETERS = array( array( - 'c' => array( - 'name' => 'Company name', - 'exampleValue' => 'apple', - 'required' => true - ) - )); + const PARAMETERS = [ [ + 'c' => [ + 'name' => 'Company name', + 'exampleValue' => 'apple', + 'required' => true + ] + ]]; - public function collectData(){ - // Redirects to https://emploi.lefigaro.fr/recherche/entreprises - $url = sprintf('%sfr/company/%s', self::URI, $this->getInput('c')); + public function collectData() + { + // Redirects to https://emploi.lefigaro.fr/recherche/entreprises + $url = sprintf('%sfr/company/%s', self::URI, $this->getInput('c')); - $html = getSimpleHTMLDOM($url); + $html = getSimpleHTMLDOM($url); - // TODO: Fix broken xpath selector - $elements = $html->find('//*[@id="company-newsfeed"]/ul/li'); + // TODO: Fix broken xpath selector + $elements = $html->find('//*[@id="company-newsfeed"]/ul/li'); - foreach($elements as $element) { - $title = $element->find('p', 0)->innertext; - if(!$title) { - continue; - } - $item = array(); - $item['uri'] = $url; - $item['title'] = mb_substr($element->find('p', 0)->innertext, 0, 100); - $item['content'] = $element->find('p', 0)->innertext;; - $this->items[] = $item; - } - } + foreach ($elements as $element) { + $title = $element->find('p', 0)->innertext; + if (!$title) { + continue; + } + $item = []; + $item['uri'] = $url; + $item['title'] = mb_substr($element->find('p', 0)->innertext, 0, 100); + $item['content'] = $element->find('p', 0)->innertext; + ; + $this->items[] = $item; + } + } } diff --git a/bridges/ViceBridge.php b/bridges/ViceBridge.php index 4dccb8ef..14272517 100644 --- a/bridges/ViceBridge.php +++ b/bridges/ViceBridge.php @@ -1,38 +1,42 @@ <?php -class ViceBridge extends FeedExpander { - const MAINTAINER = 'IceWreck'; - const NAME = 'Vice Bridge'; - const URI = 'https://www.vice.com/'; - const CACHE_TIMEOUT = 3600; // This is a news site, so don't cache for more than 10 mins - const DESCRIPTION = 'RSS feed for vice publications like Vice News, Munchies, Motherboard, etc.'; - const PARAMETERS = array( array( - 'feed' => array( - 'name' => 'Feed', - 'type' => 'list', - 'values' => array( - 'Vice News' => 'rss', - 'Motherboard - Tech' => 'en_us/rss/topic/tech', - 'Entertainment' => 'en_us/rss/topic/entertainment', - 'Noisey - Music' => 'en_us/rss/topic/music', - 'Munchies - Food' => 'en_us/rss/topic/food' - ) - ) - )); - public function collectData(){ - $feed = $this->getInput('feed'); - $feedURL = 'https://www.vice.com/' . $feed; - $this->collectExpandableDatas($feedURL, 10); - } +class ViceBridge extends FeedExpander +{ + const MAINTAINER = 'IceWreck'; + const NAME = 'Vice Bridge'; + const URI = 'https://www.vice.com/'; + const CACHE_TIMEOUT = 3600; // This is a news site, so don't cache for more than 10 mins + const DESCRIPTION = 'RSS feed for vice publications like Vice News, Munchies, Motherboard, etc.'; + const PARAMETERS = [ [ + 'feed' => [ + 'name' => 'Feed', + 'type' => 'list', + 'values' => [ + 'Vice News' => 'rss', + 'Motherboard - Tech' => 'en_us/rss/topic/tech', + 'Entertainment' => 'en_us/rss/topic/entertainment', + 'Noisey - Music' => 'en_us/rss/topic/music', + 'Munchies - Food' => 'en_us/rss/topic/food' + ] + ] + ]]; - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - // $articlePage gets the entire page's contents - $articlePage = getSimpleHTMLDOM($newsItem->link); - // text and embedded content - $article = $article . $articlePage->find('.article__body', 0); - $item['content'] = $article; + public function collectData() + { + $feed = $this->getInput('feed'); + $feedURL = 'https://www.vice.com/' . $feed; + $this->collectExpandableDatas($feedURL, 10); + } - return $item; - } + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + // $articlePage gets the entire page's contents + $articlePage = getSimpleHTMLDOM($newsItem->link); + // text and embedded content + $article = $article . $articlePage->find('.article__body', 0); + $item['content'] = $article; + + return $item; + } } diff --git a/bridges/VieDeMerdeBridge.php b/bridges/VieDeMerdeBridge.php index d9c906e5..9e6166fb 100644 --- a/bridges/VieDeMerdeBridge.php +++ b/bridges/VieDeMerdeBridge.php @@ -1,56 +1,58 @@ <?php -class VieDeMerdeBridge extends BridgeAbstract { - - const MAINTAINER = 'floviolleau'; - const NAME = 'VieDeMerde Bridge'; - const URI = 'https://www.viedemerde.fr'; - const DESCRIPTION = 'Returns latest quotes from VieDeMerde.'; - const CACHE_TIMEOUT = 7200; - - const PARAMETERS = array(array( - 'item_limit' => array( - 'name' => 'Limit number of returned items', - 'type' => 'number', - 'defaultValue' => 20 - ) - )); - - public function collectData() { - $limit = $this->getInput('item_limit'); - - if ($limit < 1) { - $limit = 20; - } - - $html = getSimpleHTMLDOM(self::URI, array()); - $quotes = $html->find('article.bg-white'); - if(sizeof($quotes) === 0) { - return; - } - - foreach($quotes as $quote) { - $item = array(); - $item['uri'] = self::URI . $quote->find('a', 0)->href; - $titleContent = $quote->find('h2', 0); - - if($titleContent) { - $item['title'] = html_entity_decode($titleContent->plaintext, ENT_QUOTES); - } else { - continue; - } - - $quoteText = $quote->find('a', 1)->plaintext; - $isAVDM = $quote->find('.vote-btn', 0)->plaintext; - $isNotAVDM = $quote->find('.vote-btn', 1)->plaintext; - $item['content'] = $quoteText . '<br>' . $isAVDM . '<br>' . $isNotAVDM; - $item['author'] = $quote->find('p', 0)->plaintext; - $item['uid'] = hash('sha256', $item['title']); - - $this->items[] = $item; - - if (count($this->items) >= $limit) { - break; - } - } - } + +class VieDeMerdeBridge extends BridgeAbstract +{ + const MAINTAINER = 'floviolleau'; + const NAME = 'VieDeMerde Bridge'; + const URI = 'https://www.viedemerde.fr'; + const DESCRIPTION = 'Returns latest quotes from VieDeMerde.'; + const CACHE_TIMEOUT = 7200; + + const PARAMETERS = [[ + 'item_limit' => [ + 'name' => 'Limit number of returned items', + 'type' => 'number', + 'defaultValue' => 20 + ] + ]]; + + public function collectData() + { + $limit = $this->getInput('item_limit'); + + if ($limit < 1) { + $limit = 20; + } + + $html = getSimpleHTMLDOM(self::URI, []); + $quotes = $html->find('article.bg-white'); + if (sizeof($quotes) === 0) { + return; + } + + foreach ($quotes as $quote) { + $item = []; + $item['uri'] = self::URI . $quote->find('a', 0)->href; + $titleContent = $quote->find('h2', 0); + + if ($titleContent) { + $item['title'] = html_entity_decode($titleContent->plaintext, ENT_QUOTES); + } else { + continue; + } + + $quoteText = $quote->find('a', 1)->plaintext; + $isAVDM = $quote->find('.vote-btn', 0)->plaintext; + $isNotAVDM = $quote->find('.vote-btn', 1)->plaintext; + $item['content'] = $quoteText . '<br>' . $isAVDM . '<br>' . $isNotAVDM; + $item['author'] = $quote->find('p', 0)->plaintext; + $item['uid'] = hash('sha256', $item['title']); + + $this->items[] = $item; + + if (count($this->items) >= $limit) { + break; + } + } + } } diff --git a/bridges/VimeoBridge.php b/bridges/VimeoBridge.php index d97026d6..80bfb8ac 100644 --- a/bridges/VimeoBridge.php +++ b/bridges/VimeoBridge.php @@ -1,175 +1,197 @@ <?php -class VimeoBridge extends BridgeAbstract { - - const NAME = 'Vimeo Bridge'; - const URI = 'https://vimeo.com/'; - const DESCRIPTION = 'Returns search results from Vimeo'; - const MAINTAINER = 'logmanoriginal'; - - const PARAMETERS = array( - array( - 'q' => array( - 'name' => 'Search Query', - 'type' => 'text', - 'exampleValue' => 'birds', - 'required' => true - ), - 'type' => array( - 'name' => 'Show results for', - 'type' => 'list', - 'defaultValue' => 'Videos', - 'values' => array( - 'Videos' => 'search', - 'On Demand' => 'search/ondemand', - 'People' => 'search/people', - 'Channels' => 'search/channels', - 'Groups' => 'search/groups' - ) - ) - ) - ); - - public function getURI() { - if(($query = $this->getInput('q')) - && ($type = $this->getInput('type'))) { - return self::URI . $type . '/sort:latest?q=' . $query; - } - - return parent::getURI(); - } - - public function collectData() { - - $html = getSimpleHTMLDOM($this->getURI(), - $header = array(), - $opts = array(), - $lowercase = true, - $forceTagsClosed = true, - $target_charset = DEFAULT_TARGET_CHARSET, - $stripRN = false, // We want to keep newline characters - $defaultBRText = DEFAULT_BR_TEXT, - $defaultSpanText = DEFAULT_SPAN_TEXT); - - $json = null; // Holds the JSON data - - /** - * Search results are included as JSON formatted string inside a script - * tag that has the variable 'vimeo.config'. The data is condensed into - * a single line of code, so we can just search for the newline. - * - * Everything after "vimeo.config = _extend((vimeo.config || {}), " is - * the JSON formatted string. - */ - foreach($html->find('script') as $script) { - foreach(explode("\n", $script) as $line) { - $line = trim($line); - - if(strpos($line, 'vimeo.config') !== 0) - continue; - - // 45 = strlen("vimeo.config = _extend((vimeo.config || {}), "); - // 47 = 45 + 2, because we don't want the final ");" - $json = json_decode(substr($line, 45, strlen($line) - 47)); - } - } - - if(is_null($json)) { - returnClientError('No results for this query!'); - } - - foreach($json->api->initial_json->data as $element) { - switch($element->type) { - case 'clip': $this->addClip($element); break; - case 'ondemand': $this->addOnDemand($element); break; - case 'people': $this->addPeople($element); break; - case 'channel': $this->addChannel($element); break; - case 'group': $this->addGroup($element); break; - - default: returnServerError('Unknown type: ' . $element->type); - } - } - - } - - private function addClip($element) { - $item = array(); - - $item['uri'] = $element->clip->link; - $item['title'] = $element->clip->name; - $item['author'] = $element->clip->user->name; - $item['timestamp'] = strtotime($element->clip->created_time); - - $item['enclosures'] = array( - end($element->clip->pictures->sizes)->link - ); - - $item['content'] = "<img src={$item['enclosures'][0]} />"; - - $this->items[] = $item; - } - - private function addOnDemand($element) { - $item = array(); - - $item['uri'] = $element->ondemand->link; - $item['title'] = $element->ondemand->name; - - // Only for films - if(isset($element->ondemand->film)) - $item['timestamp'] = strtotime($element->ondemand->film->release_time); +class VimeoBridge extends BridgeAbstract +{ + const NAME = 'Vimeo Bridge'; + const URI = 'https://vimeo.com/'; + const DESCRIPTION = 'Returns search results from Vimeo'; + const MAINTAINER = 'logmanoriginal'; + + const PARAMETERS = [ + [ + 'q' => [ + 'name' => 'Search Query', + 'type' => 'text', + 'exampleValue' => 'birds', + 'required' => true + ], + 'type' => [ + 'name' => 'Show results for', + 'type' => 'list', + 'defaultValue' => 'Videos', + 'values' => [ + 'Videos' => 'search', + 'On Demand' => 'search/ondemand', + 'People' => 'search/people', + 'Channels' => 'search/channels', + 'Groups' => 'search/groups' + ] + ] + ] + ]; + + public function getURI() + { + if ( + ($query = $this->getInput('q')) + && ($type = $this->getInput('type')) + ) { + return self::URI . $type . '/sort:latest?q=' . $query; + } + + return parent::getURI(); + } + + public function collectData() + { + $html = getSimpleHTMLDOM( + $this->getURI(), + $header = [], + $opts = [], + $lowercase = true, + $forceTagsClosed = true, + $target_charset = DEFAULT_TARGET_CHARSET, + $stripRN = false, // We want to keep newline characters + $defaultBRText = DEFAULT_BR_TEXT, + $defaultSpanText = DEFAULT_SPAN_TEXT + ); + + $json = null; // Holds the JSON data + + /** + * Search results are included as JSON formatted string inside a script + * tag that has the variable 'vimeo.config'. The data is condensed into + * a single line of code, so we can just search for the newline. + * + * Everything after "vimeo.config = _extend((vimeo.config || {}), " is + * the JSON formatted string. + */ + foreach ($html->find('script') as $script) { + foreach (explode("\n", $script) as $line) { + $line = trim($line); + + if (strpos($line, 'vimeo.config') !== 0) { + continue; + } + + // 45 = strlen("vimeo.config = _extend((vimeo.config || {}), "); + // 47 = 45 + 2, because we don't want the final ");" + $json = json_decode(substr($line, 45, strlen($line) - 47)); + } + } + + if (is_null($json)) { + returnClientError('No results for this query!'); + } + + foreach ($json->api->initial_json->data as $element) { + switch ($element->type) { + case 'clip': + $this->addClip($element); + break; + case 'ondemand': + $this->addOnDemand($element); + break; + case 'people': + $this->addPeople($element); + break; + case 'channel': + $this->addChannel($element); + break; + case 'group': + $this->addGroup($element); + break; + + default: + returnServerError('Unknown type: ' . $element->type); + } + } + } + + private function addClip($element) + { + $item = []; + + $item['uri'] = $element->clip->link; + $item['title'] = $element->clip->name; + $item['author'] = $element->clip->user->name; + $item['timestamp'] = strtotime($element->clip->created_time); + + $item['enclosures'] = [ + end($element->clip->pictures->sizes)->link + ]; + + $item['content'] = "<img src={$item['enclosures'][0]} />"; + + $this->items[] = $item; + } + + private function addOnDemand($element) + { + $item = []; + + $item['uri'] = $element->ondemand->link; + $item['title'] = $element->ondemand->name; + + // Only for films + if (isset($element->ondemand->film)) { + $item['timestamp'] = strtotime($element->ondemand->film->release_time); + } + + $item['enclosures'] = [ + end($element->ondemand->pictures->sizes)->link + ]; + + $item['content'] = "<img src={$item['enclosures'][0]} />"; + + $this->items[] = $item; + } + + private function addPeople($element) + { + $item = []; + + $item['uri'] = $element->people->link; + $item['title'] = $element->people->name; + + $item['enclosures'] = [ + end($element->people->pictures->sizes)->link + ]; + + $item['content'] = "<img src={$item['enclosures'][0]} />"; + + $this->items[] = $item; + } + + private function addChannel($element) + { + $item = []; + + $item['uri'] = $element->channel->link; + $item['title'] = $element->channel->name; - $item['enclosures'] = array( - end($element->ondemand->pictures->sizes)->link - ); + $item['enclosures'] = [ + end($element->channel->pictures->sizes)->link + ]; - $item['content'] = "<img src={$item['enclosures'][0]} />"; + $item['content'] = "<img src={$item['enclosures'][0]} />"; - $this->items[] = $item; - } + $this->items[] = $item; + } - private function addPeople($element) { - $item = array(); + private function addGroup($element) + { + $item = []; - $item['uri'] = $element->people->link; - $item['title'] = $element->people->name; + $item['uri'] = $element->group->link; + $item['title'] = $element->group->name; - $item['enclosures'] = array( - end($element->people->pictures->sizes)->link - ); + $item['enclosures'] = [ + end($element->group->pictures->sizes)->link + ]; - $item['content'] = "<img src={$item['enclosures'][0]} />"; + $item['content'] = "<img src={$item['enclosures'][0]} />"; - $this->items[] = $item; - } - - private function addChannel($element) { - $item = array(); - - $item['uri'] = $element->channel->link; - $item['title'] = $element->channel->name; - - $item['enclosures'] = array( - end($element->channel->pictures->sizes)->link - ); - - $item['content'] = "<img src={$item['enclosures'][0]} />"; - - $this->items[] = $item; - } - - private function addGroup($element) { - $item = array(); - - $item['uri'] = $element->group->link; - $item['title'] = $element->group->name; - - $item['enclosures'] = array( - end($element->group->pictures->sizes)->link - ); - - $item['content'] = "<img src={$item['enclosures'][0]} />"; - - $this->items[] = $item; - } + $this->items[] = $item; + } } diff --git a/bridges/VixenBridge.php b/bridges/VixenBridge.php index 721524e9..048b9a7b 100644 --- a/bridges/VixenBridge.php +++ b/bridges/VixenBridge.php @@ -1,99 +1,111 @@ <?php -class VixenBridge extends BridgeAbstract { - const NAME = 'Vixen Network Bridge'; - const URI = 'https://www.vixen.com'; - const DESCRIPTION = 'Latest videos from Vixen Network sites'; - const MAINTAINER = 'pubak42'; - /** - * The pictures on the pages are referenced with temporary links with - * limited validity. Greater cache timeout results in invalid links in - * the feed - */ - const CACHE_TIMEOUT = 60; +class VixenBridge extends BridgeAbstract +{ + const NAME = 'Vixen Network Bridge'; + const URI = 'https://www.vixen.com'; + const DESCRIPTION = 'Latest videos from Vixen Network sites'; + const MAINTAINER = 'pubak42'; - const PARAMETERS = array( - array( - 'site' => array( - 'type' => 'list', - 'name' => 'Site', - 'title' => 'Choose site of interest', - 'values' => array( - 'Blacked' => 'Blacked', - 'BlackedRaw' => 'BlackedRaw', - 'Tushy' => 'Tushy', - 'TushyRaw' => 'TushyRaw', - 'Vixen' => 'Vixen', - 'Slayed' => 'Slayed', - 'Deeper' => 'Deeper' - ), - ) - ) - ); + /** + * The pictures on the pages are referenced with temporary links with + * limited validity. Greater cache timeout results in invalid links in + * the feed + */ + const CACHE_TIMEOUT = 60; - public function collectData() { - $videosURL = $this->getURI() . '/videos'; + const PARAMETERS = [ + [ + 'site' => [ + 'type' => 'list', + 'name' => 'Site', + 'title' => 'Choose site of interest', + 'values' => [ + 'Blacked' => 'Blacked', + 'BlackedRaw' => 'BlackedRaw', + 'Tushy' => 'Tushy', + 'TushyRaw' => 'TushyRaw', + 'Vixen' => 'Vixen', + 'Slayed' => 'Slayed', + 'Deeper' => 'Deeper' + ], + ] + ] + ]; - $website = getSimpleHTMLDOM($videosURL); - $json = $website->getElementById('__NEXT_DATA__'); - $data = json_decode($json->innertext(), true); - $nodes = array_column($data['props']['pageProps']['edges'], 'node'); + public function collectData() + { + $videosURL = $this->getURI() . '/videos'; - foreach($nodes as $n) { - $imageURL = $n['images']['listing'][2]['highdpi']['triple']; + $website = getSimpleHTMLDOM($videosURL); + $json = $website->getElementById('__NEXT_DATA__'); + $data = json_decode($json->innertext(), true); + $nodes = array_column($data['props']['pageProps']['edges'], 'node'); - $item = [ - 'title' => $n['title'], - 'uri' => "$videosURL/$n[slug]", - 'uid' => $n['videoId'], - 'timestamp' => strtotime($n['releaseDate']), - 'enclosures' => [ $imageURL ], - 'author' => implode(' & ', array_column($n['modelsSlugged'], 'name')), - ]; + foreach ($nodes as $n) { + $imageURL = $n['images']['listing'][2]['highdpi']['triple']; - /* - * No images retrieved from here. Should be cached for as long as - * possible to avoid rate throttling - */ - $target = getSimpleHtmlDOMCached($item['uri'], 86400); - $item['content'] = $this->generateContent($imageURL, - $target->find('meta[name=description]', 0)->content, - $n['modelsSlugged']); + $item = [ + 'title' => $n['title'], + 'uri' => "$videosURL/$n[slug]", + 'uid' => $n['videoId'], + 'timestamp' => strtotime($n['releaseDate']), + 'enclosures' => [ $imageURL ], + 'author' => implode(' & ', array_column($n['modelsSlugged'], 'name')), + ]; - $item['categories'] = array_map('ucwords', - explode(',', $target->find('meta[name=keywords]', 0)->content)); + /* + * No images retrieved from here. Should be cached for as long as + * possible to avoid rate throttling + */ + $target = getSimpleHtmlDOMCached($item['uri'], 86400); + $item['content'] = $this->generateContent( + $imageURL, + $target->find('meta[name=description]', 0)->content, + $n['modelsSlugged'] + ); - $this->items[] = $item; - } - } + $item['categories'] = array_map( + 'ucwords', + explode(',', $target->find('meta[name=keywords]', 0)->content) + ); - public function getURI() { - $param = $this->getInput('site'); - return $param ? "https://www.$param.com" : self::URI; - } + $this->items[] = $item; + } + } - /** - * Return name of the bridge. Default is needed for bridge index list - */ - public function getName() { - $param = $this->getInput('site'); - return $param ? "$param Bridge" : self::NAME; - } + public function getURI() + { + $param = $this->getInput('site'); + return $param ? "https://www.$param.com" : self::URI; + } - private static function makeLink($URI, $text) { - return "<a href=\"$URI\">$text</a>"; - } + /** + * Return name of the bridge. Default is needed for bridge index list + */ + public function getName() + { + $param = $this->getInput('site'); + return $param ? "$param Bridge" : self::NAME; + } - private function generateContent($imageURI, $description, $models) { - $content = "<img src=\"$imageURI\" referrerpolicy=\"no-referrer\"/><p>$description</p>"; - $modelLinks = array_map( - function($model) { - return self::makeLink( - $this->getURI() . "/models/$model[slugged]", - $model['name']); - }, - $models - ); - return $content . '<p>Starring: ' . implode(' & ', $modelLinks) . '</p>'; - } + private static function makeLink($URI, $text) + { + return "<a href=\"$URI\">$text</a>"; + } + + private function generateContent($imageURI, $description, $models) + { + $content = "<img src=\"$imageURI\" referrerpolicy=\"no-referrer\"/><p>$description</p>"; + $modelLinks = array_map( + function ($model) { + return self::makeLink( + $this->getURI() . "/models/$model[slugged]", + $model['name'] + ); + }, + $models + ); + return $content . '<p>Starring: ' . implode(' & ', $modelLinks) . '</p>'; + } } diff --git a/bridges/VkBridge.php b/bridges/VkBridge.php index 1d0f65b0..bb554bc1 100644 --- a/bridges/VkBridge.php +++ b/bridges/VkBridge.php @@ -2,463 +2,476 @@ class VkBridge extends BridgeAbstract { - - const MAINTAINER = 'em92'; - // const MAINTAINER = 'pmaziere'; - // const MAINTAINER = 'ahiles3005'; - const NAME = 'VK.com'; - const URI = 'https://vk.com/'; - const CACHE_TIMEOUT = 300; // 5min - const DESCRIPTION = 'Working with open pages'; - const PARAMETERS = array( - array( - 'u' => array( - 'name' => 'Group or user name', - 'exampleValue' => 'elonmusk_tech', - 'required' => true - ), - 'hide_reposts' => array( - 'name' => 'Hide reposts', - 'type' => 'checkbox', - ) - ) - ); - - protected $videos = array(); - protected $pageName; - - protected function getAccessToken() - { - return 'e69b2db9f6cd4a97c0716893232587165c18be85bc1af1834560125c1d3c8ec281eb407a78cca0ae16776'; - } - - public function getURI() - { - if (!is_null($this->getInput('u'))) { - return static::URI . urlencode($this->getInput('u')); - } - - return parent::getURI(); - } - - public function getName() - { - if ($this->pageName) { - return $this->pageName; - } - - return parent::getName(); - } - - public function collectData() - { - $text_html = $this->getContents(); - - $text_html = iconv('windows-1251', 'utf-8//ignore', $text_html); - // makes album link generating work correctly - $text_html = str_replace('"class="page_album_link">', '" class="page_album_link">', $text_html); - $html = str_get_html($text_html); - $pageName = $html->find('.page_name', 0); - if (is_object($pageName)) { - $pageName = $pageName->plaintext; - $this->pageName = htmlspecialchars_decode($pageName); - } - foreach ($html->find('div.replies') as $comment_block) { - $comment_block->outertext = ''; - } - $html->load($html->save()); - - $pinned_post_item = null; - $last_post_id = 0; - - foreach ($html->find('.post') as $post) { - - if ($post->find('.wall_post_text_deleted')) { - // repost of deleted post - continue; - } - - defaultLinkTo($post, self::URI); - - $post_videos = array(); - - $is_pinned_post = false; - if (strpos($post->getAttribute('class'), 'post_fixed') !== false) { - $is_pinned_post = true; - } - - if (is_object($post->find('a.wall_post_more', 0))) { - //delete link "show full" in content - $post->find('a.wall_post_more', 0)->outertext = ''; - } - - $content_suffix = ''; - - // looking for external links - $external_link_selectors = array( - 'a.page_media_link_title', - 'div.page_media_link_title > a', - 'div.media_desc > a.lnk', - ); - - foreach($external_link_selectors as $sel) { - if (is_object($post->find($sel, 0))) { - $a = $post->find($sel, 0); - $innertext = $a->innertext; - $parsed_url = parse_url($a->getAttribute('href')); - if (strpos($parsed_url['path'], '/away.php') !== 0) continue; - parse_str($parsed_url['query'], $parsed_query); - $content_suffix .= "<br>External link: <a href='" . $parsed_query['to'] . "'>$innertext</a>"; - } - } - - // remove external link from content - $external_link_selectors_to_remove = array( - 'div.page_media_thumbed_link', - 'div.page_media_link_desc_wrap', - 'div.media_desc > a.lnk', - ); - - foreach($external_link_selectors_to_remove as $sel) { - if (is_object($post->find($sel, 0))) { - $post->find($sel, 0)->outertext = ''; - } - } - - // looking for article - $article = $post->find('a.article_snippet', 0); - if (is_object($article)) { - if (strpos($article->getAttribute('class'), 'article_snippet_mini') !== false) { - $article_title_selector = 'div.article_snippet_mini_title'; - $article_author_selector = 'div.article_snippet_mini_info > .mem_link, + const MAINTAINER = 'em92'; + // const MAINTAINER = 'pmaziere'; + // const MAINTAINER = 'ahiles3005'; + const NAME = 'VK.com'; + const URI = 'https://vk.com/'; + const CACHE_TIMEOUT = 300; // 5min + const DESCRIPTION = 'Working with open pages'; + const PARAMETERS = [ + [ + 'u' => [ + 'name' => 'Group or user name', + 'exampleValue' => 'elonmusk_tech', + 'required' => true + ], + 'hide_reposts' => [ + 'name' => 'Hide reposts', + 'type' => 'checkbox', + ] + ] + ]; + + protected $videos = []; + protected $pageName; + + protected function getAccessToken() + { + return 'e69b2db9f6cd4a97c0716893232587165c18be85bc1af1834560125c1d3c8ec281eb407a78cca0ae16776'; + } + + public function getURI() + { + if (!is_null($this->getInput('u'))) { + return static::URI . urlencode($this->getInput('u')); + } + + return parent::getURI(); + } + + public function getName() + { + if ($this->pageName) { + return $this->pageName; + } + + return parent::getName(); + } + + public function collectData() + { + $text_html = $this->getContents(); + + $text_html = iconv('windows-1251', 'utf-8//ignore', $text_html); + // makes album link generating work correctly + $text_html = str_replace('"class="page_album_link">', '" class="page_album_link">', $text_html); + $html = str_get_html($text_html); + $pageName = $html->find('.page_name', 0); + if (is_object($pageName)) { + $pageName = $pageName->plaintext; + $this->pageName = htmlspecialchars_decode($pageName); + } + foreach ($html->find('div.replies') as $comment_block) { + $comment_block->outertext = ''; + } + $html->load($html->save()); + + $pinned_post_item = null; + $last_post_id = 0; + + foreach ($html->find('.post') as $post) { + if ($post->find('.wall_post_text_deleted')) { + // repost of deleted post + continue; + } + + defaultLinkTo($post, self::URI); + + $post_videos = []; + + $is_pinned_post = false; + if (strpos($post->getAttribute('class'), 'post_fixed') !== false) { + $is_pinned_post = true; + } + + if (is_object($post->find('a.wall_post_more', 0))) { + //delete link "show full" in content + $post->find('a.wall_post_more', 0)->outertext = ''; + } + + $content_suffix = ''; + + // looking for external links + $external_link_selectors = [ + 'a.page_media_link_title', + 'div.page_media_link_title > a', + 'div.media_desc > a.lnk', + ]; + + foreach ($external_link_selectors as $sel) { + if (is_object($post->find($sel, 0))) { + $a = $post->find($sel, 0); + $innertext = $a->innertext; + $parsed_url = parse_url($a->getAttribute('href')); + if (strpos($parsed_url['path'], '/away.php') !== 0) { + continue; + } + parse_str($parsed_url['query'], $parsed_query); + $content_suffix .= "<br>External link: <a href='" . $parsed_query['to'] . "'>$innertext</a>"; + } + } + + // remove external link from content + $external_link_selectors_to_remove = [ + 'div.page_media_thumbed_link', + 'div.page_media_link_desc_wrap', + 'div.media_desc > a.lnk', + ]; + + foreach ($external_link_selectors_to_remove as $sel) { + if (is_object($post->find($sel, 0))) { + $post->find($sel, 0)->outertext = ''; + } + } + + // looking for article + $article = $post->find('a.article_snippet', 0); + if (is_object($article)) { + if (strpos($article->getAttribute('class'), 'article_snippet_mini') !== false) { + $article_title_selector = 'div.article_snippet_mini_title'; + $article_author_selector = 'div.article_snippet_mini_info > .mem_link, div.article_snippet_mini_info > .group_link'; - $article_thumb_selector = 'div.article_snippet_mini_thumb'; - } else { - $article_title_selector = 'div.article_snippet__title'; - $article_author_selector = 'div.article_snippet__author'; - $article_thumb_selector = 'div.article_snippet__image'; - } - $article_title = $article->find($article_title_selector, 0)->innertext; - $article_author = $article->find($article_author_selector, 0)->innertext; - $article_link = $article->getAttribute('href'); - $article_img_element_style = $article->find($article_thumb_selector, 0)->getAttribute('style'); - preg_match('/background-image: url\((.*)\)/', $article_img_element_style, $matches); - if (count($matches) > 0) { - $content_suffix .= "<br><img src='" . $matches[1] . "'>"; - } - $content_suffix .= "<br>Article: <a href='$article_link'>$article_title ($article_author)</a>"; - $article->outertext = ''; - } - - // get video on post - $video = $post->find('div.post_video_desc', 0); - $main_video_link = ''; - if (is_object($video)) { - $video_title = $video->find('div.post_video_title', 0)->plaintext; - $video_link = $video->find('a.lnk', 0)->getAttribute('href'); - $this->appendVideo($video_title, $video_link, $content_suffix, $post_videos); - $video->outertext = ''; - $main_video_link = $video_link; - } - - // get all other videos - foreach($post->find('a.page_post_thumb_video') as $a) { - $video_title = htmlspecialchars_decode($a->getAttribute('aria-label')); - $video_link = $a->getAttribute('href'); - if ($video_link != $main_video_link) $this->appendVideo($video_title, $video_link, $content_suffix, $post_videos); - $a->outertext = ''; - } - - // get all photos - foreach($post->find('div.wall_text a.page_post_thumb_wrap') as $a) { - $result = $this->getPhoto($a); - if ($result == null) continue; - $a->outertext = ''; - $content_suffix .= "<br>$result"; - } - - // get albums - foreach($post->find('.page_album_wrap') as $el) { - $a = $el->find('.page_album_link', 0); - $album_title = $a->find('.page_album_title_text', 0)->getAttribute('title'); - $album_link = $a->getAttribute('href'); - $el->outertext = ''; - $content_suffix .= "<br>Album: <a href='$album_link'>$album_title</a>"; - } - - // get photo documents - foreach($post->find('a.page_doc_photo_href') as $a) { - $doc_link = $a->getAttribute('href'); - $doc_gif_label_element = $a->find('.page_gif_label', 0); - $doc_title_element = $a->find('.doc_label', 0); - - if (is_object($doc_gif_label_element)) { - $gif_preview_img = backgroundToImg($a->find('.page_doc_photo', 0)); - $content_suffix .= "<br>Gif: <a href='$doc_link'>$gif_preview_img</a>"; - - } else if (is_object($doc_title_element)) { - $doc_title = $doc_title_element->innertext; - $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>"; - - } else { - continue; - - } - - $a->outertext = ''; - } - - // get other documents - foreach($post->find('div.page_doc_row') as $div) { - $doc_title_element = $div->find('a.page_doc_title', 0); - - if (is_object($doc_title_element)) { - $doc_title = $doc_title_element->innertext; - $doc_link = $doc_title_element->getAttribute('href'); - $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>"; - - } else { - continue; - - } - - $div->outertext = ''; - } - - // get polls - foreach($post->find('div.page_media_poll_wrap') as $div) { - $poll_title = $div->find('.page_media_poll_title', 0)->innertext; - $content_suffix .= "<br>Poll: $poll_title"; - foreach($div->find('div.page_poll_text') as $poll_stat_title) { - $content_suffix .= '<br>- ' . $poll_stat_title->innertext; - } - $div->outertext = ''; - } - - // get sign / post author - $post_author = $pageName; - $author_selectors = array('a.wall_signed_by', 'a.author'); - foreach($author_selectors as $author_selector) { - $a = $post->find($author_selector, 0); - if (is_object($a)) { - $post_author = $a->innertext; - $a->outertext = ''; - break; - } - } - - // fix links and get post hashtags - $hashtags = array(); - foreach($post->find('a') as $a) { - $href = $a->getAttribute('href'); - $innertext = $a->innertext; - - $hashtag_prefix = '/feed?section=search&q=%23'; - $hashtag = null; - - if ($href && substr($href, 0, strlen($hashtag_prefix)) === $hashtag_prefix) { - $hashtag = urldecode(substr($href, strlen($hashtag_prefix))); - } else if (substr($innertext, 0, 1) == '#') { - $hashtag = $innertext; - } - - if ($hashtag) { - $a->outertext = $innertext; - $hashtags[] = $hashtag; - continue; - } - - $parsed_url = parse_url($href); - - if (array_key_exists('path', $parsed_url) === false) continue; - - if (strpos($parsed_url['path'], '/away.php') === 0) { - parse_str($parsed_url['query'], $parsed_query); - $a->setAttribute('href', iconv( - 'windows-1251', - 'utf-8//ignore', - $parsed_query['to'] - )); - } - } - - $copy_quote = $post->find('div.copy_quote', 0); - if (is_object($copy_quote)) { - if ($this->getInput('hide_reposts') === true) { - continue; - } - if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) { - $copy_post_header->outertext = ''; - } - - $second_copy_quote = $copy_quote->find('div.published_sec_quote', 0); - if (is_object($second_copy_quote)) { - $second_copy_quote_author = $second_copy_quote->find('a.copy_author', 0)->outertext; - $second_copy_quote_content = $second_copy_quote->find('div.copy_post_date', 0)->outertext; - $second_copy_quote->outertext = "<br>Reposted ($second_copy_quote_author): $second_copy_quote_content"; - } - $copy_quote_author = $copy_quote->find('a.copy_author', 0)->outertext; - $copy_quote_content = $copy_quote->innertext; - $copy_quote->outertext = "<br>Reposted ($copy_quote_author): <br>$copy_quote_content"; - } - - $item = array(); - $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<a><br><img>'); - $item['content'] .= $content_suffix; - $item['categories'] = $hashtags; - - // get post link - $post_link = $post->find('a.post_link', 0)->getAttribute('href'); - preg_match('/wall-?\d+_(\d+)/', $post_link, $preg_match_result); - $item['post_id'] = intval($preg_match_result[1]); - $item['uri'] = $post_link; - $item['timestamp'] = $this->getTime($post); - $item['title'] = $this->getTitle($item['content']); - $item['author'] = $post_author; - $item['videos'] = $post_videos; - if ($is_pinned_post) { - // do not append it now - $pinned_post_item = $item; - } else { - $last_post_id = $item['post_id']; - $this->items[] = $item; - } - - } - - if (!is_null($pinned_post_item)) { - if (count($this->items) == 0) { - $this->items[] = $pinned_post_item; - } else if ($last_post_id < $pinned_post_item['post_id']) { - $this->items[] = $pinned_post_item; - usort($this->items, function ($item1, $item2) { - return $item2['post_id'] - $item1['post_id']; - }); - } - } - - $this->getCleanVideoLinks(); - } - - private function getPhoto($a) { - $onclick = $a->getAttribute('onclick'); - preg_match('/return showPhoto\(.+?({.*})/', $onclick, $preg_match_result); - if (count($preg_match_result) == 0) return; - - $arg = htmlspecialchars_decode( str_replace('queue:1', '"queue":1', $preg_match_result[1]) ); - $data = json_decode($arg, true); - if ($data == null) return; - - $thumb = $data['temp']['base'] . $data['temp']['x_'][0]; - $original = ''; - foreach(array('y_', 'z_', 'w_') as $key) { - if (!isset($data['temp'][$key])) continue; - if (!isset($data['temp'][$key][0])) continue; - if (substr($data['temp'][$key][0], 0, 4) == 'http') { - $base = ''; - } else { - $base = $data['temp']['base']; - } - $original = $base . $data['temp'][$key][0]; - } - - if ($original) { - return "<a href='$original'><img src='$thumb'></a>"; - } else { - return "<img src='$thumb'>"; - } - } - - private function getTitle($content) - { - preg_match('/^["\w\ \p{L}\(\)\?#«»-]+/mu', htmlspecialchars_decode($content), $result); - if (count($result) == 0) return 'untitled'; - return $result[0]; - } - - private function getTime($post) - { - if ($time = $post->find('span.rel_date', 0)->getAttribute('time')) { - return $time; - } else { - $strdate = $post->find('span.rel_date', 0)->plaintext; - $strdate = preg_replace('/[\x00-\x1F\x7F-\xFF]/', ' ', $strdate); - - $date = date_parse($strdate); - if (!$date['year']) { - if (strstr($strdate, 'today') !== false) { - $strdate = date('d-m-Y') . ' ' . $strdate; - } elseif (strstr($strdate, 'yesterday ') !== false) { - $time = time() - 60 * 60 * 24; - $strdate = date('d-m-Y', $time) . ' ' . $strdate; - } elseif ($date['month'] && intval(date('m')) < $date['month']) { - $strdate = $strdate . ' ' . (date('Y') - 1); - } else { - $strdate = $strdate . ' ' . date('Y'); - } - - $date = date_parse($strdate); - } elseif ($date['hour'] === false) { - $date['hour'] = $date['minute'] = '00'; - } - return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' . - $date['hour'] . ':' . $date['minute']); - } - - } - - private function getContents() - { - $header = array('Accept-language: en', 'Cookie: remixlang=3'); - - return getContents($this->getURI(), $header); - } - - protected function appendVideo($video_title, $video_link, &$content_suffix, array &$post_videos) - { - if (!$video_title) $video_title = '(empty)'; - - preg_match('/video([0-9-]+_[0-9]+)/', $video_link, $preg_match_result); - - if (count($preg_match_result) > 1) { - $video_id = $preg_match_result[1]; - $this->videos[ $video_id ] = array( - 'url' => $video_link, - 'title' => $video_title, - ); - $post_videos[] = $video_id; - } else { - $content_suffix .= '<br>Video: <a href="' . htmlspecialchars($video_link) . '">' . $video_title . '</a>'; - } - } - - protected function getCleanVideoLinks() { - $result = $this->api('video.get', array( - 'videos' => implode(',', array_keys($this->videos)), - 'count' => 200 - )); - - if (!isset($result['error'])) { - foreach($result['response']['items'] as $item) { - $video_id = strval($item['owner_id']) . '_' . strval($item['id']); - $this->videos[$video_id]['url'] = $item['player']; - } - } - - foreach($this->items as &$item) { - foreach($item['videos'] as $video_id) { - $video_link = $this->videos[$video_id]['url']; - $video_title = $this->videos[$video_id]['title']; - $item['content'] .= '<br>Video: <a href="' . htmlspecialchars($video_link) . '">' . $video_title . '</a>'; - } - unset($item['videos']); - } - } - - protected function api($method, array $params) - { - $params['v'] = '5.80'; - $params['access_token'] = $this->getAccessToken(); - return json_decode( getContents('https://api.vk.com/method/' . $method . '?' . http_build_query($params)), true ); - } + $article_thumb_selector = 'div.article_snippet_mini_thumb'; + } else { + $article_title_selector = 'div.article_snippet__title'; + $article_author_selector = 'div.article_snippet__author'; + $article_thumb_selector = 'div.article_snippet__image'; + } + $article_title = $article->find($article_title_selector, 0)->innertext; + $article_author = $article->find($article_author_selector, 0)->innertext; + $article_link = $article->getAttribute('href'); + $article_img_element_style = $article->find($article_thumb_selector, 0)->getAttribute('style'); + preg_match('/background-image: url\((.*)\)/', $article_img_element_style, $matches); + if (count($matches) > 0) { + $content_suffix .= "<br><img src='" . $matches[1] . "'>"; + } + $content_suffix .= "<br>Article: <a href='$article_link'>$article_title ($article_author)</a>"; + $article->outertext = ''; + } + + // get video on post + $video = $post->find('div.post_video_desc', 0); + $main_video_link = ''; + if (is_object($video)) { + $video_title = $video->find('div.post_video_title', 0)->plaintext; + $video_link = $video->find('a.lnk', 0)->getAttribute('href'); + $this->appendVideo($video_title, $video_link, $content_suffix, $post_videos); + $video->outertext = ''; + $main_video_link = $video_link; + } + + // get all other videos + foreach ($post->find('a.page_post_thumb_video') as $a) { + $video_title = htmlspecialchars_decode($a->getAttribute('aria-label')); + $video_link = $a->getAttribute('href'); + if ($video_link != $main_video_link) { + $this->appendVideo($video_title, $video_link, $content_suffix, $post_videos); + } + $a->outertext = ''; + } + + // get all photos + foreach ($post->find('div.wall_text a.page_post_thumb_wrap') as $a) { + $result = $this->getPhoto($a); + if ($result == null) { + continue; + } + $a->outertext = ''; + $content_suffix .= "<br>$result"; + } + + // get albums + foreach ($post->find('.page_album_wrap') as $el) { + $a = $el->find('.page_album_link', 0); + $album_title = $a->find('.page_album_title_text', 0)->getAttribute('title'); + $album_link = $a->getAttribute('href'); + $el->outertext = ''; + $content_suffix .= "<br>Album: <a href='$album_link'>$album_title</a>"; + } + + // get photo documents + foreach ($post->find('a.page_doc_photo_href') as $a) { + $doc_link = $a->getAttribute('href'); + $doc_gif_label_element = $a->find('.page_gif_label', 0); + $doc_title_element = $a->find('.doc_label', 0); + + if (is_object($doc_gif_label_element)) { + $gif_preview_img = backgroundToImg($a->find('.page_doc_photo', 0)); + $content_suffix .= "<br>Gif: <a href='$doc_link'>$gif_preview_img</a>"; + } elseif (is_object($doc_title_element)) { + $doc_title = $doc_title_element->innertext; + $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>"; + } else { + continue; + } + + $a->outertext = ''; + } + + // get other documents + foreach ($post->find('div.page_doc_row') as $div) { + $doc_title_element = $div->find('a.page_doc_title', 0); + + if (is_object($doc_title_element)) { + $doc_title = $doc_title_element->innertext; + $doc_link = $doc_title_element->getAttribute('href'); + $content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>"; + } else { + continue; + } + + $div->outertext = ''; + } + + // get polls + foreach ($post->find('div.page_media_poll_wrap') as $div) { + $poll_title = $div->find('.page_media_poll_title', 0)->innertext; + $content_suffix .= "<br>Poll: $poll_title"; + foreach ($div->find('div.page_poll_text') as $poll_stat_title) { + $content_suffix .= '<br>- ' . $poll_stat_title->innertext; + } + $div->outertext = ''; + } + + // get sign / post author + $post_author = $pageName; + $author_selectors = ['a.wall_signed_by', 'a.author']; + foreach ($author_selectors as $author_selector) { + $a = $post->find($author_selector, 0); + if (is_object($a)) { + $post_author = $a->innertext; + $a->outertext = ''; + break; + } + } + + // fix links and get post hashtags + $hashtags = []; + foreach ($post->find('a') as $a) { + $href = $a->getAttribute('href'); + $innertext = $a->innertext; + + $hashtag_prefix = '/feed?section=search&q=%23'; + $hashtag = null; + + if ($href && substr($href, 0, strlen($hashtag_prefix)) === $hashtag_prefix) { + $hashtag = urldecode(substr($href, strlen($hashtag_prefix))); + } elseif (substr($innertext, 0, 1) == '#') { + $hashtag = $innertext; + } + + if ($hashtag) { + $a->outertext = $innertext; + $hashtags[] = $hashtag; + continue; + } + + $parsed_url = parse_url($href); + + if (array_key_exists('path', $parsed_url) === false) { + continue; + } + + if (strpos($parsed_url['path'], '/away.php') === 0) { + parse_str($parsed_url['query'], $parsed_query); + $a->setAttribute('href', iconv( + 'windows-1251', + 'utf-8//ignore', + $parsed_query['to'] + )); + } + } + + $copy_quote = $post->find('div.copy_quote', 0); + if (is_object($copy_quote)) { + if ($this->getInput('hide_reposts') === true) { + continue; + } + if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) { + $copy_post_header->outertext = ''; + } + + $second_copy_quote = $copy_quote->find('div.published_sec_quote', 0); + if (is_object($second_copy_quote)) { + $second_copy_quote_author = $second_copy_quote->find('a.copy_author', 0)->outertext; + $second_copy_quote_content = $second_copy_quote->find('div.copy_post_date', 0)->outertext; + $second_copy_quote->outertext = "<br>Reposted ($second_copy_quote_author): $second_copy_quote_content"; + } + $copy_quote_author = $copy_quote->find('a.copy_author', 0)->outertext; + $copy_quote_content = $copy_quote->innertext; + $copy_quote->outertext = "<br>Reposted ($copy_quote_author): <br>$copy_quote_content"; + } + + $item = []; + $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<a><br><img>'); + $item['content'] .= $content_suffix; + $item['categories'] = $hashtags; + + // get post link + $post_link = $post->find('a.post_link', 0)->getAttribute('href'); + preg_match('/wall-?\d+_(\d+)/', $post_link, $preg_match_result); + $item['post_id'] = intval($preg_match_result[1]); + $item['uri'] = $post_link; + $item['timestamp'] = $this->getTime($post); + $item['title'] = $this->getTitle($item['content']); + $item['author'] = $post_author; + $item['videos'] = $post_videos; + if ($is_pinned_post) { + // do not append it now + $pinned_post_item = $item; + } else { + $last_post_id = $item['post_id']; + $this->items[] = $item; + } + } + + if (!is_null($pinned_post_item)) { + if (count($this->items) == 0) { + $this->items[] = $pinned_post_item; + } elseif ($last_post_id < $pinned_post_item['post_id']) { + $this->items[] = $pinned_post_item; + usort($this->items, function ($item1, $item2) { + return $item2['post_id'] - $item1['post_id']; + }); + } + } + + $this->getCleanVideoLinks(); + } + + private function getPhoto($a) + { + $onclick = $a->getAttribute('onclick'); + preg_match('/return showPhoto\(.+?({.*})/', $onclick, $preg_match_result); + if (count($preg_match_result) == 0) { + return; + } + + $arg = htmlspecialchars_decode(str_replace('queue:1', '"queue":1', $preg_match_result[1])); + $data = json_decode($arg, true); + if ($data == null) { + return; + } + + $thumb = $data['temp']['base'] . $data['temp']['x_'][0]; + $original = ''; + foreach (['y_', 'z_', 'w_'] as $key) { + if (!isset($data['temp'][$key])) { + continue; + } + if (!isset($data['temp'][$key][0])) { + continue; + } + if (substr($data['temp'][$key][0], 0, 4) == 'http') { + $base = ''; + } else { + $base = $data['temp']['base']; + } + $original = $base . $data['temp'][$key][0]; + } + + if ($original) { + return "<a href='$original'><img src='$thumb'></a>"; + } else { + return "<img src='$thumb'>"; + } + } + + private function getTitle($content) + { + preg_match('/^["\w\ \p{L}\(\)\?#«»-]+/mu', htmlspecialchars_decode($content), $result); + if (count($result) == 0) { + return 'untitled'; + } + return $result[0]; + } + + private function getTime($post) + { + if ($time = $post->find('span.rel_date', 0)->getAttribute('time')) { + return $time; + } else { + $strdate = $post->find('span.rel_date', 0)->plaintext; + $strdate = preg_replace('/[\x00-\x1F\x7F-\xFF]/', ' ', $strdate); + + $date = date_parse($strdate); + if (!$date['year']) { + if (strstr($strdate, 'today') !== false) { + $strdate = date('d-m-Y') . ' ' . $strdate; + } elseif (strstr($strdate, 'yesterday ') !== false) { + $time = time() - 60 * 60 * 24; + $strdate = date('d-m-Y', $time) . ' ' . $strdate; + } elseif ($date['month'] && intval(date('m')) < $date['month']) { + $strdate = $strdate . ' ' . (date('Y') - 1); + } else { + $strdate = $strdate . ' ' . date('Y'); + } + + $date = date_parse($strdate); + } elseif ($date['hour'] === false) { + $date['hour'] = $date['minute'] = '00'; + } + return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' . + $date['hour'] . ':' . $date['minute']); + } + } + + private function getContents() + { + $header = ['Accept-language: en', 'Cookie: remixlang=3']; + + return getContents($this->getURI(), $header); + } + + protected function appendVideo($video_title, $video_link, &$content_suffix, array &$post_videos) + { + if (!$video_title) { + $video_title = '(empty)'; + } + + preg_match('/video([0-9-]+_[0-9]+)/', $video_link, $preg_match_result); + + if (count($preg_match_result) > 1) { + $video_id = $preg_match_result[1]; + $this->videos[ $video_id ] = [ + 'url' => $video_link, + 'title' => $video_title, + ]; + $post_videos[] = $video_id; + } else { + $content_suffix .= '<br>Video: <a href="' . htmlspecialchars($video_link) . '">' . $video_title . '</a>'; + } + } + + protected function getCleanVideoLinks() + { + $result = $this->api('video.get', [ + 'videos' => implode(',', array_keys($this->videos)), + 'count' => 200 + ]); + + if (!isset($result['error'])) { + foreach ($result['response']['items'] as $item) { + $video_id = strval($item['owner_id']) . '_' . strval($item['id']); + $this->videos[$video_id]['url'] = $item['player']; + } + } + + foreach ($this->items as &$item) { + foreach ($item['videos'] as $video_id) { + $video_link = $this->videos[$video_id]['url']; + $video_title = $this->videos[$video_id]['title']; + $item['content'] .= '<br>Video: <a href="' . htmlspecialchars($video_link) . '">' . $video_title . '</a>'; + } + unset($item['videos']); + } + } + + protected function api($method, array $params) + { + $params['v'] = '5.80'; + $params['access_token'] = $this->getAccessToken(); + return json_decode(getContents('https://api.vk.com/method/' . $method . '?' . http_build_query($params)), true); + } } diff --git a/bridges/WallmineNewsBridge.php b/bridges/WallmineNewsBridge.php index f21627a0..c5009172 100644 --- a/bridges/WallmineNewsBridge.php +++ b/bridges/WallmineNewsBridge.php @@ -1,48 +1,50 @@ <?php -class WallmineNewsBridge extends BridgeAbstract { - const NAME = 'Wallmine News Bridge'; - const URI = 'https://wallmine.com'; - const DESCRIPTION = 'Returns financial news'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array(); - const CACHE_TIMEOUT = 900; // 15 mins +class WallmineNewsBridge extends BridgeAbstract +{ + const NAME = 'Wallmine News Bridge'; + const URI = 'https://wallmine.com'; + const DESCRIPTION = 'Returns financial news'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = []; - public function collectData() { - $html = getSimpleHTMLDOM($this->getURI() . '/news/'); + const CACHE_TIMEOUT = 900; // 15 mins - $html = defaultLinkTo($html, self::URI); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI() . '/news/'); - foreach($html->find('div.container.news-card') as $div) { - $item = array(); - $item['uri'] = $div->find('a', 0)->href; + $html = defaultLinkTo($html, self::URI); - $image = $div->find('img.img-fluid', 0)->src; + foreach ($html->find('div.container.news-card') as $div) { + $item = []; + $item['uri'] = $div->find('a', 0)->href; - $page = getSimpleHTMLDOMCached($item['uri'], 7200); + $image = $div->find('img.img-fluid', 0)->src; - $article = $page->find('div.container.article-container', 0); + $page = getSimpleHTMLDOMCached($item['uri'], 7200); - $item['title'] = $article->find('h1', 0)->plaintext; + $article = $page->find('div.container.article-container', 0); - $article->find('p.published-on', 0)->children(0)->outertext = ''; - $article->find('p.published-on', 0)->children(1)->outertext = ''; - $date = str_replace('at', '', $article->find('p.published-on', 0)->innertext); + $item['title'] = $article->find('h1', 0)->plaintext; - $item['timestamp'] = $date; + $article->find('p.published-on', 0)->children(0)->outertext = ''; + $article->find('p.published-on', 0)->children(1)->outertext = ''; + $date = str_replace('at', '', $article->find('p.published-on', 0)->innertext); - $article->find('h1', 0)->outertext = ''; - $article->find('p.published-on', 0)->outertext = ''; + $item['timestamp'] = $date; - $item['content'] = $article->innertext; - $item['enclosures'][] = $image; + $article->find('h1', 0)->outertext = ''; + $article->find('p.published-on', 0)->outertext = ''; - $this->items[] = $item; + $item['content'] = $article->innertext; + $item['enclosures'][] = $image; - if (count($this->items) >= 10) { - break; - } - } + $this->items[] = $item; - } + if (count($this->items) >= 10) { + break; + } + } + } } diff --git a/bridges/WallpaperflareBridge.php b/bridges/WallpaperflareBridge.php index 60486368..907288d0 100644 --- a/bridges/WallpaperflareBridge.php +++ b/bridges/WallpaperflareBridge.php @@ -1,41 +1,46 @@ <?php -class WallpaperflareBridge extends XPathAbstract { - const NAME = 'Wallpaperflare'; - const URI = 'https://wallpaperflare.com'; - const DESCRIPTION = 'Wallpaperflare is a provider for Wallpapers on nearly every topic, especially for Anime'; - const MAINTAINER = 'dhuschde'; - const PARAMETERS = array( - '' => array( - 'search' => array( - 'name' => 'Search', - 'exampleValue' => 'birds', - 'required' => true - ) - )); - const CACHE_TIMEOUT = 3600; //1 hour - const XPATH_EXPRESSION_ITEM = './/figure'; - const XPATH_EXPRESSION_ITEM_TITLE = './/img/@title'; - const XPATH_EXPRESSION_ITEM_CONTENT = ''; - const XPATH_EXPRESSION_ITEM_URI = './/a[@itemprop="url"]/@href'; - const XPATH_EXPRESSION_ITEM_AUTHOR = '/html[1]/body[1]/main[1]/section[1]/h1[1]'; - const XPATH_EXPRESSION_ITEM_TIMESTAMP = ''; - const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@data-src'; - const XPATH_EXPRESSION_ITEM_CATEGORIES = './/figcaption[@itemprop="caption description"]'; - const SETTING_FIX_ENCODING = false; - protected function getSourceUrl(){ - return 'https://www.wallpaperflare.com/search?wallpaper=' . $this->getInput('search'); - } +class WallpaperflareBridge extends XPathAbstract +{ + const NAME = 'Wallpaperflare'; + const URI = 'https://wallpaperflare.com'; + const DESCRIPTION = 'Wallpaperflare is a provider for Wallpapers on nearly every topic, especially for Anime'; + const MAINTAINER = 'dhuschde'; + const PARAMETERS = [ + '' => [ + 'search' => [ + 'name' => 'Search', + 'exampleValue' => 'birds', + 'required' => true + ] + ]]; + const CACHE_TIMEOUT = 3600; //1 hour + const XPATH_EXPRESSION_ITEM = './/figure'; + const XPATH_EXPRESSION_ITEM_TITLE = './/img/@title'; + const XPATH_EXPRESSION_ITEM_CONTENT = ''; + const XPATH_EXPRESSION_ITEM_URI = './/a[@itemprop="url"]/@href'; + const XPATH_EXPRESSION_ITEM_AUTHOR = '/html[1]/body[1]/main[1]/section[1]/h1[1]'; + const XPATH_EXPRESSION_ITEM_TIMESTAMP = ''; + const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@data-src'; + const XPATH_EXPRESSION_ITEM_CATEGORIES = './/figcaption[@itemprop="caption description"]'; + const SETTING_FIX_ENCODING = false; - public function getIcon() { - return 'https://www.google.com/s2/favicons?domain=wallpaperflare.com/'; - } + protected function getSourceUrl() + { + return 'https://www.wallpaperflare.com/search?wallpaper=' . $this->getInput('search'); + } - public function getName() { - if(!is_null($this->getInput('search'))) { - return 'Wallpaperflare - ' . $this->getInput('search'); - } else { - return 'Wallpaperflare'; - } - } + public function getIcon() + { + return 'https://www.google.com/s2/favicons?domain=wallpaperflare.com/'; + } + + public function getName() + { + if (!is_null($this->getInput('search'))) { + return 'Wallpaperflare - ' . $this->getInput('search'); + } else { + return 'Wallpaperflare'; + } + } } diff --git a/bridges/WeLiveSecurityBridge.php b/bridges/WeLiveSecurityBridge.php index 14af1ab3..6434a13a 100644 --- a/bridges/WeLiveSecurityBridge.php +++ b/bridges/WeLiveSecurityBridge.php @@ -1,38 +1,41 @@ <?php -class WeLiveSecurityBridge extends FeedExpander { - const MAINTAINER = 'ORelio'; - const NAME = 'We Live Security'; - const URI = 'https://www.welivesecurity.com/'; - const DESCRIPTION = 'Returns the newest articles.'; - const PARAMETERS = [ - [ - 'limit' => self::LIMIT, - ], - ]; +class WeLiveSecurityBridge extends FeedExpander +{ + const MAINTAINER = 'ORelio'; + const NAME = 'We Live Security'; + const URI = 'https://www.welivesecurity.com/'; + const DESCRIPTION = 'Returns the newest articles.'; + const PARAMETERS = [ + [ + 'limit' => self::LIMIT, + ], + ]; - protected function parseItem($item){ - $item = parent::parseItem($item); + protected function parseItem($item) + { + $item = parent::parseItem($item); - $article_html = getSimpleHTMLDOMCached($item['uri']); - if(!$article_html) { - $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; - return $item; - } + $article_html = getSimpleHTMLDOMCached($item['uri']); + if (!$article_html) { + $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; + return $item; + } - $article_content = $article_html->find('div.formatted', 0)->innertext; - $article_content = stripWithDelimiters($article_content, '<script', '</script>'); - $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="comments'); - $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="similar-articles'); - $article_content = stripRecursiveHTMLSection($article_content, 'span', '<span class="meta'); - $item['content'] = trim($article_content); + $article_content = $article_html->find('div.formatted', 0)->innertext; + $article_content = stripWithDelimiters($article_content, '<script', '</script>'); + $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="comments'); + $article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="similar-articles'); + $article_content = stripRecursiveHTMLSection($article_content, 'span', '<span class="meta'); + $item['content'] = trim($article_content); - return $item; - } + return $item; + } - public function collectData(){ - $feed = static::URI . 'feed/'; - $limit = $this->getInput('limit') ?? 10; - $this->collectExpandableDatas($feed, $limit); - } + public function collectData() + { + $feed = static::URI . 'feed/'; + $limit = $this->getInput('limit') ?? 10; + $this->collectExpandableDatas($feed, $limit); + } } diff --git a/bridges/WebfailBridge.php b/bridges/WebfailBridge.php index fefd539a..e55988da 100644 --- a/bridges/WebfailBridge.php +++ b/bridges/WebfailBridge.php @@ -1,156 +1,169 @@ <?php -class WebfailBridge extends BridgeAbstract { - const MAINTAINER = 'logmanoriginal'; - const URI = 'https://webfail.com'; - const NAME = 'Webfail'; - const DESCRIPTION = 'Returns the latest fails'; - const PARAMETERS = array( - 'By content type' => array( - 'language' => array( - 'name' => 'Language', - 'type' => 'list', - 'title' => 'Select your language', - 'values' => array( - 'English' => 'en', - 'German' => 'de' - ), - 'defaultValue' => 'English' - ), - 'type' => array( - 'name' => 'Type', - 'type' => 'list', - 'title' => 'Select your content type', - 'values' => array( - 'None' => '/', - 'Facebook' => '/ffdts', - 'Images' => '/images', - 'Videos' => '/videos', - 'Gifs' => '/gifs' - ), - 'defaultValue' => 'None' - ) - ) - ); - - public function getURI(){ - if(is_null($this->getInput('language'))) - return parent::getURI(); - - // e.g.: https://en.webfail.com - return 'https://' . $this->getInput('language') . '.webfail.com'; - } - - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI() . $this->getInput('type')); - - $type = array_search($this->getInput('type'), - self::PARAMETERS[$this->queriedContext]['type']['values']); - - switch(strtolower($type)) { - case 'facebook': - case 'videos': - $this->extractNews($html, $type); - break; - case 'none': - case 'images': - case 'gifs': - $this->extractArticle($html); - break; - default: returnClientError('Unknown type: ' . $type); - } - } - - private function extractNews($html, $type){ - $news = $html->find('#main', 0)->find('a.wf-list-news'); - foreach($news as $element) { - $item = array(); - $item['title'] = $this->fixTitle($element->find('div.wf-news-title', 0)->innertext); - $item['uri'] = $this->getURI() . $element->href; - - $img = $element->find('img.wf-image', 0)->src; - // Load high resolution image for 'facebook' - switch(strtolower($type)) { - case 'facebook': - $img = $this->getImageHiResUri($item['uri']); - break; - default: - } - - $description = ''; - if(!is_null($element->find('div.wf-news-description', 0))) { - $description = $element->find('div.wf-news-description', 0)->innertext; - } - - $infoElement = $element->find('div.wf-small', 0); - if (!is_null($infoElement)) { - if (preg_match('/(\d{2}\.\d{2}\.\d{4})/m', $infoElement->innertext, $matches) === 1 && count($matches) == 2) { - $dt = DateTime::createFromFormat('!d.m.Y', $matches[1]); - if ($dt !== false) { - $item['timestamp'] = $dt->getTimestamp(); - } - } - } - - $item['content'] = '<p>' - . $description - . '</p><br><a href="' - . $item['uri'] - . '"><img src="' - . $img - . '"></a>'; - - $this->items[] = $item; - } - } - - private function extractArticle($html){ - $articles = $html->find('article'); - foreach($articles as $article) { - $item = array(); - $item['title'] = $this->fixTitle($article->find('a', 1)->innertext); - - // Images, videos and gifs are provided in their own unique way - if(!is_null($article->find('img.wf-image', 0))) { // Image type - $item['uri'] = $this->getURI() . $article->find('a', 2)->href; - $item['content'] = '<a href="' - . $item['uri'] - . '"><img src="' - . $article->find('img.wf-image', 0)->src - . '"></a>'; - } elseif(!is_null($article->find('div.wf-video', 0))) { // Video type - $videoId = $this->getVideoId($article->find('div.wf-play', 0)->onclick); - $item['uri'] = 'https://youtube.com/watch?v=' . $videoId; - $item['content'] = '<a href="' - . $item['uri'] - . '"><img src="http://img.youtube.com/vi/' - . $videoId - . '/0.jpg"></a>'; - } elseif(!is_null($article->find('video[id*=gif-]', 0))) { // Gif type - $item['uri'] = $this->getURI() . $article->find('a', 2)->href; - $item['content'] = '<video controls src="' - . $article->find('video[id*=gif-]', 0)->src - . '" poster="' - . $article->find('video[id*=gif-]', 0)->poster - . '"></video>'; - } - - $this->items[] = $item; - } - } - - private function fixTitle($title){ - // This fixes titles that include umlauts (in German language) - return html_entity_decode($title, ENT_QUOTES | ENT_HTML401, 'UTF-8'); - } - - private function getVideoId($onclick){ - return substr($onclick, 21, 11); - } - - private function getImageHiResUri($url){ - // https://de.webfail.com/ef524fae509?tag=ffdt - // http://cdn.webfail.com/upl/img/ef524fae509/post2.jpg - $id = substr($url, strrpos($url, '/') + 1, strlen($url) - strrpos($url, '?') + 2); - return 'http://cdn.webfail.com/upl/img/' . $id . '/post2.jpg'; - } + +class WebfailBridge extends BridgeAbstract +{ + const MAINTAINER = 'logmanoriginal'; + const URI = 'https://webfail.com'; + const NAME = 'Webfail'; + const DESCRIPTION = 'Returns the latest fails'; + const PARAMETERS = [ + 'By content type' => [ + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'title' => 'Select your language', + 'values' => [ + 'English' => 'en', + 'German' => 'de' + ], + 'defaultValue' => 'English' + ], + 'type' => [ + 'name' => 'Type', + 'type' => 'list', + 'title' => 'Select your content type', + 'values' => [ + 'None' => '/', + 'Facebook' => '/ffdts', + 'Images' => '/images', + 'Videos' => '/videos', + 'Gifs' => '/gifs' + ], + 'defaultValue' => 'None' + ] + ] + ]; + + public function getURI() + { + if (is_null($this->getInput('language'))) { + return parent::getURI(); + } + + // e.g.: https://en.webfail.com + return 'https://' . $this->getInput('language') . '.webfail.com'; + } + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI() . $this->getInput('type')); + + $type = array_search( + $this->getInput('type'), + self::PARAMETERS[$this->queriedContext]['type']['values'] + ); + + switch (strtolower($type)) { + case 'facebook': + case 'videos': + $this->extractNews($html, $type); + break; + case 'none': + case 'images': + case 'gifs': + $this->extractArticle($html); + break; + default: + returnClientError('Unknown type: ' . $type); + } + } + + private function extractNews($html, $type) + { + $news = $html->find('#main', 0)->find('a.wf-list-news'); + foreach ($news as $element) { + $item = []; + $item['title'] = $this->fixTitle($element->find('div.wf-news-title', 0)->innertext); + $item['uri'] = $this->getURI() . $element->href; + + $img = $element->find('img.wf-image', 0)->src; + // Load high resolution image for 'facebook' + switch (strtolower($type)) { + case 'facebook': + $img = $this->getImageHiResUri($item['uri']); + break; + default: + } + + $description = ''; + if (!is_null($element->find('div.wf-news-description', 0))) { + $description = $element->find('div.wf-news-description', 0)->innertext; + } + + $infoElement = $element->find('div.wf-small', 0); + if (!is_null($infoElement)) { + if (preg_match('/(\d{2}\.\d{2}\.\d{4})/m', $infoElement->innertext, $matches) === 1 && count($matches) == 2) { + $dt = DateTime::createFromFormat('!d.m.Y', $matches[1]); + if ($dt !== false) { + $item['timestamp'] = $dt->getTimestamp(); + } + } + } + + $item['content'] = '<p>' + . $description + . '</p><br><a href="' + . $item['uri'] + . '"><img src="' + . $img + . '"></a>'; + + $this->items[] = $item; + } + } + + private function extractArticle($html) + { + $articles = $html->find('article'); + foreach ($articles as $article) { + $item = []; + $item['title'] = $this->fixTitle($article->find('a', 1)->innertext); + + // Images, videos and gifs are provided in their own unique way + if (!is_null($article->find('img.wf-image', 0))) { // Image type + $item['uri'] = $this->getURI() . $article->find('a', 2)->href; + $item['content'] = '<a href="' + . $item['uri'] + . '"><img src="' + . $article->find('img.wf-image', 0)->src + . '"></a>'; + } elseif (!is_null($article->find('div.wf-video', 0))) { // Video type + $videoId = $this->getVideoId($article->find('div.wf-play', 0)->onclick); + $item['uri'] = 'https://youtube.com/watch?v=' . $videoId; + $item['content'] = '<a href="' + . $item['uri'] + . '"><img src="http://img.youtube.com/vi/' + . $videoId + . '/0.jpg"></a>'; + } elseif (!is_null($article->find('video[id*=gif-]', 0))) { // Gif type + $item['uri'] = $this->getURI() . $article->find('a', 2)->href; + $item['content'] = '<video controls src="' + . $article->find('video[id*=gif-]', 0)->src + . '" poster="' + . $article->find('video[id*=gif-]', 0)->poster + . '"></video>'; + } + + $this->items[] = $item; + } + } + + private function fixTitle($title) + { + // This fixes titles that include umlauts (in German language) + return html_entity_decode($title, ENT_QUOTES | ENT_HTML401, 'UTF-8'); + } + + private function getVideoId($onclick) + { + return substr($onclick, 21, 11); + } + + private function getImageHiResUri($url) + { + // https://de.webfail.com/ef524fae509?tag=ffdt + // http://cdn.webfail.com/upl/img/ef524fae509/post2.jpg + $id = substr($url, strrpos($url, '/') + 1, strlen($url) - strrpos($url, '?') + 2); + return 'http://cdn.webfail.com/upl/img/' . $id . '/post2.jpg'; + } } diff --git a/bridges/WikiLeaksBridge.php b/bridges/WikiLeaksBridge.php index cf44b066..512b1c30 100644 --- a/bridges/WikiLeaksBridge.php +++ b/bridges/WikiLeaksBridge.php @@ -1,127 +1,134 @@ <?php -class WikiLeaksBridge extends BridgeAbstract { - const NAME = 'WikiLeaks'; - const URI = 'https://wikileaks.org'; - const DESCRIPTION = 'Returns the latest news or articles from WikiLeaks'; - const MAINTAINER = 'logmanoriginal'; - const PARAMETERS = array( - array( - 'category' => array( - 'name' => 'Category', - 'type' => 'list', - 'title' => 'Select your category', - 'values' => array( - 'News' => '-News-', - 'Leaks' => array( - 'All' => '-Leaks-', - 'Intelligence' => '+-Intelligence-+', - 'Global Economy' => '+-Global-Economy-+', - 'International Politics' => '+-International-Politics-+', - 'Corporations' => '+-Corporations-+', - 'Government' => '+-Government-+', - 'War & Military' => '+-War-Military-+' - ) - ), - 'defaultValue' => 'news' - ), - 'teaser' => array( - 'name' => 'Show teaser', - 'type' => 'checkbox', - 'title' => 'If checked feeds will display the teaser', - 'defaultValue' => 'checked' - ) - ) - ); - - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); - - // News are presented differently - switch($this->getInput('category')) { - case '-News-': - $this->loadNewsItems($html); - break; - default: - $this->loadLeakItems($html); - } - } - - public function getURI(){ - if(!is_null($this->getInput('category'))) { - return static::URI . '/' . $this->getInput('category') . '.html'; - } - - return parent::getURI(); - } - - public function getName(){ - if(!is_null($this->getInput('category'))) { - $category = array_search( - $this->getInput('category'), - static::PARAMETERS[0]['category']['values'] - ); - - if($category === false) { - $category = array_search( - $this->getInput('category'), - static::PARAMETERS[0]['category']['values']['Leaks'] - ); - } - - return $category . ' - ' . static::NAME; - } - - return parent::getName(); - } - - private function loadNewsItems($html){ - $articles = $html->find('div.news-articles ul li'); - - if(is_null($articles) || count($articles) === 0) { - return; - } - - foreach($articles as $article) { - $item = array(); - - $item['title'] = $article->find('h3', 0)->plaintext; - $item['uri'] = static::URI . $article->find('h3 a', 0)->href; - $item['content'] = $article->find('div.introduction', 0)->plaintext; - $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext); - - $this->items[] = $item; - } - } - - private function loadLeakItems($html){ - $articles = $html->find('li.tile'); - - if(is_null($articles) || count($articles) === 0) { - return; - } - - foreach($articles as $article) { - $item = array(); - - $item['title'] = $article->find('h2', 0)->plaintext; - $item['uri'] = static::URI . $article->find('a', 0)->href; - - $teaser = static::URI . '/' . $article->find('div.teaser img', 0)->src; - - if($this->getInput('teaser')) { - $item['content'] = '<img src="' - . $teaser - . '" /><p>' - . $article->find('div.intro', 0)->plaintext - . '</p>'; - } else { - $item['content'] = $article->find('div.intro', 0)->plaintext; - } - - $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext); - $item['enclosures'] = array($teaser); - - $this->items[] = $item; - } - } + +class WikiLeaksBridge extends BridgeAbstract +{ + const NAME = 'WikiLeaks'; + const URI = 'https://wikileaks.org'; + const DESCRIPTION = 'Returns the latest news or articles from WikiLeaks'; + const MAINTAINER = 'logmanoriginal'; + const PARAMETERS = [ + [ + 'category' => [ + 'name' => 'Category', + 'type' => 'list', + 'title' => 'Select your category', + 'values' => [ + 'News' => '-News-', + 'Leaks' => [ + 'All' => '-Leaks-', + 'Intelligence' => '+-Intelligence-+', + 'Global Economy' => '+-Global-Economy-+', + 'International Politics' => '+-International-Politics-+', + 'Corporations' => '+-Corporations-+', + 'Government' => '+-Government-+', + 'War & Military' => '+-War-Military-+' + ] + ], + 'defaultValue' => 'news' + ], + 'teaser' => [ + 'name' => 'Show teaser', + 'type' => 'checkbox', + 'title' => 'If checked feeds will display the teaser', + 'defaultValue' => 'checked' + ] + ] + ]; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + // News are presented differently + switch ($this->getInput('category')) { + case '-News-': + $this->loadNewsItems($html); + break; + default: + $this->loadLeakItems($html); + } + } + + public function getURI() + { + if (!is_null($this->getInput('category'))) { + return static::URI . '/' . $this->getInput('category') . '.html'; + } + + return parent::getURI(); + } + + public function getName() + { + if (!is_null($this->getInput('category'))) { + $category = array_search( + $this->getInput('category'), + static::PARAMETERS[0]['category']['values'] + ); + + if ($category === false) { + $category = array_search( + $this->getInput('category'), + static::PARAMETERS[0]['category']['values']['Leaks'] + ); + } + + return $category . ' - ' . static::NAME; + } + + return parent::getName(); + } + + private function loadNewsItems($html) + { + $articles = $html->find('div.news-articles ul li'); + + if (is_null($articles) || count($articles) === 0) { + return; + } + + foreach ($articles as $article) { + $item = []; + + $item['title'] = $article->find('h3', 0)->plaintext; + $item['uri'] = static::URI . $article->find('h3 a', 0)->href; + $item['content'] = $article->find('div.introduction', 0)->plaintext; + $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext); + + $this->items[] = $item; + } + } + + private function loadLeakItems($html) + { + $articles = $html->find('li.tile'); + + if (is_null($articles) || count($articles) === 0) { + return; + } + + foreach ($articles as $article) { + $item = []; + + $item['title'] = $article->find('h2', 0)->plaintext; + $item['uri'] = static::URI . $article->find('a', 0)->href; + + $teaser = static::URI . '/' . $article->find('div.teaser img', 0)->src; + + if ($this->getInput('teaser')) { + $item['content'] = '<img src="' + . $teaser + . '" /><p>' + . $article->find('div.intro', 0)->plaintext + . '</p>'; + } else { + $item['content'] = $article->find('div.intro', 0)->plaintext; + } + + $item['timestamp'] = strtotime($article->find('div.timestamp', 0)->plaintext); + $item['enclosures'] = [$teaser]; + + $this->items[] = $item; + } + } } diff --git a/bridges/WikipediaBridge.php b/bridges/WikipediaBridge.php index 1bdf2ddc..30e551ed 100644 --- a/bridges/WikipediaBridge.php +++ b/bridges/WikipediaBridge.php @@ -3,324 +3,347 @@ define('WIKIPEDIA_SUBJECT_TFA', 0); // Today's featured article define('WIKIPEDIA_SUBJECT_DYK', 1); // Did you know... -class WikipediaBridge extends BridgeAbstract { - const MAINTAINER = 'logmanoriginal'; - const NAME = 'Wikipedia bridge for many languages'; - const URI = 'https://www.wikipedia.org/'; - const DESCRIPTION = 'Returns articles for a language of your choice'; - - const PARAMETERS = array( array( - 'language' => array( - 'name' => 'Language', - 'type' => 'list', - 'title' => 'Select your language', - 'exampleValue' => 'English', - 'values' => array( - 'English' => 'en', - 'Русский' => 'ru', - 'Dutch' => 'nl', - 'Esperanto' => 'eo', - 'French' => 'fr', - 'German' => 'de', - ) - ), - 'subject' => array( - 'name' => 'Subject', - 'type' => 'list', - 'title' => 'What subject are you interested in?', - 'exampleValue' => 'Today\'s featured article', - 'values' => array( - 'Today\'s featured article' => 'tfa', - 'Did you know…' => 'dyk' - ) - ), - 'fullarticle' => array( - 'name' => 'Load full article', - 'type' => 'checkbox', - 'title' => 'Activate to always load the full article' - ) - )); - - public function getURI(){ - if(!is_null($this->getInput('language'))) { - return 'https://' - . strtolower($this->getInput('language')) - . '.wikipedia.org'; - } - - return parent::getURI(); - } - - public function getName(){ - switch($this->getInput('subject')) { - case 'tfa': - $subject = WIKIPEDIA_SUBJECT_TFA; - break; - case 'dyk': - $subject = WIKIPEDIA_SUBJECT_DYK; - break; - default: return parent::getName(); - } - - switch($subject) { - case WIKIPEDIA_SUBJECT_TFA: - $name = 'Today\'s featured article from ' - . strtolower($this->getInput('language')) - . '.wikipedia.org'; - break; - case WIKIPEDIA_SUBJECT_DYK: - $name = 'Did you know? - articles from ' - . strtolower($this->getInput('language')) - . '.wikipedia.org'; - break; - default: - $name = 'Articles from ' - . strtolower($this->getInput('language')) - . '.wikipedia.org'; - break; - } - return $name; - } - - public function collectData(){ - - switch($this->getInput('subject')) { - case 'tfa': - $subject = WIKIPEDIA_SUBJECT_TFA; - break; - case 'dyk': - $subject = WIKIPEDIA_SUBJECT_DYK; - break; - default: - $subject = WIKIPEDIA_SUBJECT_TFA; - break; - } - - $fullArticle = $this->getInput('fullarticle'); - - // This will automatically send us to the correct main page in any language (try it!) - $html = getSimpleHTMLDOM($this->getURI() . '/wiki'); - - if(!$html) - returnServerError('Could not load site: ' . $this->getURI() . '!'); - - /* - * Now read content depending on the language (make sure to create one function per language!) - * We build the function name automatically, just make sure you create a private function ending - * with your desired language code, where the language code is upper case! (en -> getContentsEN). - */ - $function = 'getContents' . ucfirst(strtolower($this->getInput('language'))); - - if(!method_exists($this, $function)) - returnServerError('A function to get the contents for your language is missing (\'' . $function . '\')!'); - - /* - * The method takes care of creating all items. - */ - $this->$function($html, $subject, $fullArticle); - } - - /** - * Replaces all relative URIs with absolute ones - * @param $element A simplehtmldom element - * @return The $element->innertext with all URIs replaced - */ - private function replaceUriInHtmlElement($element){ - return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext); - } - - /* - * Adds a new item to $items using a generic operation (should work for most - * (all?) wikis) $anchorText can be specified if the wiki in question doesn't - * use '...' (like Dutch, French and Italian) $anchorFallbackIndex can be - * used to specify a different fallback link than the first - * (e.g., -1 for the last) - */ - private function addTodaysFeaturedArticleGeneric($element, - $fullArticle, - $anchorText = '...', - $anchorFallbackIndex = 0){ - // Clean the bottom of the featured article - if ($element->find('ul', -1)) - $element->find('ul', -1)->outertext = ''; - elseif ($element->find('div', -1)) { - $element->find('div', -1)->outertext = ''; - } - - // The title and URI of the article can be found in an anchor containing - // the string '...' in most wikis ('full article ...') - $target = $element->find('p a', $anchorFallbackIndex); - foreach($element->find('//a') as $anchor) { - if(strpos($anchor->innertext, $anchorText) !== false) { - $target = $anchor; - break; - } - } - - $item = array(); - $item['uri'] = $this->getURI() . $target->href; - $item['title'] = $target->title; - - if(!$fullArticle) - $item['content'] = strip_tags($this->replaceUriInHtmlElement($element), '<a><p><br><img>'); - else - $item['content'] = $this->loadFullArticle($item['uri']); - - $this->items[] = $item; - } - - /* - * Adds a new item to $items using a generic operation (should work for most (all?) wikis) - */ - private function addDidYouKnowGeneric($element, $fullArticle){ - foreach($element->find('ul', 0)->find('li') as $entry) { - $item = array(); - - // We can only use the first anchor, there is no way of finding the 'correct' one if there are multiple - $item['uri'] = $this->getURI() . $entry->find('a', 0)->href; - $item['title'] = strip_tags($entry->innertext); - - if(!$fullArticle) - $item['content'] = $this->replaceUriInHtmlElement($entry); - else - $item['content'] = $this->loadFullArticle($item['uri']); - - $this->items[] = $item; - } - } - - /** - * Loads the full article from a given URI - */ - private function loadFullArticle($uri){ - $content_html = getSimpleHTMLDOMCached($uri); - - if(!$content_html) - returnServerError('Could not load site: ' . $uri . '!'); - - $content = $content_html->find('#mw-content-text', 0); - - if(!$content) - returnServerError('Could not find content in page: ' . $uri . '!'); - - // Let's remove a couple of things from the article - $table = $content->find('#toc', 0); // Table of contents - if(!$table === false) - $table->outertext = ''; - - foreach($content->find('ol.references') as $reference) // References - $reference->outertext = ''; - - return str_replace('href="/', 'href="' . $this->getURI() . '/', $content->innertext); - } - - /** - * Implementation for de.wikipedia.org - */ - private function getContentsDe($html, $subject, $fullArticle){ - switch($subject) { - case WIKIPEDIA_SUBJECT_TFA: - $element = $html->find('div[id=artikel] div.hauptseite-box-content', 0); - $this->addTodaysFeaturedArticleGeneric($element, $fullArticle); - break; - case WIKIPEDIA_SUBJECT_DYK: - $element = $html->find('div[id=wissenswertes]', 0); - $this->addDidYouKnowGeneric($element, $fullArticle); - break; - default: - break; - } - } - - /** - * Implementation for fr.wikipedia.org - */ - private function getContentsFr($html, $subject, $fullArticle){ - switch($subject) { - case WIKIPEDIA_SUBJECT_TFA: - $element = $html->find('div[class=accueil_2017_cadre]', 0); - $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lire la suite'); - break; - case WIKIPEDIA_SUBJECT_DYK: - $element = $html->find('div[class=accueil_2017_cadre]', 2); - $this->addDidYouKnowGeneric($element, $fullArticle); - break; - default: - break; - } - } - - /** - * Implementation for en.wikipedia.org - */ - private function getContentsEn($html, $subject, $fullArticle){ - switch($subject) { - case WIKIPEDIA_SUBJECT_TFA: - $element = $html->find('div[id=mp-tfa]', 0); - $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, -1); - break; - case WIKIPEDIA_SUBJECT_DYK: - $element = $html->find('div[id=mp-dyk]', 0); - $this->addDidYouKnowGeneric($element, $fullArticle); - break; - default: - break; - } - } - - /** - * Implementation for ru.wikipedia.org - */ - private function getContentsRu($html, $subject, $fullArticle){ - switch($subject) { - case WIKIPEDIA_SUBJECT_TFA: - $element = $html->find('div[id=main-tfa]', 0); - $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, -1); - break; - case WIKIPEDIA_SUBJECT_DYK: - $element = $html->find('div[id=main-dyk]', 0); - $this->addDidYouKnowGeneric($element, $fullArticle); - break; - default: - break; - } - } - - /** - * Implementation for eo.wikipedia.org - */ - private function getContentsEo($html, $subject, $fullArticle){ - switch($subject) { - case WIKIPEDIA_SUBJECT_TFA: - $element = $html->find('div[id=mf-artikolo-de-la-monato]', 0); - $element->find('div', -2)->outertext = ''; - $this->addTodaysFeaturedArticleGeneric($element, $fullArticle); - break; - case WIKIPEDIA_SUBJECT_DYK: - $element = $html->find('div.hp', 1)->find('table', 4)->find('td', -1); - $this->addDidYouKnowGeneric($element, $fullArticle); - break; - default: - break; - } - } - - /** - * Implementation for nl.wikipedia.org - */ - private function getContentsNl($html, $subject, $fullArticle){ - switch($subject) { - case WIKIPEDIA_SUBJECT_TFA: - $element = $html->find('td[id=segment-Uitgelicht] div', 0); - $element->find('p', 1)->outertext = ''; - $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lees verder'); - break; - case WIKIPEDIA_SUBJECT_DYK: - $element = $html->find('td[id=segment-Wist_je_dat] div', 0); - $this->addDidYouKnowGeneric($element, $fullArticle); - break; - default: - break; - } - } +class WikipediaBridge extends BridgeAbstract +{ + const MAINTAINER = 'logmanoriginal'; + const NAME = 'Wikipedia bridge for many languages'; + const URI = 'https://www.wikipedia.org/'; + const DESCRIPTION = 'Returns articles for a language of your choice'; + + const PARAMETERS = [ [ + 'language' => [ + 'name' => 'Language', + 'type' => 'list', + 'title' => 'Select your language', + 'exampleValue' => 'English', + 'values' => [ + 'English' => 'en', + 'Русский' => 'ru', + 'Dutch' => 'nl', + 'Esperanto' => 'eo', + 'French' => 'fr', + 'German' => 'de', + ] + ], + 'subject' => [ + 'name' => 'Subject', + 'type' => 'list', + 'title' => 'What subject are you interested in?', + 'exampleValue' => 'Today\'s featured article', + 'values' => [ + 'Today\'s featured article' => 'tfa', + 'Did you know…' => 'dyk' + ] + ], + 'fullarticle' => [ + 'name' => 'Load full article', + 'type' => 'checkbox', + 'title' => 'Activate to always load the full article' + ] + ]]; + + public function getURI() + { + if (!is_null($this->getInput('language'))) { + return 'https://' + . strtolower($this->getInput('language')) + . '.wikipedia.org'; + } + + return parent::getURI(); + } + + public function getName() + { + switch ($this->getInput('subject')) { + case 'tfa': + $subject = WIKIPEDIA_SUBJECT_TFA; + break; + case 'dyk': + $subject = WIKIPEDIA_SUBJECT_DYK; + break; + default: + return parent::getName(); + } + + switch ($subject) { + case WIKIPEDIA_SUBJECT_TFA: + $name = 'Today\'s featured article from ' + . strtolower($this->getInput('language')) + . '.wikipedia.org'; + break; + case WIKIPEDIA_SUBJECT_DYK: + $name = 'Did you know? - articles from ' + . strtolower($this->getInput('language')) + . '.wikipedia.org'; + break; + default: + $name = 'Articles from ' + . strtolower($this->getInput('language')) + . '.wikipedia.org'; + break; + } + return $name; + } + + public function collectData() + { + switch ($this->getInput('subject')) { + case 'tfa': + $subject = WIKIPEDIA_SUBJECT_TFA; + break; + case 'dyk': + $subject = WIKIPEDIA_SUBJECT_DYK; + break; + default: + $subject = WIKIPEDIA_SUBJECT_TFA; + break; + } + + $fullArticle = $this->getInput('fullarticle'); + + // This will automatically send us to the correct main page in any language (try it!) + $html = getSimpleHTMLDOM($this->getURI() . '/wiki'); + + if (!$html) { + returnServerError('Could not load site: ' . $this->getURI() . '!'); + } + + /* + * Now read content depending on the language (make sure to create one function per language!) + * We build the function name automatically, just make sure you create a private function ending + * with your desired language code, where the language code is upper case! (en -> getContentsEN). + */ + $function = 'getContents' . ucfirst(strtolower($this->getInput('language'))); + + if (!method_exists($this, $function)) { + returnServerError('A function to get the contents for your language is missing (\'' . $function . '\')!'); + } + + /* + * The method takes care of creating all items. + */ + $this->$function($html, $subject, $fullArticle); + } + + /** + * Replaces all relative URIs with absolute ones + * @param $element A simplehtmldom element + * @return The $element->innertext with all URIs replaced + */ + private function replaceUriInHtmlElement($element) + { + return str_replace('href="/', 'href="' . $this->getURI() . '/', $element->innertext); + } + + /* + * Adds a new item to $items using a generic operation (should work for most + * (all?) wikis) $anchorText can be specified if the wiki in question doesn't + * use '...' (like Dutch, French and Italian) $anchorFallbackIndex can be + * used to specify a different fallback link than the first + * (e.g., -1 for the last) + */ + private function addTodaysFeaturedArticleGeneric( + $element, + $fullArticle, + $anchorText = '...', + $anchorFallbackIndex = 0 + ) { + // Clean the bottom of the featured article + if ($element->find('ul', -1)) { + $element->find('ul', -1)->outertext = ''; + } elseif ($element->find('div', -1)) { + $element->find('div', -1)->outertext = ''; + } + + // The title and URI of the article can be found in an anchor containing + // the string '...' in most wikis ('full article ...') + $target = $element->find('p a', $anchorFallbackIndex); + foreach ($element->find('//a') as $anchor) { + if (strpos($anchor->innertext, $anchorText) !== false) { + $target = $anchor; + break; + } + } + + $item = []; + $item['uri'] = $this->getURI() . $target->href; + $item['title'] = $target->title; + + if (!$fullArticle) { + $item['content'] = strip_tags($this->replaceUriInHtmlElement($element), '<a><p><br><img>'); + } else { + $item['content'] = $this->loadFullArticle($item['uri']); + } + + $this->items[] = $item; + } + + /* + * Adds a new item to $items using a generic operation (should work for most (all?) wikis) + */ + private function addDidYouKnowGeneric($element, $fullArticle) + { + foreach ($element->find('ul', 0)->find('li') as $entry) { + $item = []; + + // We can only use the first anchor, there is no way of finding the 'correct' one if there are multiple + $item['uri'] = $this->getURI() . $entry->find('a', 0)->href; + $item['title'] = strip_tags($entry->innertext); + + if (!$fullArticle) { + $item['content'] = $this->replaceUriInHtmlElement($entry); + } else { + $item['content'] = $this->loadFullArticle($item['uri']); + } + + $this->items[] = $item; + } + } + + /** + * Loads the full article from a given URI + */ + private function loadFullArticle($uri) + { + $content_html = getSimpleHTMLDOMCached($uri); + + if (!$content_html) { + returnServerError('Could not load site: ' . $uri . '!'); + } + + $content = $content_html->find('#mw-content-text', 0); + + if (!$content) { + returnServerError('Could not find content in page: ' . $uri . '!'); + } + + // Let's remove a couple of things from the article + $table = $content->find('#toc', 0); // Table of contents + if (!$table === false) { + $table->outertext = ''; + } + + foreach ($content->find('ol.references') as $reference) { // References + $reference->outertext = ''; + } + + return str_replace('href="/', 'href="' . $this->getURI() . '/', $content->innertext); + } + + /** + * Implementation for de.wikipedia.org + */ + private function getContentsDe($html, $subject, $fullArticle) + { + switch ($subject) { + case WIKIPEDIA_SUBJECT_TFA: + $element = $html->find('div[id=artikel] div.hauptseite-box-content', 0); + $this->addTodaysFeaturedArticleGeneric($element, $fullArticle); + break; + case WIKIPEDIA_SUBJECT_DYK: + $element = $html->find('div[id=wissenswertes]', 0); + $this->addDidYouKnowGeneric($element, $fullArticle); + break; + default: + break; + } + } + + /** + * Implementation for fr.wikipedia.org + */ + private function getContentsFr($html, $subject, $fullArticle) + { + switch ($subject) { + case WIKIPEDIA_SUBJECT_TFA: + $element = $html->find('div[class=accueil_2017_cadre]', 0); + $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lire la suite'); + break; + case WIKIPEDIA_SUBJECT_DYK: + $element = $html->find('div[class=accueil_2017_cadre]', 2); + $this->addDidYouKnowGeneric($element, $fullArticle); + break; + default: + break; + } + } + + /** + * Implementation for en.wikipedia.org + */ + private function getContentsEn($html, $subject, $fullArticle) + { + switch ($subject) { + case WIKIPEDIA_SUBJECT_TFA: + $element = $html->find('div[id=mp-tfa]', 0); + $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, -1); + break; + case WIKIPEDIA_SUBJECT_DYK: + $element = $html->find('div[id=mp-dyk]', 0); + $this->addDidYouKnowGeneric($element, $fullArticle); + break; + default: + break; + } + } + + /** + * Implementation for ru.wikipedia.org + */ + private function getContentsRu($html, $subject, $fullArticle) + { + switch ($subject) { + case WIKIPEDIA_SUBJECT_TFA: + $element = $html->find('div[id=main-tfa]', 0); + $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, -1); + break; + case WIKIPEDIA_SUBJECT_DYK: + $element = $html->find('div[id=main-dyk]', 0); + $this->addDidYouKnowGeneric($element, $fullArticle); + break; + default: + break; + } + } + + /** + * Implementation for eo.wikipedia.org + */ + private function getContentsEo($html, $subject, $fullArticle) + { + switch ($subject) { + case WIKIPEDIA_SUBJECT_TFA: + $element = $html->find('div[id=mf-artikolo-de-la-monato]', 0); + $element->find('div', -2)->outertext = ''; + $this->addTodaysFeaturedArticleGeneric($element, $fullArticle); + break; + case WIKIPEDIA_SUBJECT_DYK: + $element = $html->find('div.hp', 1)->find('table', 4)->find('td', -1); + $this->addDidYouKnowGeneric($element, $fullArticle); + break; + default: + break; + } + } + + /** + * Implementation for nl.wikipedia.org + */ + private function getContentsNl($html, $subject, $fullArticle) + { + switch ($subject) { + case WIKIPEDIA_SUBJECT_TFA: + $element = $html->find('td[id=segment-Uitgelicht] div', 0); + $element->find('p', 1)->outertext = ''; + $this->addTodaysFeaturedArticleGeneric($element, $fullArticle, 'Lees verder'); + break; + case WIKIPEDIA_SUBJECT_DYK: + $element = $html->find('td[id=segment-Wist_je_dat] div', 0); + $this->addDidYouKnowGeneric($element, $fullArticle); + break; + default: + break; + } + } } diff --git a/bridges/WiredBridge.php b/bridges/WiredBridge.php index b15f781f..d4c7cbbb 100644 --- a/bridges/WiredBridge.php +++ b/bridges/WiredBridge.php @@ -1,103 +1,110 @@ <?php -class WiredBridge extends FeedExpander { - const MAINTAINER = 'ORelio'; - const NAME = 'WIRED Bridge'; - const URI = 'https://www.wired.com/'; - const DESCRIPTION = 'Returns the newest articles from WIRED'; - - const PARAMETERS = array( array( - 'feed' => array( - 'name' => 'Feed', - 'type' => 'list', - 'values' => array( - 'WIRED Top Stories' => 'rss', // /feed/rss - 'Business' => 'business', // /feed/category/business/latest/rss - 'Culture' => 'culture', // /feed/category/culture/latest/rss - 'Gear' => 'gear', // /feed/category/gear/latest/rss - 'Ideas' => 'ideas', // /feed/category/ideas/latest/rss - 'Science' => 'science', // /feed/category/science/latest/rss - 'Security' => 'security', // /feed/category/security/latest/rss - 'Transportation' => 'transportation', // /feed/category/transportation/latest/rss - 'Backchannel' => 'backchannel', // /feed/category/backchannel/latest/rss - 'WIRED Guides' => 'wired-guide', // /feed/tag/wired-guide/latest/rss - 'Photo' => 'photo' // /feed/category/photo/latest/rss - ) - ), - 'limit' => self::LIMIT, - )); - - public function collectData(){ - $feed = $this->getInput('feed'); - if(empty($feed) || !ctype_alpha(str_replace('-', '', $feed))) { - returnClientError('Invalid feed, please check the "feed" parameter.'); - } - - $feed_url = $this->getURI() . 'feed/'; - if ($feed != 'rss') { - if ($feed != 'wired-guide') { - $feed_url .= 'category/'; - } else { - $feed_url .= 'tag/'; - } - $feed_url .= "$feed/latest/"; - } - $feed_url .= 'rss'; - - $limit = $this->getInput('limit') ?? -1; - $this->collectExpandableDatas($feed_url, $limit); - } - - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $article = getSimpleHTMLDOMCached($item['uri']); - $item['content'] = $this->extractArticleContent($article); - - $headline = strval($newsItem->description); - if(!empty($headline)) { - $item['content'] = '<p><b>' . $headline . '</b></p>' . $item['content']; - } - - $item_image = $article->find('meta[property="og:image"]', 0); - if(!empty($item_image)) { - $item['enclosures'] = array($item_image->content); - $item['content'] = '<p><img src="' . $item_image->content . '" /></p>' . $item['content']; - } - - return $item; - } - - private function extractArticleContent($article){ - $content = $article->find('article', 0); - $truncate = true; - - if (empty($content)) { - $content = $article->find('div.listicle-main-component__container', 0); - $truncate = false; - } - - if (!empty($content)) { - $content = $content->innertext; - } - - foreach (array( - '<div class="content-header', - '<div class="mid-banner-wrap', - '<div class="related', - '<div class="social-icons', - '<div class="recirc-most-popular', - '<div class="grid--item article-related-video', - '<div class="row full-bleed-ad', - ) as $div_start) { - $content = stripRecursiveHTMLSection($content, 'div', $div_start); - } - - if ($truncate) { - //Clutter after standard article is too hard to clean properly - $content = trim(explode('<hr', $content)[0]); - } - - $content = str_replace('href="/', 'href="' . $this->getURI() . '/', $content); - - return $content; - } + +class WiredBridge extends FeedExpander +{ + const MAINTAINER = 'ORelio'; + const NAME = 'WIRED Bridge'; + const URI = 'https://www.wired.com/'; + const DESCRIPTION = 'Returns the newest articles from WIRED'; + + const PARAMETERS = [ [ + 'feed' => [ + 'name' => 'Feed', + 'type' => 'list', + 'values' => [ + 'WIRED Top Stories' => 'rss', // /feed/rss + 'Business' => 'business', // /feed/category/business/latest/rss + 'Culture' => 'culture', // /feed/category/culture/latest/rss + 'Gear' => 'gear', // /feed/category/gear/latest/rss + 'Ideas' => 'ideas', // /feed/category/ideas/latest/rss + 'Science' => 'science', // /feed/category/science/latest/rss + 'Security' => 'security', // /feed/category/security/latest/rss + 'Transportation' => 'transportation', // /feed/category/transportation/latest/rss + 'Backchannel' => 'backchannel', // /feed/category/backchannel/latest/rss + 'WIRED Guides' => 'wired-guide', // /feed/tag/wired-guide/latest/rss + 'Photo' => 'photo' // /feed/category/photo/latest/rss + ] + ], + 'limit' => self::LIMIT, + ]]; + + public function collectData() + { + $feed = $this->getInput('feed'); + if (empty($feed) || !ctype_alpha(str_replace('-', '', $feed))) { + returnClientError('Invalid feed, please check the "feed" parameter.'); + } + + $feed_url = $this->getURI() . 'feed/'; + if ($feed != 'rss') { + if ($feed != 'wired-guide') { + $feed_url .= 'category/'; + } else { + $feed_url .= 'tag/'; + } + $feed_url .= "$feed/latest/"; + } + $feed_url .= 'rss'; + + $limit = $this->getInput('limit') ?? -1; + $this->collectExpandableDatas($feed_url, $limit); + } + + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $article = getSimpleHTMLDOMCached($item['uri']); + $item['content'] = $this->extractArticleContent($article); + + $headline = strval($newsItem->description); + if (!empty($headline)) { + $item['content'] = '<p><b>' . $headline . '</b></p>' . $item['content']; + } + + $item_image = $article->find('meta[property="og:image"]', 0); + if (!empty($item_image)) { + $item['enclosures'] = [$item_image->content]; + $item['content'] = '<p><img src="' . $item_image->content . '" /></p>' . $item['content']; + } + + return $item; + } + + private function extractArticleContent($article) + { + $content = $article->find('article', 0); + $truncate = true; + + if (empty($content)) { + $content = $article->find('div.listicle-main-component__container', 0); + $truncate = false; + } + + if (!empty($content)) { + $content = $content->innertext; + } + + foreach ( + [ + '<div class="content-header', + '<div class="mid-banner-wrap', + '<div class="related', + '<div class="social-icons', + '<div class="recirc-most-popular', + '<div class="grid--item article-related-video', + '<div class="row full-bleed-ad', + ] as $div_start + ) { + $content = stripRecursiveHTMLSection($content, 'div', $div_start); + } + + if ($truncate) { + //Clutter after standard article is too hard to clean properly + $content = trim(explode('<hr', $content)[0]); + } + + $content = str_replace('href="/', 'href="' . $this->getURI() . '/', $content); + + return $content; + } } diff --git a/bridges/WordPressBridge.php b/bridges/WordPressBridge.php index 0371c834..5a80c398 100644 --- a/bridges/WordPressBridge.php +++ b/bridges/WordPressBridge.php @@ -1,107 +1,115 @@ <?php -class WordPressBridge extends FeedExpander { - const NAME = 'Wordpress Bridge'; - const URI = 'https://wordpress.org/'; - const DESCRIPTION = 'Returns the newest full posts of a WordPress powered website'; - const PARAMETERS = array( array( - 'url' => array( - 'name' => 'Blog URL', - 'exampleValue' => 'https://www.wpbeginner.com/', - 'required' => true - ) - )); +class WordPressBridge extends FeedExpander +{ + const NAME = 'Wordpress Bridge'; + const URI = 'https://wordpress.org/'; + const DESCRIPTION = 'Returns the newest full posts of a WordPress powered website'; - private function cleanContent($content){ - $content = stripWithDelimiters($content, '<script', '</script>'); - $content = preg_replace('/<div class="wpa".*/', '', $content); - $content = preg_replace('/<form.*\/form>/', '', $content); - return $content; - } + const PARAMETERS = [ [ + 'url' => [ + 'name' => 'Blog URL', + 'exampleValue' => 'https://www.wpbeginner.com/', + 'required' => true + ] + ]]; - protected function parseItem($newItem){ - $item = parent::parseItem($newItem); + private function cleanContent($content) + { + $content = stripWithDelimiters($content, '<script', '</script>'); + $content = preg_replace('/<div class="wpa".*/', '', $content); + $content = preg_replace('/<form.*\/form>/', '', $content); + return $content; + } - $article_html = getSimpleHTMLDOMCached($item['uri']); + protected function parseItem($newItem) + { + $item = parent::parseItem($newItem); - $article = null; - switch(true) { + $article_html = getSimpleHTMLDOMCached($item['uri']); - // Custom fix for theme in https://jungefreiheit.de/politik/deutschland/2022/wahl-im-saarland/ - case !is_null($article_html->find('div[data-widget_type="theme-post-content.default"]', 0)): - $article = $article_html->find('div[data-widget_type="theme-post-content.default"]', 0); - break; - case !is_null($article_html->find('[itemprop=articleBody]', 0)): - // highest priority content div - $article = $article_html->find('[itemprop=articleBody]', 0); - break; - case !is_null($article_html->find('article', 0)): - // most common content div - $article = $article_html->find('article', 0); - break; - case !is_null($article_html->find('.single-content', 0)): - // another common content div - $article = $article_html->find('.single-content', 0); - break; - case !is_null($article_html->find('.post-content', 0)): - // another common content div - $article = $article_html->find('.post-content', 0); - break; - case !is_null($article_html->find('.post', 0)): - // for old WordPress themes without HTML5 - $article = $article_html->find('.post', 0); - break; - } + $article = null; + switch (true) { + // Custom fix for theme in https://jungefreiheit.de/politik/deutschland/2022/wahl-im-saarland/ + case !is_null($article_html->find('div[data-widget_type="theme-post-content.default"]', 0)): + $article = $article_html->find('div[data-widget_type="theme-post-content.default"]', 0); + break; + case !is_null($article_html->find('[itemprop=articleBody]', 0)): + // highest priority content div + $article = $article_html->find('[itemprop=articleBody]', 0); + break; + case !is_null($article_html->find('article', 0)): + // most common content div + $article = $article_html->find('article', 0); + break; + case !is_null($article_html->find('.single-content', 0)): + // another common content div + $article = $article_html->find('.single-content', 0); + break; + case !is_null($article_html->find('.post-content', 0)): + // another common content div + $article = $article_html->find('.post-content', 0); + break; + case !is_null($article_html->find('.post', 0)): + // for old WordPress themes without HTML5 + $article = $article_html->find('.post', 0); + break; + } - foreach ($article->find('h1.entry-title') as $title) - if ($title->plaintext == $item['title']) - $title->outertext = ''; + foreach ($article->find('h1.entry-title') as $title) { + if ($title->plaintext == $item['title']) { + $title->outertext = ''; + } + } - $article_image = $article_html->find('img.wp-post-image', 0); - if(!empty($item['content']) && (!is_object($article_image) || empty($article_image->src))) { - $article_image = str_get_html($item['content'])->find('img.wp-post-image', 0); - } - if(is_object($article_image) && !empty($article_image->src)) { - if(empty($article_image->getAttribute('data-lazy-src'))) { - $article_image = $article_image->src; - } else { - $article_image = $article_image->getAttribute('data-lazy-src'); - } - $mime_type = getMimeType($article_image); - if (strpos($mime_type, 'image') === false) - $article_image .= '#.image'; // force image - if (empty($item['enclosures'])) - $item['enclosures'] = array($article_image); - else - $item['enclosures'] = array_merge($item['enclosures'], $article_image); - } + $article_image = $article_html->find('img.wp-post-image', 0); + if (!empty($item['content']) && (!is_object($article_image) || empty($article_image->src))) { + $article_image = str_get_html($item['content'])->find('img.wp-post-image', 0); + } + if (is_object($article_image) && !empty($article_image->src)) { + if (empty($article_image->getAttribute('data-lazy-src'))) { + $article_image = $article_image->src; + } else { + $article_image = $article_image->getAttribute('data-lazy-src'); + } + $mime_type = getMimeType($article_image); + if (strpos($mime_type, 'image') === false) { + $article_image .= '#.image'; // force image + } + if (empty($item['enclosures'])) { + $item['enclosures'] = [$article_image]; + } else { + $item['enclosures'] = array_merge($item['enclosures'], $article_image); + } + } - if(!is_null($article)) { - $item['content'] = $this->cleanContent($article->innertext); - $item['content'] = defaultLinkTo($item['content'], $item['uri']); - } + if (!is_null($article)) { + $item['content'] = $this->cleanContent($article->innertext); + $item['content'] = defaultLinkTo($item['content'], $item['uri']); + } - return $item; - } + return $item; + } - public function getURI(){ - $url = $this->getInput('url'); - if(empty($url)) { - $url = parent::getURI(); - } - return $url; - } + public function getURI() + { + $url = $this->getInput('url'); + if (empty($url)) { + $url = parent::getURI(); + } + return $url; + } - public function collectData(){ - if($this->getInput('url') && substr($this->getInput('url'), 0, strlen('http')) !== 'http') { - // just in case someone find a way to access local files by playing with the url - returnClientError('The url parameter must either refer to http or https protocol.'); - } - try{ - $this->collectExpandableDatas($this->getURI() . '/feed/atom/', 20); - } catch (Exception $e) { - $this->collectExpandableDatas($this->getURI() . '/?feed=atom', 20); - } - - } + public function collectData() + { + if ($this->getInput('url') && substr($this->getInput('url'), 0, strlen('http')) !== 'http') { + // just in case someone find a way to access local files by playing with the url + returnClientError('The url parameter must either refer to http or https protocol.'); + } + try { + $this->collectExpandableDatas($this->getURI() . '/feed/atom/', 20); + } catch (Exception $e) { + $this->collectExpandableDatas($this->getURI() . '/?feed=atom', 20); + } + } } diff --git a/bridges/WordPressMadaraBridge.php b/bridges/WordPressMadaraBridge.php index 3170e119..4325075c 100644 --- a/bridges/WordPressMadaraBridge.php +++ b/bridges/WordPressMadaraBridge.php @@ -1,132 +1,145 @@ <?php + /** * This bridge currently parses only chapter lists, but it can be further * extended to extract a list of manga titles using the implementation in this * project as a reference: https://github.com/manga-download/hakuneko */ -class WordPressMadaraBridge extends BridgeAbstract { - const URI = 'https://live.mangabooth.com/'; - const NAME = 'WordPress Madara'; - const DESCRIPTION = 'Returns latest chapters published through the Madara Manga theme. +class WordPressMadaraBridge extends BridgeAbstract +{ + const URI = 'https://live.mangabooth.com/'; + const NAME = 'WordPress Madara'; + const DESCRIPTION = 'Returns latest chapters published through the Madara Manga theme. The default URI shows the Madara demo page.'; - const PARAMETERS = array( - 'Manga Chapters' => array( - 'url' => array( - 'name' => 'Manga URL', - 'exampleValue' => 'https://live.mangabooth.com/manga/manga-text-chapter/', - 'required' => true - ) - ) - ); - - public function getName() { - switch($this->queriedContext) { - case 'Manga Chapters': - $mangaInfo = $this->getMangaInfo($this->getInput('url')); - return $mangaInfo['title']; - default: - return parent::getName(); - } - } - - public function getURI() { - return $this->getInput('url') ?? self::URI; - } - - public function collectData() { - $html = $this->queryAjaxChapters(); - - // Check if the list subcategorizes by volume - $volumes = $html->find('ul.volumns', 0); - if ($volumes) { - $this->parseVolumes($volumes); - } else { - $this->parseChapterList($html, null); - } - } - - protected function queryAjaxChaptersNew() { - $uri = rtrim($this->getInput('url'), '/') . '/ajax/chapters/'; - $headers = array(); - $opts = array(CURLOPT_POST => 1); - return str_get_html(getContents($uri, $headers, $opts)); - } - - protected function queryAjaxChaptersOld() { - $mangaInfo = $this->getMangaInfo($this->getInput('url')); - $uri = rtrim($mangaInfo['root'], '/') . '/wp-admin/admin-ajax.php'; - $headers = array(); - $opts = array(CURLOPT_POSTFIELDS => array( - 'action' => 'manga_get_chapters', - 'manga' => $mangaInfo['id'] - )); - return str_get_html(getContents($uri, $headers, $opts)); - } - - protected function queryAjaxChapters() { - $new = $this->queryAjaxChaptersNew(); - if ($new->find('.wp-manga-chapter')) { - return $new; - } else { - return $this->queryAjaxChaptersOld(); - } - } - - protected function parseVolumes($volumes) { - foreach($volumes->children(-1) as $volume) { - $volume_name = trim($volume->find('a.has-child', 0)->plaintext); - $this->parseChapterList($volume->find('ul', -1), $volume_name); - } - } - - protected function parseChapterList($chapters, $volume) { - $mangaInfo = $this->getMangaInfo($this->getInput('url')); - foreach($chapters->find('li.wp-manga-chapter') as $chap) { - $link = $chap->find('a', 0); - - $item = array(); - $item['title'] = ($volume ?? '') . ' ' . trim($link->plaintext); - $item['uri'] = $link->href; - $item['uid'] = $link->href; - $item['timestamp'] = $chap->find('span.chapter-release-date', 0)->plaintext; - $item['author'] = $mangaInfo['author'] ?? null; - $item['categories'] = $mangaInfo['categories'] ?? null; - $this->items[] = $item; - } - } - - /** - * Retrieves manga info from cache or title page. - * The returned array contains 'title', 'author', and 'categories' keys for use in feed items. - * The 'id' key contains the manga title id, used for the old ajax api. - * The 'root' key contains the website root. - * - * @param $url - * @return array - */ - protected function getMangaInfo($url) { - $url_cache = 'TitleInfo_' . preg_replace('/[^\w]/', '.', rtrim($url, '/')); - $cache = $this->loadCacheValue($url_cache); - if (isset($cache)) { - return $cache; - } - - $info = array(); - $html = getSimpleHTMLDOMCached($url); - - $info['title'] = html_entity_decode($html->find('*[property=og:title]', 0)->content); - $author = $html->find('.author-content', 0); - if (!is_null($author)) - $info['author'] = trim($author->plaintext); - $cats = $html->find('.genres-content', 0); - if (!is_null($cats)) - $info['categories'] = explode(', ', trim($cats->plaintext)); - - $info['id'] = $html->find('#manga-chapters-holder', 0)->getAttribute('data-id'); - // It's possible to find this from the input parameters, but it is already available here. - $info['root'] = $html->find('a.logo', 0)->href; - - $this->saveCacheValue($url_cache, $info); - return $info; - } + const PARAMETERS = [ + 'Manga Chapters' => [ + 'url' => [ + 'name' => 'Manga URL', + 'exampleValue' => 'https://live.mangabooth.com/manga/manga-text-chapter/', + 'required' => true + ] + ] + ]; + + public function getName() + { + switch ($this->queriedContext) { + case 'Manga Chapters': + $mangaInfo = $this->getMangaInfo($this->getInput('url')); + return $mangaInfo['title']; + default: + return parent::getName(); + } + } + + public function getURI() + { + return $this->getInput('url') ?? self::URI; + } + + public function collectData() + { + $html = $this->queryAjaxChapters(); + + // Check if the list subcategorizes by volume + $volumes = $html->find('ul.volumns', 0); + if ($volumes) { + $this->parseVolumes($volumes); + } else { + $this->parseChapterList($html, null); + } + } + + protected function queryAjaxChaptersNew() + { + $uri = rtrim($this->getInput('url'), '/') . '/ajax/chapters/'; + $headers = []; + $opts = [CURLOPT_POST => 1]; + return str_get_html(getContents($uri, $headers, $opts)); + } + + protected function queryAjaxChaptersOld() + { + $mangaInfo = $this->getMangaInfo($this->getInput('url')); + $uri = rtrim($mangaInfo['root'], '/') . '/wp-admin/admin-ajax.php'; + $headers = []; + $opts = [CURLOPT_POSTFIELDS => [ + 'action' => 'manga_get_chapters', + 'manga' => $mangaInfo['id'] + ]]; + return str_get_html(getContents($uri, $headers, $opts)); + } + + protected function queryAjaxChapters() + { + $new = $this->queryAjaxChaptersNew(); + if ($new->find('.wp-manga-chapter')) { + return $new; + } else { + return $this->queryAjaxChaptersOld(); + } + } + + protected function parseVolumes($volumes) + { + foreach ($volumes->children(-1) as $volume) { + $volume_name = trim($volume->find('a.has-child', 0)->plaintext); + $this->parseChapterList($volume->find('ul', -1), $volume_name); + } + } + + protected function parseChapterList($chapters, $volume) + { + $mangaInfo = $this->getMangaInfo($this->getInput('url')); + foreach ($chapters->find('li.wp-manga-chapter') as $chap) { + $link = $chap->find('a', 0); + + $item = []; + $item['title'] = ($volume ?? '') . ' ' . trim($link->plaintext); + $item['uri'] = $link->href; + $item['uid'] = $link->href; + $item['timestamp'] = $chap->find('span.chapter-release-date', 0)->plaintext; + $item['author'] = $mangaInfo['author'] ?? null; + $item['categories'] = $mangaInfo['categories'] ?? null; + $this->items[] = $item; + } + } + + /** + * Retrieves manga info from cache or title page. + * The returned array contains 'title', 'author', and 'categories' keys for use in feed items. + * The 'id' key contains the manga title id, used for the old ajax api. + * The 'root' key contains the website root. + * + * @param $url + * @return array + */ + protected function getMangaInfo($url) + { + $url_cache = 'TitleInfo_' . preg_replace('/[^\w]/', '.', rtrim($url, '/')); + $cache = $this->loadCacheValue($url_cache); + if (isset($cache)) { + return $cache; + } + + $info = []; + $html = getSimpleHTMLDOMCached($url); + + $info['title'] = html_entity_decode($html->find('*[property=og:title]', 0)->content); + $author = $html->find('.author-content', 0); + if (!is_null($author)) { + $info['author'] = trim($author->plaintext); + } + $cats = $html->find('.genres-content', 0); + if (!is_null($cats)) { + $info['categories'] = explode(', ', trim($cats->plaintext)); + } + + $info['id'] = $html->find('#manga-chapters-holder', 0)->getAttribute('data-id'); + // It's possible to find this from the input parameters, but it is already available here. + $info['root'] = $html->find('a.logo', 0)->href; + + $this->saveCacheValue($url_cache, $info); + return $info; + } } diff --git a/bridges/WordPressPluginUpdateBridge.php b/bridges/WordPressPluginUpdateBridge.php index 272022dd..a092d72f 100644 --- a/bridges/WordPressPluginUpdateBridge.php +++ b/bridges/WordPressPluginUpdateBridge.php @@ -1,62 +1,64 @@ <?php -final class WordPressPluginUpdateBridge extends BridgeAbstract { - const MAINTAINER = 'dvikan'; - const NAME = 'WordPress Plugins Update Bridge'; - const URI = 'https://wordpress.org/plugins/'; - const DESCRIPTION = 'Returns latest updates of wordpress.org plugins.'; - - const PARAMETERS = [ - [ - // The incorrectly named pluginUrl is kept for BC - 'pluginUrl' => [ - 'name' => 'Plugin slug', - 'exampleValue' => 'akismet', - 'required' => true, - 'title' => 'Slug or url', - ] - ] - ]; - - public function collectData() { - $input = trim($this->getInput('pluginUrl')); - if (preg_match('#https://wordpress\.org/plugins/([\w-]+)#', $input, $m)) { - $slug = $m[1]; - } else { - $slug = str_replace(['/'], '', $input); - } - - $pluginData = self::fetchPluginData($slug); - - if ($pluginData->versions === []) { - throw new \Exception('This plugin does not have versioning data'); - } - - // We don't need trunk. I think it's the latest commit. - unset($pluginData->versions->trunk); - - foreach ($pluginData->versions as $version => $downloadUrl) { - $this->items[] = [ - 'title' => $version, - 'uri' => sprintf('https://wordpress.org/plugins/%s/#developers', $slug), - 'uid' => $downloadUrl, - ]; - } - - usort($this->items, function($a, $b) { - return version_compare($b['title'], $a['title']); - }); - } - - /** - * Fetch plugin data from wordpress.org json api - * - * https://codex.wordpress.org/WordPress.org_API#Plugins - * https://wordpress.org/support/topic/using-the-wordpress-org-api/ - */ - private static function fetchPluginData(string $slug): \stdClass - { - $api = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=%s'; - return json_decode(getContents(sprintf($api, $slug))); - } +final class WordPressPluginUpdateBridge extends BridgeAbstract +{ + const MAINTAINER = 'dvikan'; + const NAME = 'WordPress Plugins Update Bridge'; + const URI = 'https://wordpress.org/plugins/'; + const DESCRIPTION = 'Returns latest updates of wordpress.org plugins.'; + + const PARAMETERS = [ + [ + // The incorrectly named pluginUrl is kept for BC + 'pluginUrl' => [ + 'name' => 'Plugin slug', + 'exampleValue' => 'akismet', + 'required' => true, + 'title' => 'Slug or url', + ] + ] + ]; + + public function collectData() + { + $input = trim($this->getInput('pluginUrl')); + if (preg_match('#https://wordpress\.org/plugins/([\w-]+)#', $input, $m)) { + $slug = $m[1]; + } else { + $slug = str_replace(['/'], '', $input); + } + + $pluginData = self::fetchPluginData($slug); + + if ($pluginData->versions === []) { + throw new \Exception('This plugin does not have versioning data'); + } + + // We don't need trunk. I think it's the latest commit. + unset($pluginData->versions->trunk); + + foreach ($pluginData->versions as $version => $downloadUrl) { + $this->items[] = [ + 'title' => $version, + 'uri' => sprintf('https://wordpress.org/plugins/%s/#developers', $slug), + 'uid' => $downloadUrl, + ]; + } + + usort($this->items, function ($a, $b) { + return version_compare($b['title'], $a['title']); + }); + } + + /** + * Fetch plugin data from wordpress.org json api + * + * https://codex.wordpress.org/WordPress.org_API#Plugins + * https://wordpress.org/support/topic/using-the-wordpress-org-api/ + */ + private static function fetchPluginData(string $slug): \stdClass + { + $api = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=%s'; + return json_decode(getContents(sprintf($api, $slug))); + } } diff --git a/bridges/WorldCosplayBridge.php b/bridges/WorldCosplayBridge.php index b60d7948..cb28eee2 100644 --- a/bridges/WorldCosplayBridge.php +++ b/bridges/WorldCosplayBridge.php @@ -1,142 +1,146 @@ <?php -class WorldCosplayBridge extends BridgeAbstract { - const NAME = 'WorldCosplay Bridge'; - const URI = 'https://worldcosplay.net/'; - const DESCRIPTION = 'Returns WorldCosplay photos'; - const MAINTAINER = 'AxorPL'; - const API_CHARACTER = 'api/photo/list.json?character_id=%u&limit=%u'; - const API_COSPLAYER = 'api/member/photos.json?member_id=%u&limit=%u'; - const API_SERIES = 'api/photo/list.json?title_id=%u&limit=%u'; - const API_TAG = 'api/tag/photo_list.json?id=%u&limit=%u'; +class WorldCosplayBridge extends BridgeAbstract +{ + const NAME = 'WorldCosplay Bridge'; + const URI = 'https://worldcosplay.net/'; + const DESCRIPTION = 'Returns WorldCosplay photos'; + const MAINTAINER = 'AxorPL'; - const CONTENT_HTML - = '<a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>'; + const API_CHARACTER = 'api/photo/list.json?character_id=%u&limit=%u'; + const API_COSPLAYER = 'api/member/photos.json?member_id=%u&limit=%u'; + const API_SERIES = 'api/photo/list.json?title_id=%u&limit=%u'; + const API_TAG = 'api/tag/photo_list.json?id=%u&limit=%u'; - const ERR_CONTEXT = 'No context provided'; - const ERR_QUERY = 'Unable to query: %s'; + const CONTENT_HTML + = '<a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>'; - const LIMIT_MIN = 1; - const LIMIT_MAX = 24; + const ERR_CONTEXT = 'No context provided'; + const ERR_QUERY = 'Unable to query: %s'; - const PARAMETERS = array( - 'Character' => array( - 'cid' => array( - 'name' => 'Character ID', - 'type' => 'number', - 'required' => true, - 'title' => 'WorldCosplay character ID', - 'exampleValue' => 18204 - ) - ), - 'Cosplayer' => array( - 'uid' => array( - 'name' => 'Cosplayer ID', - 'type' => 'number', - 'required' => true, - 'title' => 'Cosplayer\'s WorldCosplay profile ID', - 'exampleValue' => 406782 - ) - ), - 'Series' => array( - 'sid' => array( - 'name' => 'Series ID', - 'type' => 'number', - 'required' => true, - 'title' => 'WorldCosplay series ID', - 'exampleValue' => 3139 - ) - ), - 'Tag' => array( - 'tid' => array( - 'name' => 'Tag ID', - 'type' => 'number', - 'required' => true, - 'title' => 'WorldCosplay tag ID', - 'exampleValue' => 33643 - ) - ), - 'global' => array( - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Maximum number of photos to return', - 'exampleValue' => 5, - 'defaultValue' => 5 - ) - ) - ); + const LIMIT_MIN = 1; + const LIMIT_MAX = 24; - public function collectData() { - $limit = $this->getInput('limit'); - $limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit)); - switch($this->queriedContext) { - case 'Character': - $id = $this->getInput('cid'); - $url = self::API_CHARACTER; - break; - case 'Cosplayer': - $id = $this->getInput('uid'); - $url = self::API_COSPLAYER; - break; - case 'Series': - $id = $this->getInput('sid'); - $url = self::API_SERIES; - break; - case 'Tag': - $id = $this->getInput('tid'); - $url = self::API_TAG; - break; - default: - returnClientError(self::ERR_CONTEXT); - } - $url = self::URI . sprintf($url, $id, $limit); + const PARAMETERS = [ + 'Character' => [ + 'cid' => [ + 'name' => 'Character ID', + 'type' => 'number', + 'required' => true, + 'title' => 'WorldCosplay character ID', + 'exampleValue' => 18204 + ] + ], + 'Cosplayer' => [ + 'uid' => [ + 'name' => 'Cosplayer ID', + 'type' => 'number', + 'required' => true, + 'title' => 'Cosplayer\'s WorldCosplay profile ID', + 'exampleValue' => 406782 + ] + ], + 'Series' => [ + 'sid' => [ + 'name' => 'Series ID', + 'type' => 'number', + 'required' => true, + 'title' => 'WorldCosplay series ID', + 'exampleValue' => 3139 + ] + ], + 'Tag' => [ + 'tid' => [ + 'name' => 'Tag ID', + 'type' => 'number', + 'required' => true, + 'title' => 'WorldCosplay tag ID', + 'exampleValue' => 33643 + ] + ], + 'global' => [ + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Maximum number of photos to return', + 'exampleValue' => 5, + 'defaultValue' => 5 + ] + ] + ]; - $json = json_decode(getContents($url)); - if($json->has_error) { - returnServerError($json->message); - } - $list = $json->list; + public function collectData() + { + $limit = $this->getInput('limit'); + $limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit)); + switch ($this->queriedContext) { + case 'Character': + $id = $this->getInput('cid'); + $url = self::API_CHARACTER; + break; + case 'Cosplayer': + $id = $this->getInput('uid'); + $url = self::API_COSPLAYER; + break; + case 'Series': + $id = $this->getInput('sid'); + $url = self::API_SERIES; + break; + case 'Tag': + $id = $this->getInput('tid'); + $url = self::API_TAG; + break; + default: + returnClientError(self::ERR_CONTEXT); + } + $url = self::URI . sprintf($url, $id, $limit); - foreach($list as $img) { - $image = isset($img->photo) ? $img->photo : $img; - $item = array( - 'uri' => self::URI . substr($image->url, 1), - 'title' => $image->subject, - 'timestamp' => $image->created_at, - 'author' => $img->member->global_name, - 'enclosures' => array($image->large_url), - 'uid' => $image->id, - ); - $item['content'] = sprintf( - self::CONTENT_HTML, - $item['uri'], - $item['enclosures'][0], - $item['title'], - $item['title'] - ); - $this->items[] = $item; - } - } + $json = json_decode(getContents($url)); + if ($json->has_error) { + returnServerError($json->message); + } + $list = $json->list; - public function getName() { - switch($this->queriedContext) { - case 'Character': - $id = $this->getInput('cid'); - break; - case 'Cosplayer': - $id = $this->getInput('uid'); - break; - case 'Series': - $id = $this->getInput('sid'); - break; - case 'Tag': - $id = $this->getInput('tid'); - break; - default: - return parent::getName(); - } - return sprintf('%s %u - ', $this->queriedContext, $id) . self::NAME; - } + foreach ($list as $img) { + $image = isset($img->photo) ? $img->photo : $img; + $item = [ + 'uri' => self::URI . substr($image->url, 1), + 'title' => $image->subject, + 'timestamp' => $image->created_at, + 'author' => $img->member->global_name, + 'enclosures' => [$image->large_url], + 'uid' => $image->id, + ]; + $item['content'] = sprintf( + self::CONTENT_HTML, + $item['uri'], + $item['enclosures'][0], + $item['title'], + $item['title'] + ); + $this->items[] = $item; + } + } + + public function getName() + { + switch ($this->queriedContext) { + case 'Character': + $id = $this->getInput('cid'); + break; + case 'Cosplayer': + $id = $this->getInput('uid'); + break; + case 'Series': + $id = $this->getInput('sid'); + break; + case 'Tag': + $id = $this->getInput('tid'); + break; + default: + return parent::getName(); + } + return sprintf('%s %u - ', $this->queriedContext, $id) . self::NAME; + } } diff --git a/bridges/WorldOfTanksBridge.php b/bridges/WorldOfTanksBridge.php index d48b2d6c..6e7a594b 100644 --- a/bridges/WorldOfTanksBridge.php +++ b/bridges/WorldOfTanksBridge.php @@ -1,58 +1,62 @@ <?php -class WorldOfTanksBridge extends FeedExpander { - - const MAINTAINER = 'Riduidel'; - const NAME = 'World of Tanks'; - const URI = 'https://worldoftanks.eu/'; - const DESCRIPTION = 'News about the tank slaughter game.'; - - const PARAMETERS = array( array( - 'lang' => array( - 'name' => 'Langue', - 'type' => 'list', - 'values' => array( - 'Français' => 'fr', - 'English' => 'en', - 'Español' => 'es', - 'Deutsch' => 'de', - 'Čeština' => 'cs', - 'Polski' => 'pl', - 'Türkçe' => 'tr' - ) - ) - )); - - const POSSIBLE_ARTICLES = array('article', 'rich-article'); - - public function collectData() { - $this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang'))); - } - - protected function parseItem($newsItem){ - $item = parent::parseItem($newsItem); - $item['content'] = $this->loadFullArticle($item['uri']); - return $item; - } - - /** - * Loads the full article and returns the contents - * @param $uri The article URI - * @return The article content - */ - private function loadFullArticle($uri){ - $html = getSimpleHTMLDOMCached($uri); - - foreach(self::POSSIBLE_ARTICLES as $article_class) { - $content = $html->find('article', 0); - - if($content !== null) { - // Remove the scripts, please - foreach($content->find('script') as $script) { - $script->outertext = ''; - } - return $content->innertext; - } - } - return null; - } + +class WorldOfTanksBridge extends FeedExpander +{ + const MAINTAINER = 'Riduidel'; + const NAME = 'World of Tanks'; + const URI = 'https://worldoftanks.eu/'; + const DESCRIPTION = 'News about the tank slaughter game.'; + + const PARAMETERS = [ [ + 'lang' => [ + 'name' => 'Langue', + 'type' => 'list', + 'values' => [ + 'Français' => 'fr', + 'English' => 'en', + 'Español' => 'es', + 'Deutsch' => 'de', + 'Čeština' => 'cs', + 'Polski' => 'pl', + 'Türkçe' => 'tr' + ] + ] + ]]; + + const POSSIBLE_ARTICLES = ['article', 'rich-article']; + + public function collectData() + { + $this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang'))); + } + + protected function parseItem($newsItem) + { + $item = parent::parseItem($newsItem); + $item['content'] = $this->loadFullArticle($item['uri']); + return $item; + } + + /** + * Loads the full article and returns the contents + * @param $uri The article URI + * @return The article content + */ + private function loadFullArticle($uri) + { + $html = getSimpleHTMLDOMCached($uri); + + foreach (self::POSSIBLE_ARTICLES as $article_class) { + $content = $html->find('article', 0); + + if ($content !== null) { + // Remove the scripts, please + foreach ($content->find('script') as $script) { + $script->outertext = ''; + } + return $content->innertext; + } + } + return null; + } } diff --git a/bridges/XPathBridge.php b/bridges/XPathBridge.php index 5aa280e0..98defddc 100644 --- a/bridges/XPathBridge.php +++ b/bridges/XPathBridge.php @@ -1,127 +1,128 @@ <?php -class XPathBridge extends XPathAbstract { - const NAME = 'XPathBridge'; - const URI = 'https://github.com/rss-bridge/rss-bridge'; - const DESCRIPTION - = 'Parse any webpage using <a href="https://devhints.io/xpath" target="_blank">XPath expressions</a>'; - const MAINTAINER = 'Niehztog'; - const PARAMETERS = array( - '' => array( - - 'url' => array( - 'name' => 'Enter web page URL', - 'title' => <<<"EOL" +class XPathBridge extends XPathAbstract +{ + const NAME = 'XPathBridge'; + const URI = 'https://github.com/rss-bridge/rss-bridge'; + const DESCRIPTION + = 'Parse any webpage using <a href="https://devhints.io/xpath" target="_blank">XPath expressions</a>'; + const MAINTAINER = 'Niehztog'; + const PARAMETERS = [ + '' => [ + + 'url' => [ + 'name' => 'Enter web page URL', + 'title' => <<<"EOL" You can specify any website URL which serves data suited for display in RSS feeds (for example a news blog). EOL - , 'type' => 'text', - 'exampleValue' => 'https://news.blizzard.com/en-en', - 'defaultValue' => 'https://news.blizzard.com/en-en', - 'required' => true - ), - - 'item' => array( - 'name' => 'Item selector', - 'title' => <<<"EOL" + , 'type' => 'text', + 'exampleValue' => 'https://news.blizzard.com/en-en', + 'defaultValue' => 'https://news.blizzard.com/en-en', + 'required' => true + ], + + 'item' => [ + 'name' => 'Item selector', + 'title' => <<<"EOL" Enter an XPath expression matching a list of dom nodes, each node containing one feed article item in total (usually a surrounding <div> or <span> tag). This will be the context nodes for all of the following expressions. This expression usually starts with a single forward slash. EOL - , 'type' => 'text', - 'exampleValue' => '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article', - 'defaultValue' => '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article', - 'required' => true - ), - - 'title' => array( - 'name' => 'Item title selector', - 'title' => <<<"EOL" + , 'type' => 'text', + 'exampleValue' => '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article', + 'defaultValue' => '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article', + 'required' => true + ], + + 'title' => [ + 'name' => 'Item title selector', + 'title' => <<<"EOL" This expression should match a node contained within each article item node containing the article headline. It should start with a dot followed by two forward slashes, referring to any descendant nodes of the article item node. EOL - , 'type' => 'text', - 'exampleValue' => './/div/div[2]/h2', - 'defaultValue' => './/div/div[2]/h2', - 'required' => true - ), - - 'content' => array( - 'name' => 'Item description selector', - 'title' => <<<"EOL" + , 'type' => 'text', + 'exampleValue' => './/div/div[2]/h2', + 'defaultValue' => './/div/div[2]/h2', + 'required' => true + ], + + 'content' => [ + 'name' => 'Item description selector', + 'title' => <<<"EOL" This expression should match a node contained within each article item node containing the article content or description. It should start with a dot followed by two forward slashes, referring to any descendant nodes of the article item node. EOL - , 'type' => 'text', - 'exampleValue' => './/div[@class="ArticleListItem-description"]/div[@class="h6"]', - 'defaultValue' => './/div[@class="ArticleListItem-description"]/div[@class="h6"]', - 'required' => false - ), - - 'uri' => array( - 'name' => 'Item URL selector', - 'title' => <<<"EOL" + , 'type' => 'text', + 'exampleValue' => './/div[@class="ArticleListItem-description"]/div[@class="h6"]', + 'defaultValue' => './/div[@class="ArticleListItem-description"]/div[@class="h6"]', + 'required' => false + ], + + 'uri' => [ + 'name' => 'Item URL selector', + 'title' => <<<"EOL" This expression should match a node's attribute containing the article URL (usually the href attribute of an <a> tag). It should start with a dot followed by two forward slashes, referring to any descendant nodes of the article item node. Attributes can be selected by prepending an @ char before the attributes name. EOL - , 'type' => 'text', - 'exampleValue' => './/a[@class="ArticleLink ArticleLink"]/@href', - 'defaultValue' => './/a[@class="ArticleLink ArticleLink"]/@href', - 'required' => false - ), - - 'author' => array( - 'name' => 'Item author selector', - 'title' => <<<"EOL" + , 'type' => 'text', + 'exampleValue' => './/a[@class="ArticleLink ArticleLink"]/@href', + 'defaultValue' => './/a[@class="ArticleLink ArticleLink"]/@href', + 'required' => false + ], + + 'author' => [ + 'name' => 'Item author selector', + 'title' => <<<"EOL" This expression should match a node contained within each article item node containing the article author's name. It should start with a dot followed by two forward slashes, referring to any descendant nodes of the article item node. EOL - , 'type' => 'text', - 'required' => false - ), + , 'type' => 'text', + 'required' => false + ], - 'timestamp' => array( - 'name' => 'Item date selector', - 'title' => <<<"EOL" + 'timestamp' => [ + 'name' => 'Item date selector', + 'title' => <<<"EOL" This expression should match a node or node's attribute containing the article timestamp or date (parsable by PHP's strtotime function). It should start with a dot followed by two forward slashes, referring to any descendant nodes of the article item node. Attributes can be selected by prepending an @ char before the attributes name. EOL - , 'type' => 'text', - 'exampleValue' => './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp', - 'defaultValue' => './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp', - 'required' => false - ), - - 'enclosures' => array( - 'name' => 'Item image selector', - 'title' => <<<"EOL" + , 'type' => 'text', + 'exampleValue' => './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp', + 'defaultValue' => './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp', + 'required' => false + ], + + 'enclosures' => [ + 'name' => 'Item image selector', + 'title' => <<<"EOL" This expression should match a node's attribute containing an article image URL (usually the src attribute of an <img> tag or a style attribute). It should start with a dot followed by two forward slashes, referring to any descendant nodes of the article item node. Attributes can be selected by prepending an @ char before the attributes name. EOL - , 'type' => 'text', - 'exampleValue' => './/div[@class="ArticleListItem-image"]/@style', - 'defaultValue' => './/div[@class="ArticleListItem-image"]/@style', - 'required' => false - ), - - 'categories' => array( - 'name' => 'Item category selector', - 'title' => <<<"EOL" + , 'type' => 'text', + 'exampleValue' => './/div[@class="ArticleListItem-image"]/@style', + 'defaultValue' => './/div[@class="ArticleListItem-image"]/@style', + 'required' => false + ], + + 'categories' => [ + 'name' => 'Item category selector', + 'title' => <<<"EOL" This expression should match a node or node's attribute contained within each article item node containing the article category. This could be inside <div> or <span> tags or sometimes be hidden @@ -130,122 +131,134 @@ forward slashes, referring to any descendant nodes of the article item node. Attributes can be selected by prepending an @ char before the attributes name. EOL - , 'type' => 'text', - 'exampleValue' => './/div[@class="ArticleListItem-label"]', - 'defaultValue' => './/div[@class="ArticleListItem-label"]', - 'required' => false - ), - - 'fix_encoding' => array( - 'name' => 'Fix encoding', - 'title' => <<<"EOL" + , 'type' => 'text', + 'exampleValue' => './/div[@class="ArticleListItem-label"]', + 'defaultValue' => './/div[@class="ArticleListItem-label"]', + 'required' => false + ], + + 'fix_encoding' => [ + 'name' => 'Fix encoding', + 'title' => <<<"EOL" Check this to fix feed encoding by invoking PHP's utf8_decode function on all extracted texts. Try this in case you see "broken" or "weird" characters in your feed where you'd normally expect umlauts or any other non-ascii characters. EOL - , 'type' => 'checkbox', - 'required' => false - ), - - ) - ); - - /** - * Source Web page URL (should provide either HTML or XML content) - * @return string - */ - protected function getSourceUrl(){ - return $this->encodeUri($this->getInput('url')); - } - - /** - * XPath expression for extracting the feed items from the source page - * @return string - */ - protected function getExpressionItem(){ - return urldecode($this->getInput('item')); - } - - /** - * XPath expression for extracting an item title from the item context - * @return string - */ - protected function getExpressionItemTitle(){ - return urldecode($this->getInput('title')); - } - - /** - * XPath expression for extracting an item's content from the item context - * @return string - */ - protected function getExpressionItemContent(){ - return urldecode($this->getInput('content')); - } - - /** - * XPath expression for extracting an item link from the item context - * @return string - */ - protected function getExpressionItemUri(){ - return urldecode($this->getInput('uri')); - } - - /** - * XPath expression for extracting an item author from the item context - * @return string - */ - protected function getExpressionItemAuthor(){ - return urldecode($this->getInput('author')); - } - - /** - * XPath expression for extracting an item timestamp from the item context - * @return string - */ - protected function getExpressionItemTimestamp(){ - return urldecode($this->getInput('timestamp')); - } - - /** - * XPath expression for extracting item enclosures (media content like - * images or movies) from the item context - * @return string - */ - protected function getExpressionItemEnclosures(){ - return urldecode($this->getInput('enclosures')); - } - - /** - * XPath expression for extracting an item category from the item context - * @return string - */ - protected function getExpressionItemCategories(){ - return urldecode($this->getInput('categories')); - } - - /** - * Fix encoding - * @return string - */ - protected function getSettingFixEncoding(){ - return $this->getInput('fix_encoding'); - } - - /** - * Fixes URL encoding issues in input URL's - * @param $uri - * @return string|string[] - */ - private function encodeUri($uri) - { - if (strpos($uri, 'https%3A%2F%2F') === 0 - || strpos($uri, 'http%3A%2F%2F') === 0) { - $uri = urldecode($uri); - } - - $uri = str_replace('|', '%7C', $uri); - - return $uri; - } + , 'type' => 'checkbox', + 'required' => false + ], + + ] + ]; + + /** + * Source Web page URL (should provide either HTML or XML content) + * @return string + */ + protected function getSourceUrl() + { + return $this->encodeUri($this->getInput('url')); + } + + /** + * XPath expression for extracting the feed items from the source page + * @return string + */ + protected function getExpressionItem() + { + return urldecode($this->getInput('item')); + } + + /** + * XPath expression for extracting an item title from the item context + * @return string + */ + protected function getExpressionItemTitle() + { + return urldecode($this->getInput('title')); + } + + /** + * XPath expression for extracting an item's content from the item context + * @return string + */ + protected function getExpressionItemContent() + { + return urldecode($this->getInput('content')); + } + + /** + * XPath expression for extracting an item link from the item context + * @return string + */ + protected function getExpressionItemUri() + { + return urldecode($this->getInput('uri')); + } + + /** + * XPath expression for extracting an item author from the item context + * @return string + */ + protected function getExpressionItemAuthor() + { + return urldecode($this->getInput('author')); + } + + /** + * XPath expression for extracting an item timestamp from the item context + * @return string + */ + protected function getExpressionItemTimestamp() + { + return urldecode($this->getInput('timestamp')); + } + + /** + * XPath expression for extracting item enclosures (media content like + * images or movies) from the item context + * @return string + */ + protected function getExpressionItemEnclosures() + { + return urldecode($this->getInput('enclosures')); + } + + /** + * XPath expression for extracting an item category from the item context + * @return string + */ + protected function getExpressionItemCategories() + { + return urldecode($this->getInput('categories')); + } + + /** + * Fix encoding + * @return string + */ + protected function getSettingFixEncoding() + { + return $this->getInput('fix_encoding'); + } + + /** + * Fixes URL encoding issues in input URL's + * @param $uri + * @return string|string[] + */ + private function encodeUri($uri) + { + if ( + strpos($uri, 'https%3A%2F%2F') === 0 + || strpos($uri, 'http%3A%2F%2F') === 0 + ) { + $uri = urldecode($uri); + } + + $uri = str_replace('|', '%7C', $uri); + + return $uri; + } } diff --git a/bridges/XbooruBridge.php b/bridges/XbooruBridge.php index 2df4f4e1..d4a132e2 100644 --- a/bridges/XbooruBridge.php +++ b/bridges/XbooruBridge.php @@ -1,14 +1,15 @@ <?php -class XbooruBridge extends GelbooruBridge { +class XbooruBridge extends GelbooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Xbooru'; + const URI = 'https://xbooru.com/'; + const DESCRIPTION = 'Returns images from given page'; - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Xbooru'; - const URI = 'https://xbooru.com/'; - const DESCRIPTION = 'Returns images from given page'; - - protected function buildThumbnailURI($element){ - return $this->getURI() . 'thumbnails/' . $element->directory - . '/thumbnail_' . $element->hash . '.jpg'; - } + protected function buildThumbnailURI($element) + { + return $this->getURI() . 'thumbnails/' . $element->directory + . '/thumbnail_' . $element->hash . '.jpg'; + } } diff --git a/bridges/XenForoBridge.php b/bridges/XenForoBridge.php index 4904f6cf..1ecb1d74 100644 --- a/bridges/XenForoBridge.php +++ b/bridges/XenForoBridge.php @@ -1,4 +1,5 @@ <?php + /** * This bridge generates feeds for threads from forums running XenForo version 2 * @@ -13,452 +14,430 @@ * - https://xenforo.com/ * - https://en.wikipedia.org/wiki/XenForo */ -class XenForoBridge extends BridgeAbstract { - - // Bridge specific constants - const CONTEXT_THREAD = 'Thread'; - const XENFORO_VERSION_1 = '1.0'; - const XENFORO_VERSION_2 = '2.0'; - - // RSS-Bridge constants - const NAME = 'XenForo Bridge'; - const URI = 'https://xenforo.com/'; - const DESCRIPTION = 'Generates feeds for threads in forums powered by XenForo'; - const MAINTAINER = 'logmanoriginal'; - const PARAMETERS = array( - self::CONTEXT_THREAD => array( - 'url' => array( - 'name' => 'Thread URL', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert URL to the thread for which the feed should be generated', - 'exampleValue' => 'https://xenforo.com/community/threads/guide-to-suggestions.2285/' - ) - ), - 'global' => array( - 'limit' => array( - 'name' => 'Limit', - 'type' => 'number', - 'required' => false, - 'title' => 'Specify maximum number of elements to return in the feed', - 'defaultValue' => 10 - ) - ) - ); - const CACHE_TIMEOUT = 7200; // 10 minutes - - private $title = ''; - private $threadurl = ''; - private $version; // Holds the XenForo version - - public function getName() { - - switch($this->queriedContext) { - case self::CONTEXT_THREAD: return $this->title . ' - ' . static::NAME; - } - - return parent::getName(); - - } - - public function getURI() { - - switch($this->queriedContext) { - case self::CONTEXT_THREAD: return $this->threadurl; - } - - return parent::getURI(); - - } - - public function collectData() { - - $this->threadurl = filter_var( - $this->getInput('url'), - FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED); - - if($this->threadurl === false) { - returnClientError('The URL you provided is invalid!'); - } - - $urlparts = parse_url($this->threadurl, PHP_URL_SCHEME); - - // Scheme must be "http" or "https" - if(preg_match('/http[s]{0,1}/', parse_url($this->threadurl, PHP_URL_SCHEME)) == false) { - returnClientError('The URL you provided doesn\'t specify a valid scheme (http or https)!'); - } - - // Path cannot be root (../) - if(parse_url($this->threadurl, PHP_URL_PATH) === '/') { - returnClientError('The URL you provided doesn\'t link to a valid thread (root path)!'); - } - - // XenForo adds a thread ID to the URL, like "...-thread.454934283". It must be present - if(preg_match('/.+\.\d+[\/]{0,1}/', parse_URL($this->threadurl, PHP_URL_PATH)) == false) { - returnClientError('The URL you provided doesn\'t link to a valid thread (ID missing)!'); - } - - // We want to start at the first page in the thread. XenForo uses "../page-n" syntax - // to identify pages (except for the first page). - // Notice: XenForo uses the concept of "sentinels" to find and replace parts in the - // URL. Technically forum hosts can change the syntax! - if(preg_match('/.+\/(page-\d+.*)$/', $this->threadurl, $matches) != false) { - - // before: https://xenforo.com/community/threads/guide-to-suggestions.2285/page-5 - // after : https://xenforo.com/community/threads/guide-to-suggestions.2285/ - $this->threadurl = str_replace($matches[1], '', $this->threadurl); - - } - - $html = getSimpleHTMLDOMCached($this->threadurl); - - $html = defaultLinkTo($html, $this->threadurl); - - // Notice: The DOM structure changes depending on the XenForo version used - if($mainContent = $html->find('div.mainContent', 0)) { - $this->version = self::XENFORO_VERSION_1; - } elseif ($mainContent = $html->find('div[class~="p-body"]', 0)) { - $this->version = self::XENFORO_VERSION_2; - } else { - returnServerError('This forum is currently not supported!'); - } - - switch($this->version) { - case self::XENFORO_VERSION_1: - - $titleBar = $mainContent->find('div.titleBar > h1', 0) - or returnServerError('Error finding title bar!'); - - $this->title = $titleBar->plaintext; - - // Store items from current page (we'll use $this->items as LIFO buffer) - $this->extractThreadPostsV1($html, $this->threadurl); - $this->extractPagesV1($html); - - break; - - case self::XENFORO_VERSION_2: - - $titleBar = $mainContent->find('div[class~="p-title"] h1', 0) - or returnServerError('Error finding title bar!'); - - $this->title = $titleBar->plaintext; - $this->extractThreadPostsV2($html, $this->threadurl); - $this->extractPagesV2($html); - - break; - } - - usort($this->items, function($a, $b) { - return $b['timestamp'] <=> $a['timestamp']; - }); - - $this->items = array_slice($this->items, 0, $this->getInput('limit')); - } - - /** - * Extracts thread posts - * @param $html A simplehtmldom object - * @param $url The url from which $html was loaded - */ - private function extractThreadPostsV1($html, $url) { - - $lang = $html->find('html', 0)->lang; - - // Posts are contained in an "ol" - $messageList = $html->find('#messageList > li') - or returnServerError('Error finding message list!'); - - foreach($messageList as $post) { - - if(!isset($post->attr['id'])) { // Skip ads - continue; - } - - $item = array(); - - $item['uri'] = $url . '#' . $post->getAttribute('id'); - - $content = $post->find('.messageContent > article', 0); - - // Add some style to quotes - foreach($content->find('.bbCodeQuote') as $quote) { - $quote->style = ' +class XenForoBridge extends BridgeAbstract +{ + // Bridge specific constants + const CONTEXT_THREAD = 'Thread'; + const XENFORO_VERSION_1 = '1.0'; + const XENFORO_VERSION_2 = '2.0'; + + // RSS-Bridge constants + const NAME = 'XenForo Bridge'; + const URI = 'https://xenforo.com/'; + const DESCRIPTION = 'Generates feeds for threads in forums powered by XenForo'; + const MAINTAINER = 'logmanoriginal'; + const PARAMETERS = [ + self::CONTEXT_THREAD => [ + 'url' => [ + 'name' => 'Thread URL', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert URL to the thread for which the feed should be generated', + 'exampleValue' => 'https://xenforo.com/community/threads/guide-to-suggestions.2285/' + ] + ], + 'global' => [ + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Specify maximum number of elements to return in the feed', + 'defaultValue' => 10 + ] + ] + ]; + const CACHE_TIMEOUT = 7200; // 10 minutes + + private $title = ''; + private $threadurl = ''; + private $version; // Holds the XenForo version + + public function getName() + { + switch ($this->queriedContext) { + case self::CONTEXT_THREAD: + return $this->title . ' - ' . static::NAME; + } + + return parent::getName(); + } + + public function getURI() + { + switch ($this->queriedContext) { + case self::CONTEXT_THREAD: + return $this->threadurl; + } + + return parent::getURI(); + } + + public function collectData() + { + $this->threadurl = filter_var( + $this->getInput('url'), + FILTER_VALIDATE_URL, + FILTER_FLAG_PATH_REQUIRED + ); + + if ($this->threadurl === false) { + returnClientError('The URL you provided is invalid!'); + } + + $urlparts = parse_url($this->threadurl, PHP_URL_SCHEME); + + // Scheme must be "http" or "https" + if (preg_match('/http[s]{0,1}/', parse_url($this->threadurl, PHP_URL_SCHEME)) == false) { + returnClientError('The URL you provided doesn\'t specify a valid scheme (http or https)!'); + } + + // Path cannot be root (../) + if (parse_url($this->threadurl, PHP_URL_PATH) === '/') { + returnClientError('The URL you provided doesn\'t link to a valid thread (root path)!'); + } + + // XenForo adds a thread ID to the URL, like "...-thread.454934283". It must be present + if (preg_match('/.+\.\d+[\/]{0,1}/', parse_URL($this->threadurl, PHP_URL_PATH)) == false) { + returnClientError('The URL you provided doesn\'t link to a valid thread (ID missing)!'); + } + + // We want to start at the first page in the thread. XenForo uses "../page-n" syntax + // to identify pages (except for the first page). + // Notice: XenForo uses the concept of "sentinels" to find and replace parts in the + // URL. Technically forum hosts can change the syntax! + if (preg_match('/.+\/(page-\d+.*)$/', $this->threadurl, $matches) != false) { + // before: https://xenforo.com/community/threads/guide-to-suggestions.2285/page-5 + // after : https://xenforo.com/community/threads/guide-to-suggestions.2285/ + $this->threadurl = str_replace($matches[1], '', $this->threadurl); + } + + $html = getSimpleHTMLDOMCached($this->threadurl); + + $html = defaultLinkTo($html, $this->threadurl); + + // Notice: The DOM structure changes depending on the XenForo version used + if ($mainContent = $html->find('div.mainContent', 0)) { + $this->version = self::XENFORO_VERSION_1; + } elseif ($mainContent = $html->find('div[class~="p-body"]', 0)) { + $this->version = self::XENFORO_VERSION_2; + } else { + returnServerError('This forum is currently not supported!'); + } + + switch ($this->version) { + case self::XENFORO_VERSION_1: + $titleBar = $mainContent->find('div.titleBar > h1', 0) + or returnServerError('Error finding title bar!'); + + $this->title = $titleBar->plaintext; + + // Store items from current page (we'll use $this->items as LIFO buffer) + $this->extractThreadPostsV1($html, $this->threadurl); + $this->extractPagesV1($html); + + break; + + case self::XENFORO_VERSION_2: + $titleBar = $mainContent->find('div[class~="p-title"] h1', 0) + or returnServerError('Error finding title bar!'); + + $this->title = $titleBar->plaintext; + $this->extractThreadPostsV2($html, $this->threadurl); + $this->extractPagesV2($html); + + break; + } + + usort($this->items, function ($a, $b) { + return $b['timestamp'] <=> $a['timestamp']; + }); + + $this->items = array_slice($this->items, 0, $this->getInput('limit')); + } + + /** + * Extracts thread posts + * @param $html A simplehtmldom object + * @param $url The url from which $html was loaded + */ + private function extractThreadPostsV1($html, $url) + { + $lang = $html->find('html', 0)->lang; + + // Posts are contained in an "ol" + $messageList = $html->find('#messageList > li') + or returnServerError('Error finding message list!'); + + foreach ($messageList as $post) { + if (!isset($post->attr['id'])) { // Skip ads + continue; + } + + $item = []; + + $item['uri'] = $url . '#' . $post->getAttribute('id'); + + $content = $post->find('.messageContent > article', 0); + + // Add some style to quotes + foreach ($content->find('.bbCodeQuote') as $quote) { + $quote->style = ' color: #495566; background-color: rgb(248,251,253); border: 1px solid rgb(111, 140, 180); border-color: rgb(111, 140, 180); font-style: italic;'; - } - - // Remove script tags - foreach($content->find('script') as $script) { - $script->outertext = ''; - } - - $item['content'] = $content->innertext; - - // Remove quotes (for the title) - foreach($content->find('.bbCodeQuote') as $quote) { - $quote->innertext = ''; - } - - $title = trim($content->plaintext); - - if(strlen($title) > 70) { - $item['title'] = substr($title, 0, strpos($title, ' ', 70)) . '...'; - } else { - $item['title'] = $title; - } - - /** - * Timestamps are presented in two forms: - * - * 1) short version (for older posts?) - * <span - * class="DateTime" - * title="22 Oct. 2018 at 23:47" - * >22 Oct. 2018</span> - * - * This form has to be interpreted depending on the current language. - * - * 2) long version (for newer posts?) - * <abbr - * class="DateTime" - * data-time="1541008785" - * data-diff="310694" - * data-datestring="31 Oct. 2018" - * data-timestring="18:59" - * title="31 Oct. 2018 at 18:59" - * >Wednesday at 18:59</abbr> - * - * This form has the timestamp embedded (data-time) - */ - if($timestamp = $post->find('abbr.DateTime', 0)) { // long version (preffered) - $item['timestamp'] = $timestamp->{'data-time'}; - } elseif($timestamp = $post->find('span.DateTime', 0)) { // short version - $item['timestamp'] = $this->fixDate($timestamp->title, $lang); - } - - $item['author'] = $post->getAttribute('data-author'); - - // Bridge specific properties - $item['id'] = $post->getAttribute('id'); - - $this->items[] = $item; - - } - - } - - private function extractThreadPostsV2($html, $url) { - - $lang = $html->find('html', 0)->lang; - - $messageList = $html->find('div[class~="block-body"] article') - or returnServerError('Error finding message list!'); - - foreach($messageList as $post) { - - if(!isset($post->attr['id'])) { // Skip ads - continue; - } - - $item = array(); - - $item['uri'] = $url . '#' . $post->getAttribute('id'); - - $title = $post->find('div[class~="message-content"] article', 0)->plaintext; - $end = strpos($title, ' ', min(70, strlen($title))); - $item['title'] = substr($title, 0, $end); - - if ($post->find('time[datetime]', 0)) { - $item['timestamp'] = $post->find('time[datetime]', 0)->datetime; - } else { - $item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang); - } - $item['author'] = $post->getAttribute('data-author'); - $item['content'] = $post->find('div[class~="message-content"] article', 0); - - // Bridge specific properties - $item['id'] = $post->getAttribute('id'); - - $this->items[] = $item; - - } - - } - - private function extractPagesV1($html) { - - // A navigation bar becomes available if the number of posts grows too - // high. When this happens we need to load further pages (from last backwards) - if(($pageNav = $html->find('div.PageNav', 0))) { - - $lastpage = $pageNav->{'data-last'}; - $baseurl = $pageNav->{'data-baseurl'}; - $sentinel = $pageNav->{'data-sentinel'}; - - $hosturl = parse_url($this->threadurl, PHP_URL_SCHEME) - . '://' - . parse_url($this->threadurl, PHP_URL_HOST) - . '/'; - - $page = $lastpage; - - // Load at least the last page - do { - - $pageurl = str_replace($sentinel, $lastpage, $baseurl); - - // We can optimize performance by caching all but the last page - if($page != $lastpage) { - $html = getSimpleHTMLDOMCached($pageurl) - or returnServerError('Error loading contents from ' . $pageurl . '!'); - } else { - $html = getSimpleHTMLDOM($pageurl) - or returnServerError('Error loading contents from ' . $pageurl . '!'); - } - - $html = defaultLinkTo($html, $hosturl); - - $this->extractThreadPostsV1($html, $pageurl); - - $page--; - - } while (count($this->items) < $this->getInput('limit') && $page != 1); - - } - - } - - private function extractPagesV2($html) { - - // A navigation bar becomes available if the number of posts grows too - // high. When this happens we need to load further pages (from last backwards) - if(($pageNav = $html->find('div.pageNav', 0))) { - - foreach($pageNav->find('li') as $nav) { - $lastpage = $nav->plaintext; - } - - // Manually extract baseurl and inject sentinel - $baseurl = $pageNav->find('li > a', -1)->href; - $baseurl = str_replace('page-' . $lastpage, 'page-{{sentinel}}', $baseurl); - - $sentinel = '{{sentinel}}'; - - $hosturl = parse_url($this->threadurl, PHP_URL_SCHEME) - . '://' - . parse_url($this->threadurl, PHP_URL_HOST); - - $page = $lastpage; - - // Load at least the last page - do { - - $pageurl = str_replace($sentinel, $lastpage, $baseurl); - - // We can optimize performance by caching all but the last page - if($page != $lastpage) { - $html = getSimpleHTMLDOMCached($pageurl) - or returnServerError('Error loading contents from ' . $pageurl . '!'); - } else { - $html = getSimpleHTMLDOM($pageurl) - or returnServerError('Error loading contents from ' . $pageurl . '!'); - } - - $html = defaultLinkTo($html, $hosturl); - - $this->extractThreadPostsV2($html, $pageurl); - - $page--; - - } while (count($this->items) < $this->getInput('limit') && $page != 1); - - } - - } - - /** - * Fixes dates depending on the choosen language: - * - * de : dd.mm.yy - * en : dd.mm.yy - * it : dd/mm/yy - * - * Basically strtotime doesn't convert dates correctly due to formats - * being hard to interpret. So we use the DateTime object. - * - * We don't know the timezone, so just assume +00:00 (or whatever - * DateTime chooses) - */ - private function fixDate($date, $lang = 'en-US') { - - $mnamesen = array( - 'January', - 'Feburary', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' - ); - - switch($lang) { - case 'en-US': // example: Jun 9, 2018 at 11:46 PM - - $df = date_create_from_format('M d, Y \a\t H:i A', $date); - break; - - case 'de-DE': // example: 19 Juli 2018 um 19:27 Uhr - - $mnamesde = array( - 'Januar', - 'Februar', - 'März', - 'April', - 'Mai', - 'Juni', - 'Juli', - 'August', - 'September', - 'Oktober', - 'November', - 'Dezember' - ); - - $mnamesdeshort = array( - 'Jan.', - 'Feb.', - 'Mär.', - 'Apr.', - 'Mai', - 'Juni', - 'Juli', - 'Aug.', - 'Sep.', - 'Okt.', - 'Nov.', - 'Dez.' - ); - - $date = str_ireplace($mnamesde, $mnamesen, $date); - $date = str_ireplace($mnamesdeshort, $mnamesen, $date); - - $df = date_create_from_format('d M Y \u\m H:i \U\h\r', $date); - break; - - } - - // Debug::log(date_format($df, 'U')); - - return date_format($df, 'U'); - - } + } + + // Remove script tags + foreach ($content->find('script') as $script) { + $script->outertext = ''; + } + + $item['content'] = $content->innertext; + + // Remove quotes (for the title) + foreach ($content->find('.bbCodeQuote') as $quote) { + $quote->innertext = ''; + } + + $title = trim($content->plaintext); + + if (strlen($title) > 70) { + $item['title'] = substr($title, 0, strpos($title, ' ', 70)) . '...'; + } else { + $item['title'] = $title; + } + + /** + * Timestamps are presented in two forms: + * + * 1) short version (for older posts?) + * <span + * class="DateTime" + * title="22 Oct. 2018 at 23:47" + * >22 Oct. 2018</span> + * + * This form has to be interpreted depending on the current language. + * + * 2) long version (for newer posts?) + * <abbr + * class="DateTime" + * data-time="1541008785" + * data-diff="310694" + * data-datestring="31 Oct. 2018" + * data-timestring="18:59" + * title="31 Oct. 2018 at 18:59" + * >Wednesday at 18:59</abbr> + * + * This form has the timestamp embedded (data-time) + */ + if ($timestamp = $post->find('abbr.DateTime', 0)) { // long version (preffered) + $item['timestamp'] = $timestamp->{'data-time'}; + } elseif ($timestamp = $post->find('span.DateTime', 0)) { // short version + $item['timestamp'] = $this->fixDate($timestamp->title, $lang); + } + + $item['author'] = $post->getAttribute('data-author'); + + // Bridge specific properties + $item['id'] = $post->getAttribute('id'); + + $this->items[] = $item; + } + } + + private function extractThreadPostsV2($html, $url) + { + $lang = $html->find('html', 0)->lang; + + $messageList = $html->find('div[class~="block-body"] article') + or returnServerError('Error finding message list!'); + + foreach ($messageList as $post) { + if (!isset($post->attr['id'])) { // Skip ads + continue; + } + + $item = []; + + $item['uri'] = $url . '#' . $post->getAttribute('id'); + + $title = $post->find('div[class~="message-content"] article', 0)->plaintext; + $end = strpos($title, ' ', min(70, strlen($title))); + $item['title'] = substr($title, 0, $end); + + if ($post->find('time[datetime]', 0)) { + $item['timestamp'] = $post->find('time[datetime]', 0)->datetime; + } else { + $item['timestamp'] = $this->fixDate($post->find('time', 0)->title, $lang); + } + $item['author'] = $post->getAttribute('data-author'); + $item['content'] = $post->find('div[class~="message-content"] article', 0); + + // Bridge specific properties + $item['id'] = $post->getAttribute('id'); + + $this->items[] = $item; + } + } + + private function extractPagesV1($html) + { + // A navigation bar becomes available if the number of posts grows too + // high. When this happens we need to load further pages (from last backwards) + if (($pageNav = $html->find('div.PageNav', 0))) { + $lastpage = $pageNav->{'data-last'}; + $baseurl = $pageNav->{'data-baseurl'}; + $sentinel = $pageNav->{'data-sentinel'}; + + $hosturl = parse_url($this->threadurl, PHP_URL_SCHEME) + . '://' + . parse_url($this->threadurl, PHP_URL_HOST) + . '/'; + + $page = $lastpage; + + // Load at least the last page + do { + $pageurl = str_replace($sentinel, $lastpage, $baseurl); + + // We can optimize performance by caching all but the last page + if ($page != $lastpage) { + $html = getSimpleHTMLDOMCached($pageurl) + or returnServerError('Error loading contents from ' . $pageurl . '!'); + } else { + $html = getSimpleHTMLDOM($pageurl) + or returnServerError('Error loading contents from ' . $pageurl . '!'); + } + + $html = defaultLinkTo($html, $hosturl); + + $this->extractThreadPostsV1($html, $pageurl); + + $page--; + } while (count($this->items) < $this->getInput('limit') && $page != 1); + } + } + + private function extractPagesV2($html) + { + // A navigation bar becomes available if the number of posts grows too + // high. When this happens we need to load further pages (from last backwards) + if (($pageNav = $html->find('div.pageNav', 0))) { + foreach ($pageNav->find('li') as $nav) { + $lastpage = $nav->plaintext; + } + + // Manually extract baseurl and inject sentinel + $baseurl = $pageNav->find('li > a', -1)->href; + $baseurl = str_replace('page-' . $lastpage, 'page-{{sentinel}}', $baseurl); + + $sentinel = '{{sentinel}}'; + + $hosturl = parse_url($this->threadurl, PHP_URL_SCHEME) + . '://' + . parse_url($this->threadurl, PHP_URL_HOST); + + $page = $lastpage; + + // Load at least the last page + do { + $pageurl = str_replace($sentinel, $lastpage, $baseurl); + + // We can optimize performance by caching all but the last page + if ($page != $lastpage) { + $html = getSimpleHTMLDOMCached($pageurl) + or returnServerError('Error loading contents from ' . $pageurl . '!'); + } else { + $html = getSimpleHTMLDOM($pageurl) + or returnServerError('Error loading contents from ' . $pageurl . '!'); + } + + $html = defaultLinkTo($html, $hosturl); + + $this->extractThreadPostsV2($html, $pageurl); + + $page--; + } while (count($this->items) < $this->getInput('limit') && $page != 1); + } + } + + /** + * Fixes dates depending on the choosen language: + * + * de : dd.mm.yy + * en : dd.mm.yy + * it : dd/mm/yy + * + * Basically strtotime doesn't convert dates correctly due to formats + * being hard to interpret. So we use the DateTime object. + * + * We don't know the timezone, so just assume +00:00 (or whatever + * DateTime chooses) + */ + private function fixDate($date, $lang = 'en-US') + { + $mnamesen = [ + 'January', + 'Feburary', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + + switch ($lang) { + case 'en-US': // example: Jun 9, 2018 at 11:46 PM + $df = date_create_from_format('M d, Y \a\t H:i A', $date); + break; + + case 'de-DE': // example: 19 Juli 2018 um 19:27 Uhr + $mnamesde = [ + 'Januar', + 'Februar', + 'März', + 'April', + 'Mai', + 'Juni', + 'Juli', + 'August', + 'September', + 'Oktober', + 'November', + 'Dezember' + ]; + + $mnamesdeshort = [ + 'Jan.', + 'Feb.', + 'Mär.', + 'Apr.', + 'Mai', + 'Juni', + 'Juli', + 'Aug.', + 'Sep.', + 'Okt.', + 'Nov.', + 'Dez.' + ]; + + $date = str_ireplace($mnamesde, $mnamesen, $date); + $date = str_ireplace($mnamesdeshort, $mnamesen, $date); + + $df = date_create_from_format('d M Y \u\m H:i \U\h\r', $date); + break; + } + + // Debug::log(date_format($df, 'U')); + + return date_format($df, 'U'); + } } diff --git a/bridges/YGGTorrentBridge.php b/bridges/YGGTorrentBridge.php index 30b5ca7a..f0c31f11 100644 --- a/bridges/YGGTorrentBridge.php +++ b/bridges/YGGTorrentBridge.php @@ -3,148 +3,156 @@ /* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge * by erwang.providing the functionality of both in one. */ -class YGGTorrentBridge extends BridgeAbstract { +class YGGTorrentBridge extends BridgeAbstract +{ + const MAINTAINER = 'teromene'; + const NAME = 'Yggtorrent Bridge'; + const URI = 'https://www5.yggtorrent.fi'; + const DESCRIPTION = 'Returns torrent search from Yggtorrent'; - const MAINTAINER = 'teromene'; - const NAME = 'Yggtorrent Bridge'; - const URI = 'https://www5.yggtorrent.fi'; - const DESCRIPTION = 'Returns torrent search from Yggtorrent'; + const PARAMETERS = [ + [ + 'cat' => [ + 'name' => 'category', + 'type' => 'list', + 'values' => [ + 'Toutes les catégories' => 'all.all', + 'Film/Vidéo - Toutes les sous-catégories' => '2145.all', + 'Film/Vidéo - Animation' => '2145.2178', + 'Film/Vidéo - Animation Série' => '2145.2179', + 'Film/Vidéo - Concert' => '2145.2180', + 'Film/Vidéo - Documentaire' => '2145.2181', + 'Film/Vidéo - Émission TV' => '2145.2182', + 'Film/Vidéo - Film' => '2145.2183', + 'Film/Vidéo - Série TV' => '2145.2184', + 'Film/Vidéo - Spectacle' => '2145.2185', + 'Film/Vidéo - Sport' => '2145.2186', + 'Film/Vidéo - Vidéo-clips' => '2145.2186', + 'Audio - Toutes les sous-catégories' => '2139.all', + 'Audio - Karaoké' => '2139.2147', + 'Audio - Musique' => '2139.2148', + 'Audio - Podcast Radio' => '2139.2150', + 'Audio - Samples' => '2139.2149', + 'Jeu vidéo - Toutes les sous-catégories' => '2142.all', + 'Jeu vidéo - Autre' => '2142.2167', + 'Jeu vidéo - Linux' => '2142.2159', + 'Jeu vidéo - MacOS' => '2142.2160', + 'Jeu vidéo - Microsoft' => '2142.2162', + 'Jeu vidéo - Nintendo' => '2142.2163', + 'Jeu vidéo - Smartphone' => '2142.2165', + 'Jeu vidéo - Sony' => '2142.2164', + 'Jeu vidéo - Tablette' => '2142.2166', + 'Jeu vidéo - Windows' => '2142.2161', + 'eBook - Toutes les sous-catégories' => '2140.all', + 'eBook - Audio' => '2140.2151', + 'eBook - Bds' => '2140.2152', + 'eBook - Comics' => '2140.2153', + 'eBook - Livres' => '2140.2154', + 'eBook - Mangas' => '2140.2155', + 'eBook - Presse' => '2140.2156', + 'Emulation - Toutes les sous-catégories' => '2141.all', + 'Emulation - Emulateurs' => '2141.2157', + 'Emulation - Roms' => '2141.2158', + 'GPS - Toutes les sous-catégories' => '2141.all', + 'GPS - Applications' => '2141.2168', + 'GPS - Cartes' => '2141.2169', + 'GPS - Divers' => '2141.2170' + ] + ], + 'nom' => [ + 'name' => 'Nom', + 'description' => 'Nom du torrent', + 'type' => 'text', + 'exampleValue' => 'matrix' + ], + 'description' => [ + 'name' => 'Description', + 'description' => 'Description du torrent', + 'type' => 'text' + ], + 'fichier' => [ + 'name' => 'Fichier', + 'description' => 'Fichier du torrent', + 'type' => 'text' + ], + 'uploader' => [ + 'name' => 'Uploader', + 'description' => 'Uploader du torrent', + 'type' => 'text' + ], - const PARAMETERS = array( - array( - 'cat' => array( - 'name' => 'category', - 'type' => 'list', - 'values' => array( - 'Toutes les catégories' => 'all.all', - 'Film/Vidéo - Toutes les sous-catégories' => '2145.all', - 'Film/Vidéo - Animation' => '2145.2178', - 'Film/Vidéo - Animation Série' => '2145.2179', - 'Film/Vidéo - Concert' => '2145.2180', - 'Film/Vidéo - Documentaire' => '2145.2181', - 'Film/Vidéo - Émission TV' => '2145.2182', - 'Film/Vidéo - Film' => '2145.2183', - 'Film/Vidéo - Série TV' => '2145.2184', - 'Film/Vidéo - Spectacle' => '2145.2185', - 'Film/Vidéo - Sport' => '2145.2186', - 'Film/Vidéo - Vidéo-clips' => '2145.2186', - 'Audio - Toutes les sous-catégories' => '2139.all', - 'Audio - Karaoké' => '2139.2147', - 'Audio - Musique' => '2139.2148', - 'Audio - Podcast Radio' => '2139.2150', - 'Audio - Samples' => '2139.2149', - 'Jeu vidéo - Toutes les sous-catégories' => '2142.all', - 'Jeu vidéo - Autre' => '2142.2167', - 'Jeu vidéo - Linux' => '2142.2159', - 'Jeu vidéo - MacOS' => '2142.2160', - 'Jeu vidéo - Microsoft' => '2142.2162', - 'Jeu vidéo - Nintendo' => '2142.2163', - 'Jeu vidéo - Smartphone' => '2142.2165', - 'Jeu vidéo - Sony' => '2142.2164', - 'Jeu vidéo - Tablette' => '2142.2166', - 'Jeu vidéo - Windows' => '2142.2161', - 'eBook - Toutes les sous-catégories' => '2140.all', - 'eBook - Audio' => '2140.2151', - 'eBook - Bds' => '2140.2152', - 'eBook - Comics' => '2140.2153', - 'eBook - Livres' => '2140.2154', - 'eBook - Mangas' => '2140.2155', - 'eBook - Presse' => '2140.2156', - 'Emulation - Toutes les sous-catégories' => '2141.all', - 'Emulation - Emulateurs' => '2141.2157', - 'Emulation - Roms' => '2141.2158', - 'GPS - Toutes les sous-catégories' => '2141.all', - 'GPS - Applications' => '2141.2168', - 'GPS - Cartes' => '2141.2169', - 'GPS - Divers' => '2141.2170' - ) - ), - 'nom' => array( - 'name' => 'Nom', - 'description' => 'Nom du torrent', - 'type' => 'text', - 'exampleValue' => 'matrix' - ), - 'description' => array( - 'name' => 'Description', - 'description' => 'Description du torrent', - 'type' => 'text' - ), - 'fichier' => array( - 'name' => 'Fichier', - 'description' => 'Fichier du torrent', - 'type' => 'text' - ), - 'uploader' => array( - 'name' => 'Uploader', - 'description' => 'Uploader du torrent', - 'type' => 'text' - ), + ] + ]; - ) - ); + public function collectData() + { + $catInfo = explode('.', $this->getInput('cat')); + $category = $catInfo[0]; + $subcategory = $catInfo[1]; - public function collectData() { - $catInfo = explode('.', $this->getInput('cat')); - $category = $catInfo[0]; - $subcategory = $catInfo[1]; + $html = getSimpleHTMLDOM(self::URI . '/engine/search?name=' + . $this->getInput('nom') + . '&description=' + . $this->getInput('description') + . '&file=' + . $this->getInput('fichier') + . '&uploader=' + . $this->getInput('uploader') + . '&category=' + . $category + . '&sub_category=' + . $subcategory + . '&do=search&order=desc&sort=publish_date'); - $html = getSimpleHTMLDOM(self::URI . '/engine/search?name=' - . $this->getInput('nom') - . '&description=' - . $this->getInput('description') - . '&file=' - . $this->getInput('fichier') - . '&uploader=' - . $this->getInput('uploader') - . '&category=' - . $category - . '&sub_category=' - . $subcategory - . '&do=search&order=desc&sort=publish_date'); + $count = 0; + $results = $html->find('.results', 0); + if (!$results) { + return; + } - $count = 0; - $results = $html->find('.results', 0); - if(!$results) return; + foreach ($results->find('tr') as $row) { + $count++; + if ($count == 1) { + continue; // Skip table header + } + if ($count == 22) { + break; // Stop processing after 21 items (20 + 1 table header) + } + $item = []; + $item['timestamp'] = $row->find('.hidden', 1)->plaintext; + $item['title'] = $row->find('a#torrent_name', 0)->plaintext; + $item['uri'] = $this->processLink($row->find('a#torrent_name', 0)->href); + $item['seeders'] = $row->find('td', 7)->plaintext; + $item['leechers'] = $row->find('td', 8)->plaintext; + $item['size'] = $row->find('td', 5)->plaintext; + $item = array_merge($item, $this->collectTorrentData($item['uri'])); - foreach($results->find('tr') as $row) { - $count++; - if($count == 1) continue; // Skip table header - if($count == 22) break; // Stop processing after 21 items (20 + 1 table header) - $item = array(); - $item['timestamp'] = $row->find('.hidden', 1)->plaintext; - $item['title'] = $row->find('a#torrent_name', 0)->plaintext; - $item['uri'] = $this->processLink($row->find('a#torrent_name', 0)->href); - $item['seeders'] = $row->find('td', 7)->plaintext; - $item['leechers'] = $row->find('td', 8)->plaintext; - $item['size'] = $row->find('td', 5)->plaintext; - $item = array_merge($item, $this->collectTorrentData($item['uri'])); + $this->items[] = $item; + } + } - $this->items[] = $item; - } + /** + * Convert special characters like é to %C3%A9 in the url + */ + private function processLink($url) + { + $url = explode('/', $url); + foreach ($url as $index => $value) { + // Skip https://{self::URI}/ + if ($index < 3) { + continue; + } + // Decode first so that characters like + are not encoded + $url[$index] = urlencode(urldecode($value)); + } + return implode('/', $url); + } - } - - /** - * Convert special characters like é to %C3%A9 in the url - */ - private function processLink($url) { - $url = explode('/', $url); - foreach($url as $index => $value) { - // Skip https://{self::URI}/ - if ($index < 3) { - continue; - } - // Decode first so that characters like + are not encoded - $url[$index] = urlencode(urldecode($value)); - } - return implode('/', $url); - } - - private function collectTorrentData($url) { - $page = defaultLinkTo(getSimpleHTMLDOMCached($url), self::URI); - $author = $page->find('.informations tr', 5)->find('td', 1)->plaintext; - $content = $page->find('.default', 1); - return array('author' => $author, 'content' => $content); - } + private function collectTorrentData($url) + { + $page = defaultLinkTo(getSimpleHTMLDOMCached($url), self::URI); + $author = $page->find('.informations tr', 5)->find('td', 1)->plaintext; + $content = $page->find('.default', 1); + return ['author' => $author, 'content' => $content]; + } } diff --git a/bridges/YandereBridge.php b/bridges/YandereBridge.php index 9a93ef6c..0dc11022 100644 --- a/bridges/YandereBridge.php +++ b/bridges/YandereBridge.php @@ -1,10 +1,9 @@ <?php -class YandereBridge extends MoebooruBridge { - - const MAINTAINER = 'mitsukarenai'; - const NAME = 'Yande.re'; - const URI = 'https://yande.re/'; - const DESCRIPTION = 'Returns images from given page and tags'; - +class YandereBridge extends MoebooruBridge +{ + const MAINTAINER = 'mitsukarenai'; + const NAME = 'Yande.re'; + const URI = 'https://yande.re/'; + const DESCRIPTION = 'Returns images from given page and tags'; } diff --git a/bridges/YeggiBridge.php b/bridges/YeggiBridge.php index e08a8426..07f1dd4d 100644 --- a/bridges/YeggiBridge.php +++ b/bridges/YeggiBridge.php @@ -1,98 +1,100 @@ <?php -class YeggiBridge extends BridgeAbstract { - const NAME = 'Yeggi Search'; - const URI = 'https://www.yeggi.com'; - const DESCRIPTION = 'Returns 3D Models from Thingiverse, MyMiniFactory, Cults3D, and more'; - const MAINTAINER = 'AntoineTurmel'; - const PARAMETERS = array( - array( - 'query' => array( - 'name' => 'Search query', - 'type' => 'text', - 'required' => true, - 'title' => 'Insert your search term here', - 'exampleValue' => 'vase' - ), - 'sortby' => array( - 'name' => 'Sort by', - 'type' => 'list', - 'required' => false, - 'values' => array( - 'Best match' => '0', - 'Popular' => '1', - 'Latest' => '2', - ), - 'defaultValue' => 'newest' - ), - 'show' => array( - 'name' => 'Show', - 'type' => 'list', - 'required' => false, - 'values' => array( - 'All' => '0', - 'Free' => '1', - 'For sale' => '2', - ), - 'defaultValue' => 'all' - ), - 'showimage' => array( - 'name' => 'Show image in content', - 'type' => 'checkbox', - 'required' => false, - 'title' => 'Activate to show the image in the content', - 'defaultValue' => 'checked' - ) - ) - ); +class YeggiBridge extends BridgeAbstract +{ + const NAME = 'Yeggi Search'; + const URI = 'https://www.yeggi.com'; + const DESCRIPTION = 'Returns 3D Models from Thingiverse, MyMiniFactory, Cults3D, and more'; + const MAINTAINER = 'AntoineTurmel'; + const PARAMETERS = [ + [ + 'query' => [ + 'name' => 'Search query', + 'type' => 'text', + 'required' => true, + 'title' => 'Insert your search term here', + 'exampleValue' => 'vase' + ], + 'sortby' => [ + 'name' => 'Sort by', + 'type' => 'list', + 'required' => false, + 'values' => [ + 'Best match' => '0', + 'Popular' => '1', + 'Latest' => '2', + ], + 'defaultValue' => 'newest' + ], + 'show' => [ + 'name' => 'Show', + 'type' => 'list', + 'required' => false, + 'values' => [ + 'All' => '0', + 'Free' => '1', + 'For sale' => '2', + ], + 'defaultValue' => 'all' + ], + 'showimage' => [ + 'name' => 'Show image in content', + 'type' => 'checkbox', + 'required' => false, + 'title' => 'Activate to show the image in the content', + 'defaultValue' => 'checked' + ] + ] + ]; - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); - $results = $html->find('div.item_1_A'); + $results = $html->find('div.item_1_A'); - foreach($results as $result) { + foreach ($results as $result) { + $item = []; + $title = $result->find('.item_3_B_2', 0)->plaintext; + $explodeTitle = explode(' ', $title); + if (count($explodeTitle) == 2) { + $item['title'] = $explodeTitle[1]; + } else { + $item['title'] = $explodeTitle[0]; + } + $item['uri'] = self::URI . $result->find('a', 0)->href; + $item['author'] = 'Yeggi'; - $item = array(); - $title = $result->find('.item_3_B_2', 0)->plaintext; - $explodeTitle = explode(' ', $title); - if(count($explodeTitle) == 2) { - $item['title'] = $explodeTitle[1]; - } else { - $item['title'] = $explodeTitle[0]; - } - $item['uri'] = self::URI . $result->find('a', 0)->href; - $item['author'] = 'Yeggi'; + $text = $result->find('i'); + $item['content'] = $text[0]->plaintext . ' on ' . $text[1]->plaintext; + $item['uid'] = hash('md5', $item['title']); - $text = $result->find('i'); - $item['content'] = $text[0]->plaintext . ' on ' . $text[1]->plaintext; - $item['uid'] = hash('md5', $item['title']); + foreach ($result->find('.item_3_B_2 > a[href^=/q/]') as $tag) { + $item['tags'][] = $tag->plaintext; + } - foreach($result->find('.item_3_B_2 > a[href^=/q/]') as $tag) { - $item['tags'][] = $tag->plaintext; - } + $image = $result->find('img', 0)->src; - $image = $result->find('img', 0)->src; + if ($this->getInput('showimage')) { + $item['content'] .= '<br><img src="' . $image . '">'; + } - if($this->getInput('showimage')) { - $item['content'] .= '<br><img src="' . $image . '">'; - } + $item['enclosures'] = [$image]; - $item['enclosures'] = array($image); + $this->items[] = $item; + } + } - $this->items[] = $item; - } - } + public function getURI() + { + if (!is_null($this->getInput('query'))) { + $uri = self::URI . '/q/' . urlencode($this->getInput('query')) . '/'; + $uri .= '?o_f=' . $this->getInput('show'); + $uri .= '&o_s=' . $this->getInput('sortby'); - public function getURI(){ - if(!is_null($this->getInput('query'))) { - $uri = self::URI . '/q/' . urlencode($this->getInput('query')) . '/'; - $uri .= '?o_f=' . $this->getInput('show'); - $uri .= '&o_s=' . $this->getInput('sortby'); + return $uri; + } - return $uri; - } - - return parent::getURI(); - } + return parent::getURI(); + } } diff --git a/bridges/YouTubeCommunityTabBridge.php b/bridges/YouTubeCommunityTabBridge.php index 842e7489..c44e9557 100644 --- a/bridges/YouTubeCommunityTabBridge.php +++ b/bridges/YouTubeCommunityTabBridge.php @@ -1,271 +1,280 @@ <?php -class YouTubeCommunityTabBridge extends BridgeAbstract { - const NAME = 'YouTube Community Tab Bridge'; - const URI = 'https://www.youtube.com'; - const DESCRIPTION = 'Returns posts from a channel\'s community tab'; - const MAINTAINER = 'VerifiedJoseph'; - const PARAMETERS = array( - 'By channel ID' => array( - 'channel' => array( - 'name' => 'Channel ID', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'UCULkRHBdLC5ZcEQBaL0oYHQ' - ) - ), - 'By username' => array( - 'username' => array( - 'name' => 'Username', - 'type' => 'text', - 'required' => true, - 'exampleValue' => 'YouTubeUK' - ), - ) - ); - - const CACHE_TIMEOUT = 3600; // 1 hour - - private $feedUrl = ''; - private $feedName = ''; - private $itemTitle = ''; - - private $urlRegex = '/youtube\.com\/(channel|user|c)\/([\w]+)\/community/'; - private $jsonRegex = '/var ytInitialData = (.*);<\/script>/'; - - public function detectParameters($url) { - $params = array(); - - if(preg_match($this->urlRegex, $url, $matches)) { - if ($matches[1] === 'channel') { - $params['context'] = 'By channel ID'; - $params['channel'] = $matches[2]; - } - - if ($matches[1] === 'user') { - $params['context'] = 'By username'; - $params['username'] = $matches[2]; - } - - return $params; - } - - return null; - } - - public function collectData() { - - if (is_null($this->getInput('username')) === false) { - try { - $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'c'); - $html = getSimpleHTMLDOM($this->feedUrl); - - } catch (Exception $e) { - $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'user'); - $html = getSimpleHTMLDOM($this->feedUrl); - } - } else { - $this->feedUrl = $this->buildCommunityUri($this->getInput('channel'), 'channel'); - $html = getSimpleHTMLDOM($this->feedUrl); - } - - $json = $this->extractJson($html->find('body', 0)->innertext); - - $this->feedName = $json->header->c4TabbedHeaderRenderer->title; - - if ($this->hasCommunityTab($json) === false) { - returnServerError('Channel does not have a community tab'); - } - - foreach ($this->getCommunityPosts($json) as $post) { - $this->itemTitle = ''; - - if (!isset($post->backstagePostThreadRenderer)) { - continue; - } - - $details = $post->backstagePostThreadRenderer->post->backstagePostRenderer; - - $item = array(); - $item['uri'] = self::URI . '/post/' . $details->postId; - $item['author'] = $details->authorText->runs[0]->text; - $item['content'] = ''; - - if (isset($details->contentText)) { - $text = $this->getText($details->contentText->runs); - - $this->itemTitle = $this->ellipsisTitle($text); - $item['content'] = $text; - } - - $item['content'] .= $this->getAttachments($details); - $item['title'] = $this->itemTitle; - - $this->items[] = $item; - } - } - - public function getURI() { - - if (!empty($this->feedUri)) { - return $this->feedUri; - } - - return parent::getURI(); - } - public function getName() { - - if (!empty($this->feedName)) { - return $this->feedName . ' - YouTube Community Tab'; - } - - return parent::getName(); - } - - /** - * Build Community URI - */ - private function buildCommunityUri($value, $type) { - return self::URI . '/' . $type . '/' . $value . '/community'; - } - - /** - * Extract JSON from page - */ - private function extractJson($html) { - - if (!preg_match($this->jsonRegex, $html, $parts)) { - returnServerError('Failed to extract data from page'); - } - - $data = json_decode($parts[1]); - - if ($data === false) { - returnServerError('Failed to decode extracted data'); - } - - return $data; - } - - /** - * Check if channel has a community tab - */ - private function hasCommunityTab($json) { - - foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) { - if (isset($tab->tabRenderer) - && str_ends_with($tab->tabRenderer->endpoint->commandMetadata->webCommandMetadata->url, 'community')) { - - return true; - } - } - - return false; - } - - /** - * Get community tab posts - */ - private function getCommunityPosts($json) { - - foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) { - if (isset($tab->tabRenderer) - && str_ends_with($tab->tabRenderer->endpoint->commandMetadata->webCommandMetadata->url, 'community')) { - - return $tab->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer->contents; - } - } - } - - /** - * Get text content for a post - */ - private function getText($runs) { - $text = ''; - - foreach ($runs as $part) { - $text .= $this->formatUrls($part->text); - } - - return nl2br($text); - } - - /** - * Get attachments for posts - */ - private function getAttachments($details) { - $content = ''; - - if (isset($details->backstageAttachment)) { - $attachments = $details->backstageAttachment; - - // Video - if (isset($attachments->videoRenderer) && isset($attachments->videoRenderer->videoId)) { - if (empty($this->itemTitle)) { - $this->itemTitle = $this->feedName . ' posted a video'; - } - - $content = <<<EOD +class YouTubeCommunityTabBridge extends BridgeAbstract +{ + const NAME = 'YouTube Community Tab Bridge'; + const URI = 'https://www.youtube.com'; + const DESCRIPTION = 'Returns posts from a channel\'s community tab'; + const MAINTAINER = 'VerifiedJoseph'; + const PARAMETERS = [ + 'By channel ID' => [ + 'channel' => [ + 'name' => 'Channel ID', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'UCULkRHBdLC5ZcEQBaL0oYHQ' + ] + ], + 'By username' => [ + 'username' => [ + 'name' => 'Username', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'YouTubeUK' + ], + ] + ]; + + const CACHE_TIMEOUT = 3600; // 1 hour + + private $feedUrl = ''; + private $feedName = ''; + private $itemTitle = ''; + + private $urlRegex = '/youtube\.com\/(channel|user|c)\/([\w]+)\/community/'; + private $jsonRegex = '/var ytInitialData = (.*);<\/script>/'; + + public function detectParameters($url) + { + $params = []; + + if (preg_match($this->urlRegex, $url, $matches)) { + if ($matches[1] === 'channel') { + $params['context'] = 'By channel ID'; + $params['channel'] = $matches[2]; + } + + if ($matches[1] === 'user') { + $params['context'] = 'By username'; + $params['username'] = $matches[2]; + } + + return $params; + } + + return null; + } + + public function collectData() + { + if (is_null($this->getInput('username')) === false) { + try { + $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'c'); + $html = getSimpleHTMLDOM($this->feedUrl); + } catch (Exception $e) { + $this->feedUrl = $this->buildCommunityUri($this->getInput('username'), 'user'); + $html = getSimpleHTMLDOM($this->feedUrl); + } + } else { + $this->feedUrl = $this->buildCommunityUri($this->getInput('channel'), 'channel'); + $html = getSimpleHTMLDOM($this->feedUrl); + } + + $json = $this->extractJson($html->find('body', 0)->innertext); + + $this->feedName = $json->header->c4TabbedHeaderRenderer->title; + + if ($this->hasCommunityTab($json) === false) { + returnServerError('Channel does not have a community tab'); + } + + foreach ($this->getCommunityPosts($json) as $post) { + $this->itemTitle = ''; + + if (!isset($post->backstagePostThreadRenderer)) { + continue; + } + + $details = $post->backstagePostThreadRenderer->post->backstagePostRenderer; + + $item = []; + $item['uri'] = self::URI . '/post/' . $details->postId; + $item['author'] = $details->authorText->runs[0]->text; + $item['content'] = ''; + + if (isset($details->contentText)) { + $text = $this->getText($details->contentText->runs); + + $this->itemTitle = $this->ellipsisTitle($text); + $item['content'] = $text; + } + + $item['content'] .= $this->getAttachments($details); + $item['title'] = $this->itemTitle; + + $this->items[] = $item; + } + } + + public function getURI() + { + if (!empty($this->feedUri)) { + return $this->feedUri; + } + + return parent::getURI(); + } + + public function getName() + { + if (!empty($this->feedName)) { + return $this->feedName . ' - YouTube Community Tab'; + } + + return parent::getName(); + } + + /** + * Build Community URI + */ + private function buildCommunityUri($value, $type) + { + return self::URI . '/' . $type . '/' . $value . '/community'; + } + + /** + * Extract JSON from page + */ + private function extractJson($html) + { + if (!preg_match($this->jsonRegex, $html, $parts)) { + returnServerError('Failed to extract data from page'); + } + + $data = json_decode($parts[1]); + + if ($data === false) { + returnServerError('Failed to decode extracted data'); + } + + return $data; + } + + /** + * Check if channel has a community tab + */ + private function hasCommunityTab($json) + { + foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) { + if ( + isset($tab->tabRenderer) + && str_ends_with($tab->tabRenderer->endpoint->commandMetadata->webCommandMetadata->url, 'community') + ) { + return true; + } + } + + return false; + } + + /** + * Get community tab posts + */ + private function getCommunityPosts($json) + { + foreach ($json->contents->twoColumnBrowseResultsRenderer->tabs as $tab) { + if ( + isset($tab->tabRenderer) + && str_ends_with($tab->tabRenderer->endpoint->commandMetadata->webCommandMetadata->url, 'community') + ) { + return $tab->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer->contents; + } + } + } + + /** + * Get text content for a post + */ + private function getText($runs) + { + $text = ''; + + foreach ($runs as $part) { + $text .= $this->formatUrls($part->text); + } + + return nl2br($text); + } + + /** + * Get attachments for posts + */ + private function getAttachments($details) + { + $content = ''; + + if (isset($details->backstageAttachment)) { + $attachments = $details->backstageAttachment; + + // Video + if (isset($attachments->videoRenderer) && isset($attachments->videoRenderer->videoId)) { + if (empty($this->itemTitle)) { + $this->itemTitle = $this->feedName . ' posted a video'; + } + + $content = <<<EOD <iframe width="100%" height="410" src="https://www.youtube.com/embed/{$attachments->videoRenderer->videoId}" frameborder="0" allow="encrypted-media;" allowfullscreen></iframe> EOD; - } + } - // Image - if (isset($attachments->backstageImageRenderer)) { - if (empty($this->itemTitle)) { - $this->itemTitle = $this->feedName . ' posted an image'; - } + // Image + if (isset($attachments->backstageImageRenderer)) { + if (empty($this->itemTitle)) { + $this->itemTitle = $this->feedName . ' posted an image'; + } - $lastThumb = end($attachments->backstageImageRenderer->image->thumbnails); + $lastThumb = end($attachments->backstageImageRenderer->image->thumbnails); - $content = <<<EOD + $content = <<<EOD <p><img src="{$lastThumb->url}"></p> EOD; - } + } - // Poll - if (isset($attachments->pollRenderer)) { - if (empty($this->itemTitle)) { - $this->itemTitle = $this->feedName . ' posted a poll'; - } + // Poll + if (isset($attachments->pollRenderer)) { + if (empty($this->itemTitle)) { + $this->itemTitle = $this->feedName . ' posted a poll'; + } - $pollChoices = ''; + $pollChoices = ''; - foreach ($attachments->pollRenderer->choices as $choice) { - $pollChoices .= <<<EOD + foreach ($attachments->pollRenderer->choices as $choice) { + $pollChoices .= <<<EOD <li>{$choice->text->runs[0]->text}</li> EOD; - } + } - $content = <<<EOD + $content = <<<EOD <hr><p>Poll ({$attachments->pollRenderer->totalVotes->simpleText})<br><ul>{$pollChoices}</ul><p> EOD; - } - } - - return $content; - } - - /* - Ellipsis text for title - */ - private function ellipsisTitle($text) { - $length = 100; - - if (strlen($text) > $length) { - $text = explode('<br>', wordwrap($text, $length, '<br>')); - return $text[0] . '...'; - } - - return $text; - } - - private function formatUrls($content) { - return preg_replace( - '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims', - '<a target="_blank" href="$1" target="_blank">$1</a> ', - $content - ); - } + } + } + + return $content; + } + + /* + Ellipsis text for title + */ + private function ellipsisTitle($text) + { + $length = 100; + + if (strlen($text) > $length) { + $text = explode('<br>', wordwrap($text, $length, '<br>')); + return $text[0] . '...'; + } + + return $text; + } + + private function formatUrls($content) + { + return preg_replace( + '/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims', + '<a target="_blank" href="$1" target="_blank">$1</a> ', + $content + ); + } } diff --git a/bridges/YoutubeBridge.php b/bridges/YoutubeBridge.php index 536d6c29..31414472 100644 --- a/bridges/YoutubeBridge.php +++ b/bridges/YoutubeBridge.php @@ -1,4 +1,5 @@ <?php + /** * RssBridgeYoutube * Returns the newest videos @@ -6,424 +7,442 @@ * change: define('MAX_FILE_SIZE', 600000); * into: define('MAX_FILE_SIZE', 900000); (or more) */ -class YoutubeBridge extends BridgeAbstract { - - const NAME = 'YouTube Bridge'; - const URI = 'https://www.youtube.com/'; - const CACHE_TIMEOUT = 10800; // 3h - const DESCRIPTION = 'Returns the 10 newest videos by username/channel/playlist or search'; - const MAINTAINER = 'em92'; - - const PARAMETERS = array( - 'By username' => array( - 'u' => array( - 'name' => 'username', - 'exampleValue' => 'LinusTechTips', - 'required' => true - ) - ), - 'By channel id' => array( - 'c' => array( - 'name' => 'channel id', - 'exampleValue' => 'UCw38-8_Ibv_L6hlKChHO9dQ', - 'required' => true - ) - ), - 'By custom name' => array( - 'custom' => array( - 'name' => 'custom name', - 'exampleValue' => 'LinusTechTips', - 'required' => true - ) - ), - 'By playlist Id' => array( - 'p' => array( - 'name' => 'playlist id', - 'exampleValue' => 'PL8mG-RkN2uTzJc8N0EoyhdC54prvBBLpj', - 'required' => true - ) - ), - 'Search result' => array( - 's' => array( - 'name' => 'search keyword', - 'exampleValue' => 'LinusTechTips', - 'required' => true - ), - 'pa' => array( - 'name' => 'page', - 'type' => 'number', - 'title' => 'This option is not work anymore, as YouTube will always return the same page', - 'exampleValue' => 1 - ) - ), - 'global' => array( - 'duration_min' => array( - 'name' => 'min. duration (minutes)', - 'type' => 'number', - 'title' => 'Minimum duration for the video in minutes', - 'exampleValue' => 5 - ), - 'duration_max' => array( - 'name' => 'max. duration (minutes)', - 'type' => 'number', - 'title' => 'Maximum duration for the video in minutes', - 'exampleValue' => 10 - ) - ) - ); - - private $feedName = ''; - private $feeduri = ''; - private $channel_name = ''; - // This took from repo BetterVideoRss of VerifiedJoseph. +class YoutubeBridge extends BridgeAbstract +{ + const NAME = 'YouTube Bridge'; + const URI = 'https://www.youtube.com/'; + const CACHE_TIMEOUT = 10800; // 3h + const DESCRIPTION = 'Returns the 10 newest videos by username/channel/playlist or search'; + const MAINTAINER = 'em92'; + + const PARAMETERS = [ + 'By username' => [ + 'u' => [ + 'name' => 'username', + 'exampleValue' => 'LinusTechTips', + 'required' => true + ] + ], + 'By channel id' => [ + 'c' => [ + 'name' => 'channel id', + 'exampleValue' => 'UCw38-8_Ibv_L6hlKChHO9dQ', + 'required' => true + ] + ], + 'By custom name' => [ + 'custom' => [ + 'name' => 'custom name', + 'exampleValue' => 'LinusTechTips', + 'required' => true + ] + ], + 'By playlist Id' => [ + 'p' => [ + 'name' => 'playlist id', + 'exampleValue' => 'PL8mG-RkN2uTzJc8N0EoyhdC54prvBBLpj', + 'required' => true + ] + ], + 'Search result' => [ + 's' => [ + 'name' => 'search keyword', + 'exampleValue' => 'LinusTechTips', + 'required' => true + ], + 'pa' => [ + 'name' => 'page', + 'type' => 'number', + 'title' => 'This option is not work anymore, as YouTube will always return the same page', + 'exampleValue' => 1 + ] + ], + 'global' => [ + 'duration_min' => [ + 'name' => 'min. duration (minutes)', + 'type' => 'number', + 'title' => 'Minimum duration for the video in minutes', + 'exampleValue' => 5 + ], + 'duration_max' => [ + 'name' => 'max. duration (minutes)', + 'type' => 'number', + 'title' => 'Maximum duration for the video in minutes', + 'exampleValue' => 10 + ] + ] + ]; + + private $feedName = ''; + private $feeduri = ''; + private $channel_name = ''; + // This took from repo BetterVideoRss of VerifiedJoseph. const URI_REGEX = '/(https?:\/\/(?:www\.)?(?:[a-zA-Z0-9-.]{2,256}\.[a-z]{2,20})(\:[0-9]{2 ,4})?(?:\/[a-zA-Z0-9@:%_\+.,~#"\'!?&\/\/=\-*]+|\/)?)/ims'; //phpcs:ignore - private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){ - $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true); - - // Skip unavailable videos - if(strpos($html->innertext, 'IS_UNAVAILABLE_PAGE') !== false) { - return; - } - - $elAuthor = $html->find('span[itemprop=author] > link[itemprop=name]', 0); - if (!is_null($elAuthor)) { - $author = $elAuthor->getAttribute('content'); - } - - $elDatePublished = $html->find('meta[itemprop=datePublished]', 0); - if(!is_null($elDatePublished)) - $time = strtotime($elDatePublished->getAttribute('content')); - - $jsonData = $this->getJSONData($html); - $jsonData = $jsonData->contents->twoColumnWatchNextResults->results->results->contents; - - $videoSecondaryInfo = null; - foreach($jsonData as $item) { - if (isset($item->videoSecondaryInfoRenderer)) { - $videoSecondaryInfo = $item->videoSecondaryInfoRenderer; - break; - } - } - if (!$videoSecondaryInfo) { - returnServerError('Could not find videoSecondaryInfoRenderer. Error at: ' . $vid); - } - - if(isset($videoSecondaryInfo->description)) { - foreach($videoSecondaryInfo->description->runs as $description) { - if(isset($description->navigationEndpoint)) { - $metadata = $description->navigationEndpoint->commandMetadata->webCommandMetadata; - $web_type = $metadata->webPageType; - $url = $metadata->url; - $text = ''; - switch ($web_type) { - case 'WEB_PAGE_TYPE_UNKNOWN': - $url_components = parse_url($url); - if(isset($url_components['query']) && strpos($url_components['query'], '&q=') !== false) { - parse_str($url_components['query'], $params); - $url = urldecode($params['q']); - } - $text = $url; - break; - case 'WEB_PAGE_TYPE_WATCH': - case 'WEB_PAGE_TYPE_BROWSE': - $url = 'https://www.youtube.com' . $url; - $text = $description->text; - break; - } - $desc .= "<a href=\"$url\" target=\"_blank\">$text</a>"; - } else { - $desc .= nl2br($description->text); - } - } - } - } - - private function ytBridgeAddItem($vid, $title, $author, $desc, $time, $thumbnail = ''){ - $item = array(); - $item['id'] = $vid; - $item['title'] = $title; - $item['author'] = $author; - $item['timestamp'] = $time; - $item['uri'] = self::URI . 'watch?v=' . $vid; - if(!$thumbnail) { - $thumbnail = '0'; // Fallback to default thumbnail if there aren't any provided. - } - $thumbnailUri = str_replace('/www.', '/img.', self::URI) . 'vi/' . $vid . '/' . $thumbnail . '.jpg'; - $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $thumbnailUri . '" /></a><br />' . $desc; - $this->items[] = $item; - } - - private function ytBridgeParseXmlFeed($xml) { - foreach($xml->find('entry') as $element) { - $title = $this->ytBridgeFixTitle($element->find('title', 0)->plaintext); - $author = $element->find('name', 0)->plaintext; - $desc = $element->find('media:description', 0)->innertext; - - // Make sure the description is easy on the eye :) - $desc = htmlspecialchars($desc); - $desc = nl2br($desc); - $desc = preg_replace(self::URI_REGEX, - '<a href="$1" target="_blank">$1</a> ', - $desc); - - $vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext); - $time = strtotime($element->find('published', 0)->plaintext); - if(strpos($vid, 'googleads') === false) - $this->ytBridgeAddItem($vid, $title, $author, $desc, $time); - } - $this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName() - } - - private function ytBridgeFixTitle($title) { - // convert both Ӓ and " to UTF-8 - return html_entity_decode($title, ENT_QUOTES, 'UTF-8'); - } - - private function ytGetSimpleHTMLDOM($url, $cached = false){ - $header = array( - 'Accept-Language: en-US' - ); - $opts = array(); - $lowercase = true; - $forceTagsClosed = true; - $target_charset = DEFAULT_TARGET_CHARSET; - $stripRN = false; - $defaultBRText = DEFAULT_BR_TEXT; - $defaultSpanText = DEFAULT_SPAN_TEXT; - if ($cached) { - return getSimpleHTMLDOMCached($url, - 86400, - $header, - $opts, - $lowercase, - $forceTagsClosed, - $target_charset, - $stripRN, - $defaultBRText, - $defaultSpanText); - } - return getSimpleHTMLDOM($url, - $header, - $opts, - $lowercase, - $forceTagsClosed, - $target_charset, - $stripRN, - $defaultBRText, - $defaultSpanText); - } - - private function getJSONData($html) { - $scriptRegex = '/var ytInitialData = (.*?);<\/script>/'; - preg_match($scriptRegex, $html, $matches) or returnServerError('Could not find ytInitialData'); - return json_decode($matches[1]); - } - - private function parseJSONListing($jsonData) { - $duration_min = $this->getInput('duration_min') ?: -1; - $duration_min = $duration_min * 60; - - $duration_max = $this->getInput('duration_max') ?: INF; - $duration_max = $duration_max * 60; - - if($duration_max < $duration_min) { - returnClientError('Max duration must be greater than min duration!'); - } - - // $vid_list = ''; - - foreach($jsonData as $item) { - $wrapper = null; - if(isset($item->gridVideoRenderer)) { - $wrapper = $item->gridVideoRenderer; - } elseif(isset($item->videoRenderer)) { - $wrapper = $item->videoRenderer; - } elseif(isset($item->playlistVideoRenderer)) { - $wrapper = $item->playlistVideoRenderer; - } else - continue; - - $vid = $wrapper->videoId; - $title = $wrapper->title->runs[0]->text; - if(isset($wrapper->ownerText)) { - $this->channel_name = $wrapper->ownerText->runs[0]->text; - } elseif(isset($wrapper->shortBylineText)) { - $this->channel_name = $wrapper->shortBylineText->runs[0]->text; - } - - $author = ''; - $desc = ''; - $time = ''; - - // The duration comes in one of the formats: - // hh:mm:ss / mm:ss / m:ss - // 01:03:30 / 15:06 / 1:24 - $durationText = 0; - if(isset($wrapper->lengthText)) { - $durationText = $wrapper->lengthText; - } else { - foreach($wrapper->thumbnailOverlays as $overlay) { - if(isset($overlay->thumbnailOverlayTimeStatusRenderer)) { - $durationText = $overlay->thumbnailOverlayTimeStatusRenderer->text; - break; - } - } - } - - if(isset($durationText->simpleText)) { - $durationText = trim($durationText->simpleText); - } else { - $durationText = 0; - } - - if(preg_match('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', $durationText)) { - $durationText = preg_replace('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', '$1:$2:$3', $durationText); - } else { - $durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText); - } - sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds); - $duration = $hours * 3600 + $minutes * 60 + $seconds; - if($duration < $duration_min || $duration > $duration_max) { - continue; - } - - // $vid_list .= $vid . ','; - $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time); - $this->ytBridgeAddItem($vid, $title, $author, $desc, $time); - } - } - - public function collectData(){ - - $xml = ''; - $html = ''; - $url_feed = ''; - $url_listing = ''; - - if($this->getInput('u')) { /* User and Channel modes */ - $this->request = $this->getInput('u'); - $url_feed = self::URI . 'feeds/videos.xml?user=' . urlencode($this->request); - $url_listing = self::URI . 'user/' . urlencode($this->request) . '/videos'; - } elseif($this->getInput('c')) { - $this->request = $this->getInput('c'); - $url_feed = self::URI . 'feeds/videos.xml?channel_id=' . urlencode($this->request); - $url_listing = self::URI . 'channel/' . urlencode($this->request) . '/videos'; - } elseif($this->getInput('custom')) { - $this->request = $this->getInput('custom'); - $url_listing = self::URI . urlencode($this->request) . '/videos'; - } - - if(!empty($url_feed) || !empty($url_listing)) { - $this->feeduri = $url_listing; - if(!empty($this->getInput('custom'))) { - $html = $this->ytGetSimpleHTMLDOM($url_listing); - $jsonData = $this->getJSONData($html); - $url_feed = $jsonData->metadata->channelMetadataRenderer->rssUrl; - } - if(!$this->skipFeeds()) { - $html = $this->ytGetSimpleHTMLDOM($url_feed); - $this->ytBridgeParseXmlFeed($html); - } else { - if(empty($this->getInput('custom'))) { - $html = $this->ytGetSimpleHTMLDOM($url_listing); - $jsonData = $this->getJSONData($html); - } - $channel_id = ''; - if(isset($jsonData->contents)) { - $channel_id = $jsonData->metadata->channelMetadataRenderer->externalId; - $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[1]; - $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0]; - $jsonData = $jsonData->itemSectionRenderer->contents[0]->gridRenderer->items; - $this->parseJSONListing($jsonData); - } else { - returnServerError('Unable to get data from YouTube. Username/Channel: ' . $this->request); - } - } - $this->feedName = str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); - } elseif($this->getInput('p')) { /* playlist mode */ - // TODO: this mode makes a lot of excess video query requests. - // To make less requests, we need to cache following dictionary "videoId -> datePublished, duration" - // This cache will be used to find out, which videos to fetch - // to make feed of 15 items or more, if there a lot of videos published on that date. - $this->request = $this->getInput('p'); - $url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request); - $url_listing = self::URI . 'playlist?list=' . urlencode($this->request); - $html = $this->ytGetSimpleHTMLDOM($url_listing); - $jsonData = $this->getJSONData($html); - // TODO: this method returns only first 100 video items - // if it has more videos, playlistVideoListRenderer will have continuationItemRenderer as last element - $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[0]; - $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer; - $jsonData = $jsonData->contents[0]->playlistVideoListRenderer->contents; - $item_count = count($jsonData); - - if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) { - $this->ytBridgeParseXmlFeed($xml); - } else { - $this->parseJSONListing($jsonData); - } - $this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName() - usort($this->items, function ($item1, $item2) { - if(!is_int($item1['timestamp']) && !is_int($item2['timestamp'])) { - $item1['timestamp'] = strtotime($item1['timestamp']); - $item2['timestamp'] = strtotime($item2['timestamp']); - } - return $item2['timestamp'] - $item1['timestamp']; - }); - } elseif($this->getInput('s')) { /* search mode */ - $this->request = $this->getInput('s'); - $url_listing = self::URI - . 'results?search_query=' - . urlencode($this->request) - . '&sp=CAI%253D'; - - $html = $this->ytGetSimpleHTMLDOM($url_listing); - - $jsonData = $this->getJSONData($html); - $jsonData = $jsonData->contents->twoColumnSearchResultsRenderer->primaryContents; - $jsonData = $jsonData->sectionListRenderer->contents; - foreach($jsonData as $data) { // Search result includes some ads, have to filter them - if(isset($data->itemSectionRenderer->contents[0]->videoRenderer)) { - $jsonData = $data->itemSectionRenderer->contents; - break; - } - } - $this->parseJSONListing($jsonData); - $this->feeduri = $url_listing; - $this->feedName = 'Search: ' . $this->request; // feedName will be used by getName() - } else { /* no valid mode */ - returnClientError("You must either specify either:\n - YouTube + private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time) + { + $html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid", true); + + // Skip unavailable videos + if (strpos($html->innertext, 'IS_UNAVAILABLE_PAGE') !== false) { + return; + } + + $elAuthor = $html->find('span[itemprop=author] > link[itemprop=name]', 0); + if (!is_null($elAuthor)) { + $author = $elAuthor->getAttribute('content'); + } + + $elDatePublished = $html->find('meta[itemprop=datePublished]', 0); + if (!is_null($elDatePublished)) { + $time = strtotime($elDatePublished->getAttribute('content')); + } + + $jsonData = $this->getJSONData($html); + $jsonData = $jsonData->contents->twoColumnWatchNextResults->results->results->contents; + + $videoSecondaryInfo = null; + foreach ($jsonData as $item) { + if (isset($item->videoSecondaryInfoRenderer)) { + $videoSecondaryInfo = $item->videoSecondaryInfoRenderer; + break; + } + } + if (!$videoSecondaryInfo) { + returnServerError('Could not find videoSecondaryInfoRenderer. Error at: ' . $vid); + } + + if (isset($videoSecondaryInfo->description)) { + foreach ($videoSecondaryInfo->description->runs as $description) { + if (isset($description->navigationEndpoint)) { + $metadata = $description->navigationEndpoint->commandMetadata->webCommandMetadata; + $web_type = $metadata->webPageType; + $url = $metadata->url; + $text = ''; + switch ($web_type) { + case 'WEB_PAGE_TYPE_UNKNOWN': + $url_components = parse_url($url); + if (isset($url_components['query']) && strpos($url_components['query'], '&q=') !== false) { + parse_str($url_components['query'], $params); + $url = urldecode($params['q']); + } + $text = $url; + break; + case 'WEB_PAGE_TYPE_WATCH': + case 'WEB_PAGE_TYPE_BROWSE': + $url = 'https://www.youtube.com' . $url; + $text = $description->text; + break; + } + $desc .= "<a href=\"$url\" target=\"_blank\">$text</a>"; + } else { + $desc .= nl2br($description->text); + } + } + } + } + + private function ytBridgeAddItem($vid, $title, $author, $desc, $time, $thumbnail = '') + { + $item = []; + $item['id'] = $vid; + $item['title'] = $title; + $item['author'] = $author; + $item['timestamp'] = $time; + $item['uri'] = self::URI . 'watch?v=' . $vid; + if (!$thumbnail) { + $thumbnail = '0'; // Fallback to default thumbnail if there aren't any provided. + } + $thumbnailUri = str_replace('/www.', '/img.', self::URI) . 'vi/' . $vid . '/' . $thumbnail . '.jpg'; + $item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $thumbnailUri . '" /></a><br />' . $desc; + $this->items[] = $item; + } + + private function ytBridgeParseXmlFeed($xml) + { + foreach ($xml->find('entry') as $element) { + $title = $this->ytBridgeFixTitle($element->find('title', 0)->plaintext); + $author = $element->find('name', 0)->plaintext; + $desc = $element->find('media:description', 0)->innertext; + + // Make sure the description is easy on the eye :) + $desc = htmlspecialchars($desc); + $desc = nl2br($desc); + $desc = preg_replace( + self::URI_REGEX, + '<a href="$1" target="_blank">$1</a> ', + $desc + ); + + $vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext); + $time = strtotime($element->find('published', 0)->plaintext); + if (strpos($vid, 'googleads') === false) { + $this->ytBridgeAddItem($vid, $title, $author, $desc, $time); + } + } + $this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName() + } + + private function ytBridgeFixTitle($title) + { + // convert both Ӓ and " to UTF-8 + return html_entity_decode($title, ENT_QUOTES, 'UTF-8'); + } + + private function ytGetSimpleHTMLDOM($url, $cached = false) + { + $header = [ + 'Accept-Language: en-US' + ]; + $opts = []; + $lowercase = true; + $forceTagsClosed = true; + $target_charset = DEFAULT_TARGET_CHARSET; + $stripRN = false; + $defaultBRText = DEFAULT_BR_TEXT; + $defaultSpanText = DEFAULT_SPAN_TEXT; + if ($cached) { + return getSimpleHTMLDOMCached( + $url, + 86400, + $header, + $opts, + $lowercase, + $forceTagsClosed, + $target_charset, + $stripRN, + $defaultBRText, + $defaultSpanText + ); + } + return getSimpleHTMLDOM( + $url, + $header, + $opts, + $lowercase, + $forceTagsClosed, + $target_charset, + $stripRN, + $defaultBRText, + $defaultSpanText + ); + } + + private function getJSONData($html) + { + $scriptRegex = '/var ytInitialData = (.*?);<\/script>/'; + preg_match($scriptRegex, $html, $matches) or returnServerError('Could not find ytInitialData'); + return json_decode($matches[1]); + } + + private function parseJSONListing($jsonData) + { + $duration_min = $this->getInput('duration_min') ?: -1; + $duration_min = $duration_min * 60; + + $duration_max = $this->getInput('duration_max') ?: INF; + $duration_max = $duration_max * 60; + + if ($duration_max < $duration_min) { + returnClientError('Max duration must be greater than min duration!'); + } + + // $vid_list = ''; + + foreach ($jsonData as $item) { + $wrapper = null; + if (isset($item->gridVideoRenderer)) { + $wrapper = $item->gridVideoRenderer; + } elseif (isset($item->videoRenderer)) { + $wrapper = $item->videoRenderer; + } elseif (isset($item->playlistVideoRenderer)) { + $wrapper = $item->playlistVideoRenderer; + } else { + continue; + } + + $vid = $wrapper->videoId; + $title = $wrapper->title->runs[0]->text; + if (isset($wrapper->ownerText)) { + $this->channel_name = $wrapper->ownerText->runs[0]->text; + } elseif (isset($wrapper->shortBylineText)) { + $this->channel_name = $wrapper->shortBylineText->runs[0]->text; + } + + $author = ''; + $desc = ''; + $time = ''; + + // The duration comes in one of the formats: + // hh:mm:ss / mm:ss / m:ss + // 01:03:30 / 15:06 / 1:24 + $durationText = 0; + if (isset($wrapper->lengthText)) { + $durationText = $wrapper->lengthText; + } else { + foreach ($wrapper->thumbnailOverlays as $overlay) { + if (isset($overlay->thumbnailOverlayTimeStatusRenderer)) { + $durationText = $overlay->thumbnailOverlayTimeStatusRenderer->text; + break; + } + } + } + + if (isset($durationText->simpleText)) { + $durationText = trim($durationText->simpleText); + } else { + $durationText = 0; + } + + if (preg_match('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', $durationText)) { + $durationText = preg_replace('/([\d]{1,2}):([\d]{1,2})\:([\d]{2})/', '$1:$2:$3', $durationText); + } else { + $durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText); + } + sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds); + $duration = $hours * 3600 + $minutes * 60 + $seconds; + if ($duration < $duration_min || $duration > $duration_max) { + continue; + } + + // $vid_list .= $vid . ','; + $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time); + $this->ytBridgeAddItem($vid, $title, $author, $desc, $time); + } + } + + public function collectData() + { + $xml = ''; + $html = ''; + $url_feed = ''; + $url_listing = ''; + + if ($this->getInput('u')) { /* User and Channel modes */ + $this->request = $this->getInput('u'); + $url_feed = self::URI . 'feeds/videos.xml?user=' . urlencode($this->request); + $url_listing = self::URI . 'user/' . urlencode($this->request) . '/videos'; + } elseif ($this->getInput('c')) { + $this->request = $this->getInput('c'); + $url_feed = self::URI . 'feeds/videos.xml?channel_id=' . urlencode($this->request); + $url_listing = self::URI . 'channel/' . urlencode($this->request) . '/videos'; + } elseif ($this->getInput('custom')) { + $this->request = $this->getInput('custom'); + $url_listing = self::URI . urlencode($this->request) . '/videos'; + } + + if (!empty($url_feed) || !empty($url_listing)) { + $this->feeduri = $url_listing; + if (!empty($this->getInput('custom'))) { + $html = $this->ytGetSimpleHTMLDOM($url_listing); + $jsonData = $this->getJSONData($html); + $url_feed = $jsonData->metadata->channelMetadataRenderer->rssUrl; + } + if (!$this->skipFeeds()) { + $html = $this->ytGetSimpleHTMLDOM($url_feed); + $this->ytBridgeParseXmlFeed($html); + } else { + if (empty($this->getInput('custom'))) { + $html = $this->ytGetSimpleHTMLDOM($url_listing); + $jsonData = $this->getJSONData($html); + } + $channel_id = ''; + if (isset($jsonData->contents)) { + $channel_id = $jsonData->metadata->channelMetadataRenderer->externalId; + $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[1]; + $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0]; + $jsonData = $jsonData->itemSectionRenderer->contents[0]->gridRenderer->items; + $this->parseJSONListing($jsonData); + } else { + returnServerError('Unable to get data from YouTube. Username/Channel: ' . $this->request); + } + } + $this->feedName = str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); + } elseif ($this->getInput('p')) { /* playlist mode */ + // TODO: this mode makes a lot of excess video query requests. + // To make less requests, we need to cache following dictionary "videoId -> datePublished, duration" + // This cache will be used to find out, which videos to fetch + // to make feed of 15 items or more, if there a lot of videos published on that date. + $this->request = $this->getInput('p'); + $url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request); + $url_listing = self::URI . 'playlist?list=' . urlencode($this->request); + $html = $this->ytGetSimpleHTMLDOM($url_listing); + $jsonData = $this->getJSONData($html); + // TODO: this method returns only first 100 video items + // if it has more videos, playlistVideoListRenderer will have continuationItemRenderer as last element + $jsonData = $jsonData->contents->twoColumnBrowseResultsRenderer->tabs[0]; + $jsonData = $jsonData->tabRenderer->content->sectionListRenderer->contents[0]->itemSectionRenderer; + $jsonData = $jsonData->contents[0]->playlistVideoListRenderer->contents; + $item_count = count($jsonData); + + if ($item_count <= 15 && !$this->skipFeeds() && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) { + $this->ytBridgeParseXmlFeed($xml); + } else { + $this->parseJSONListing($jsonData); + } + $this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName() + usort($this->items, function ($item1, $item2) { + if (!is_int($item1['timestamp']) && !is_int($item2['timestamp'])) { + $item1['timestamp'] = strtotime($item1['timestamp']); + $item2['timestamp'] = strtotime($item2['timestamp']); + } + return $item2['timestamp'] - $item1['timestamp']; + }); + } elseif ($this->getInput('s')) { /* search mode */ + $this->request = $this->getInput('s'); + $url_listing = self::URI + . 'results?search_query=' + . urlencode($this->request) + . '&sp=CAI%253D'; + + $html = $this->ytGetSimpleHTMLDOM($url_listing); + + $jsonData = $this->getJSONData($html); + $jsonData = $jsonData->contents->twoColumnSearchResultsRenderer->primaryContents; + $jsonData = $jsonData->sectionListRenderer->contents; + foreach ($jsonData as $data) { // Search result includes some ads, have to filter them + if (isset($data->itemSectionRenderer->contents[0]->videoRenderer)) { + $jsonData = $data->itemSectionRenderer->contents; + break; + } + } + $this->parseJSONListing($jsonData); + $this->feeduri = $url_listing; + $this->feedName = 'Search: ' . $this->request; // feedName will be used by getName() + } else { /* no valid mode */ + returnClientError("You must either specify either:\n - YouTube username (?u=...)\n - Channel id (?c=...)\n - Playlist id (?p=...)\n - Search (?s=...)"); - } - } - - private function skipFeeds() { - return ($this->getInput('duration_min') || $this->getInput('duration_max')); - } - - public function getURI() - { - if (!is_null($this->getInput('p'))) { - return static::URI . 'playlist?list=' . $this->getInput('p'); - } elseif($this->feeduri) { - return $this->feeduri; - } - - return parent::getURI(); - } - - public function getName(){ - // Name depends on queriedContext: - switch($this->queriedContext) { - case 'By username': - case 'By channel id': - case 'By custom name': - case 'By playlist Id': - case 'Search result': - return htmlspecialchars_decode($this->feedName) . ' - YouTube'; // We already know it's a bridge, right? - default: - return parent::getName(); - } - } + } + } + + private function skipFeeds() + { + return ($this->getInput('duration_min') || $this->getInput('duration_max')); + } + + public function getURI() + { + if (!is_null($this->getInput('p'))) { + return static::URI . 'playlist?list=' . $this->getInput('p'); + } elseif ($this->feeduri) { + return $this->feeduri; + } + + return parent::getURI(); + } + + public function getName() + { + // Name depends on queriedContext: + switch ($this->queriedContext) { + case 'By username': + case 'By channel id': + case 'By custom name': + case 'By playlist Id': + case 'Search result': + return htmlspecialchars_decode($this->feedName) . ' - YouTube'; // We already know it's a bridge, right? + default: + return parent::getName(); + } + } } diff --git a/bridges/ZDNetBridge.php b/bridges/ZDNetBridge.php index 927e37ae..09bde8e3 100644 --- a/bridges/ZDNetBridge.php +++ b/bridges/ZDNetBridge.php @@ -1,204 +1,209 @@ <?php -class ZDNetBridge extends FeedExpander { - const MAINTAINER = 'ORelio'; - const NAME = 'ZDNet Bridge'; - const URI = 'https://www.zdnet.com/'; - const DESCRIPTION = 'Technology News, Analysis, Comments and Product Reviews for IT Professionals.'; +class ZDNetBridge extends FeedExpander +{ + const MAINTAINER = 'ORelio'; + const NAME = 'ZDNet Bridge'; + const URI = 'https://www.zdnet.com/'; + const DESCRIPTION = 'Technology News, Analysis, Comments and Product Reviews for IT Professionals.'; - //http://www.zdnet.com/zdnet.opml - const PARAMETERS = array( array( - 'feed' => array( - 'name' => 'Feed', - 'type' => 'list', - 'values' => array( - 'Subscribe to ZDNet RSS Feeds' => array( - 'All Blogs' => 'blog', - 'Just News' => 'news', - 'All Reviews' => 'topic/reviews', - 'Latest Downloads' => 'downloads!recent', - 'Latest Articles' => '/', - 'Latest Australia Articles' => 'au', - 'Latest UK Articles' => 'uk', - 'Latest US Articles' => 'us', - 'Latest Asia Articles' => 'as' - ), - 'Keep up with ZDNet Blogs RSS:' => array( - 'Transforming the Datacenter' => 'blog/transforming-datacenter', - 'SMB India' => 'blog/smb-india', - 'Indonesia BizTech' => 'blog/indonesia-biztech', - 'Hong Kong Techie' => 'blog/hong-kong-techie', - 'Tech Taiwan' => 'blog/tech-taiwan', - 'Startup India' => 'blog/startup-india', - 'Starting Up Asia' => 'blog/starting-up-asia', - 'Next-Gen Partner' => 'blog/partner', - 'Post-PC Developments' => 'blog/post-pc', - 'Benelux' => 'blog/benelux', - 'Heat Sink' => 'blog/heat-sink', - 'Italy\'s got tech' => 'blog/italy', - 'African Enterprise' => 'blog/african-enterprise', - 'New Tech for Old India' => 'blog/new-india', - 'Estonia Uncovered' => 'blog/estonia', - 'IT Iberia' => 'blog/iberia', - 'Brazil Tech' => 'blog/brazil', - '500 words into the future' => 'blog/500-words-into-the-future', - 'ÜberTech' => 'blog/ubertech', - 'All About Microsoft' => 'blog/microsoft', - 'Back office' => 'blog/back-office', - 'Barker Bites Back' => 'blog/barker-bites-back', - 'Between the Lines' => 'blog/btl', - 'Big on Data' => 'blog/big-data', - 'bootstrappr' => 'blog/bootstrappr', - 'By The Way' => 'blog/by-the-way', - 'Central European Processing' => 'blog/central-europe', - 'Cloud Builders' => 'blog/cloud-builders', - 'Communication Breakdown' => 'blog/communication-breakdown', - 'Collaboration 2.0' => 'blog/collaboration', - 'Constellation Research' => 'blog/constellation', - 'Consumerization: BYOD' => 'blog/consumerization', - 'DIY-IT' => 'blog/diy-it', - 'Enterprise Web 2.0' => 'blog/hinchcliffe', - 'Five Nines: The Next Gen Datacenter' => 'blog/datacenter', - 'Forrester Research' => 'blog/forrester', - 'Full Duplex' => 'blog/full-duplex', - 'Gen Why?' => 'blog/gen-why', - 'Hardware 2.0' => 'blog/hardware', - 'Identity Matters' => 'blog/identity', - 'iGeneration' => 'blog/igeneration', - 'Internet of Everything' => 'blog/cisco', - 'Beyond IT Failure' => 'blog/projectfailures', - 'Jamie\'s Mostly Linux Stuff' => 'blog/jamies-mostly-linux-stuff', - 'Jack\'s Blog' => 'blog/jacks-blog', - 'Laptops & Desktops' => 'blog/computers', - 'Linux and Open Source' => 'blog/open-source', - 'London Calling' => 'blog/london', - 'Mapping Babel' => 'blog/mapping-babel', - 'Mixed Signals' => 'blog/mixed-signals', - 'Mobile India' => 'blog/mobile-india', - 'Mobile News' => 'blog/mobile-news', - 'Networking' => 'blog/networking', - 'Norse Code' => 'blog/norse-code', - 'Null Pointer' => 'blog/null-pointer', - 'The Full Tilt' => 'blog/the-full-tilt', - 'Pinoy Post' => 'blog/pinoy-post', - 'Practically Tech' => 'blog/practically-tech', - 'Product Central' => 'blog/product-central', - 'Pulp Tech' => 'blog/violetblue', - 'Qubits and Pieces' => 'blog/qubits-and-pieces', - 'Securify This!' => 'blog/securify-this', - 'Service Oriented' => 'blog/service-oriented', - 'Small Talk' => 'blog/small-talk', - 'Small Business Matters' => 'blog/small-business-matters', - 'Smartphones and Cell Phones' => 'blog/cell-phones', - 'Social Business' => 'blog/feeds', - 'Social CRM: The Conversation' => 'blog/crm', - 'Software & Services Safari' => 'blog/sommer', - 'Storage Bits' => 'blog/storage', - 'Stacking up Open Clouds' => 'blog/apac-redhat', - 'Techie Isles' => 'blog/techie-isles', - 'Technolatte' => 'blog/technolatte', - 'Tech Podium' => 'blog/tech-podium', - 'Tel Aviv Tech' => 'blog/tel-aviv', - 'Tech Broiler' => 'blog/perlow', - 'The SANMAN' => 'blog/the-sanman', - 'The open source revolution' => 'blog/the-open-source-revolution', - 'The German View' => 'blog/german', - 'The Ed Bott Report' => 'blog/bott', - 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer', - 'The Apple Core' => 'blog/apple', - 'Tom Foremski: IMHO' => 'blog/foremski', - 'Twisted Wire' => 'blog/twisted-wire', - 'Vive la tech' => 'blog/france', - 'Virtually Speaking' => 'blog/virtualization', - 'View from China' => 'blog/china', - 'Web design & Free Software' => 'blog/web-design-and-free-software', - 'ZDNet Government' => 'blog/government', - 'ZDNet UK Book Reviews' => 'blog/zdnet-uk-book-reviews', - 'ZDNet UK First Take' => 'blog/zdnet-uk-first-take', - 'Zero Day' => 'blog/security' - ), - 'ZDNet Hot Topics RSS:' => array( - 'Apple' => 'topic/apple', - 'Collaboration' => 'topic/collaboration', - 'Enterprise Software' => 'topic/enterprise-software', - 'Google' => 'topic/google', - 'Great debate' => 'topic/great-debate', - 'Hardware' => 'topic/hardware', - 'IBM' => 'topic/ibm', - 'iOS' => 'topic/ios', - 'iPhone' => 'topic/iphone', - 'iPad' => 'topic/ipad', - 'IT Priorities' => 'topic/it-priorities', - 'Laptops' => 'topic/laptops', - 'Legal' => 'topic/legal', - 'Linux' => 'topic/linux', - 'Microsoft' => 'topic/microsoft', - 'Mobile OS' => 'topic/mobile-os', - 'Mobility' => 'topic/mobility', - 'Networking' => 'topic/networking', - 'Oracle' => 'topic/oracle', - 'Processors' => 'topic/processors', - 'Samsung' => 'topic/samsung', - 'Security' => 'topic/security', - 'Small business: going big on mobility' => 'topic/small-business-going-big-on-mobility' - ), - 'Product Blogs:' => array( - 'Digital Cameras & Camcorders' => 'blog/digitalcameras', - 'Home Theater' => 'blog/home-theater', - 'Laptops and Desktops' => 'blog/computers', - 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer', - 'Smartphones and Cell Phones' => 'blog/cell-phones', - 'The ToyBox' => 'blog/gadgetreviews' - ), - 'Vertical Blogs:' => array( - 'ZDNet Education' => 'blog/education', - 'ZDNet Healthcare' => 'blog/healthcare', - 'ZDNet Government' => 'blog/government' - ) - ) - ), - 'limit' => self::LIMIT, - )); + //http://www.zdnet.com/zdnet.opml + const PARAMETERS = [ [ + 'feed' => [ + 'name' => 'Feed', + 'type' => 'list', + 'values' => [ + 'Subscribe to ZDNet RSS Feeds' => [ + 'All Blogs' => 'blog', + 'Just News' => 'news', + 'All Reviews' => 'topic/reviews', + 'Latest Downloads' => 'downloads!recent', + 'Latest Articles' => '/', + 'Latest Australia Articles' => 'au', + 'Latest UK Articles' => 'uk', + 'Latest US Articles' => 'us', + 'Latest Asia Articles' => 'as' + ], + 'Keep up with ZDNet Blogs RSS:' => [ + 'Transforming the Datacenter' => 'blog/transforming-datacenter', + 'SMB India' => 'blog/smb-india', + 'Indonesia BizTech' => 'blog/indonesia-biztech', + 'Hong Kong Techie' => 'blog/hong-kong-techie', + 'Tech Taiwan' => 'blog/tech-taiwan', + 'Startup India' => 'blog/startup-india', + 'Starting Up Asia' => 'blog/starting-up-asia', + 'Next-Gen Partner' => 'blog/partner', + 'Post-PC Developments' => 'blog/post-pc', + 'Benelux' => 'blog/benelux', + 'Heat Sink' => 'blog/heat-sink', + 'Italy\'s got tech' => 'blog/italy', + 'African Enterprise' => 'blog/african-enterprise', + 'New Tech for Old India' => 'blog/new-india', + 'Estonia Uncovered' => 'blog/estonia', + 'IT Iberia' => 'blog/iberia', + 'Brazil Tech' => 'blog/brazil', + '500 words into the future' => 'blog/500-words-into-the-future', + 'ÜberTech' => 'blog/ubertech', + 'All About Microsoft' => 'blog/microsoft', + 'Back office' => 'blog/back-office', + 'Barker Bites Back' => 'blog/barker-bites-back', + 'Between the Lines' => 'blog/btl', + 'Big on Data' => 'blog/big-data', + 'bootstrappr' => 'blog/bootstrappr', + 'By The Way' => 'blog/by-the-way', + 'Central European Processing' => 'blog/central-europe', + 'Cloud Builders' => 'blog/cloud-builders', + 'Communication Breakdown' => 'blog/communication-breakdown', + 'Collaboration 2.0' => 'blog/collaboration', + 'Constellation Research' => 'blog/constellation', + 'Consumerization: BYOD' => 'blog/consumerization', + 'DIY-IT' => 'blog/diy-it', + 'Enterprise Web 2.0' => 'blog/hinchcliffe', + 'Five Nines: The Next Gen Datacenter' => 'blog/datacenter', + 'Forrester Research' => 'blog/forrester', + 'Full Duplex' => 'blog/full-duplex', + 'Gen Why?' => 'blog/gen-why', + 'Hardware 2.0' => 'blog/hardware', + 'Identity Matters' => 'blog/identity', + 'iGeneration' => 'blog/igeneration', + 'Internet of Everything' => 'blog/cisco', + 'Beyond IT Failure' => 'blog/projectfailures', + 'Jamie\'s Mostly Linux Stuff' => 'blog/jamies-mostly-linux-stuff', + 'Jack\'s Blog' => 'blog/jacks-blog', + 'Laptops & Desktops' => 'blog/computers', + 'Linux and Open Source' => 'blog/open-source', + 'London Calling' => 'blog/london', + 'Mapping Babel' => 'blog/mapping-babel', + 'Mixed Signals' => 'blog/mixed-signals', + 'Mobile India' => 'blog/mobile-india', + 'Mobile News' => 'blog/mobile-news', + 'Networking' => 'blog/networking', + 'Norse Code' => 'blog/norse-code', + 'Null Pointer' => 'blog/null-pointer', + 'The Full Tilt' => 'blog/the-full-tilt', + 'Pinoy Post' => 'blog/pinoy-post', + 'Practically Tech' => 'blog/practically-tech', + 'Product Central' => 'blog/product-central', + 'Pulp Tech' => 'blog/violetblue', + 'Qubits and Pieces' => 'blog/qubits-and-pieces', + 'Securify This!' => 'blog/securify-this', + 'Service Oriented' => 'blog/service-oriented', + 'Small Talk' => 'blog/small-talk', + 'Small Business Matters' => 'blog/small-business-matters', + 'Smartphones and Cell Phones' => 'blog/cell-phones', + 'Social Business' => 'blog/feeds', + 'Social CRM: The Conversation' => 'blog/crm', + 'Software & Services Safari' => 'blog/sommer', + 'Storage Bits' => 'blog/storage', + 'Stacking up Open Clouds' => 'blog/apac-redhat', + 'Techie Isles' => 'blog/techie-isles', + 'Technolatte' => 'blog/technolatte', + 'Tech Podium' => 'blog/tech-podium', + 'Tel Aviv Tech' => 'blog/tel-aviv', + 'Tech Broiler' => 'blog/perlow', + 'The SANMAN' => 'blog/the-sanman', + 'The open source revolution' => 'blog/the-open-source-revolution', + 'The German View' => 'blog/german', + 'The Ed Bott Report' => 'blog/bott', + 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer', + 'The Apple Core' => 'blog/apple', + 'Tom Foremski: IMHO' => 'blog/foremski', + 'Twisted Wire' => 'blog/twisted-wire', + 'Vive la tech' => 'blog/france', + 'Virtually Speaking' => 'blog/virtualization', + 'View from China' => 'blog/china', + 'Web design & Free Software' => 'blog/web-design-and-free-software', + 'ZDNet Government' => 'blog/government', + 'ZDNet UK Book Reviews' => 'blog/zdnet-uk-book-reviews', + 'ZDNet UK First Take' => 'blog/zdnet-uk-first-take', + 'Zero Day' => 'blog/security' + ], + 'ZDNet Hot Topics RSS:' => [ + 'Apple' => 'topic/apple', + 'Collaboration' => 'topic/collaboration', + 'Enterprise Software' => 'topic/enterprise-software', + 'Google' => 'topic/google', + 'Great debate' => 'topic/great-debate', + 'Hardware' => 'topic/hardware', + 'IBM' => 'topic/ibm', + 'iOS' => 'topic/ios', + 'iPhone' => 'topic/iphone', + 'iPad' => 'topic/ipad', + 'IT Priorities' => 'topic/it-priorities', + 'Laptops' => 'topic/laptops', + 'Legal' => 'topic/legal', + 'Linux' => 'topic/linux', + 'Microsoft' => 'topic/microsoft', + 'Mobile OS' => 'topic/mobile-os', + 'Mobility' => 'topic/mobility', + 'Networking' => 'topic/networking', + 'Oracle' => 'topic/oracle', + 'Processors' => 'topic/processors', + 'Samsung' => 'topic/samsung', + 'Security' => 'topic/security', + 'Small business: going big on mobility' => 'topic/small-business-going-big-on-mobility' + ], + 'Product Blogs:' => [ + 'Digital Cameras & Camcorders' => 'blog/digitalcameras', + 'Home Theater' => 'blog/home-theater', + 'Laptops and Desktops' => 'blog/computers', + 'The Mobile Gadgeteer' => 'blog/mobile-gadgeteer', + 'Smartphones and Cell Phones' => 'blog/cell-phones', + 'The ToyBox' => 'blog/gadgetreviews' + ], + 'Vertical Blogs:' => [ + 'ZDNet Education' => 'blog/education', + 'ZDNet Healthcare' => 'blog/healthcare', + 'ZDNet Government' => 'blog/government' + ] + ] + ], + 'limit' => self::LIMIT, + ]]; - public function collectData(){ - $baseUri = static::URI; - $feed = $this->getInput('feed'); - if(strpos($feed, 'downloads!') !== false) { - $feed = str_replace('downloads!', '', $feed); - $baseUri = str_replace('www.', 'downloads.', $baseUri); - } - $url = $baseUri . trim($feed, '/') . '/rss.xml'; - $limit = $this->getInput('limit') ?? 10; - $this->collectExpandableDatas($url, $limit); - } + public function collectData() + { + $baseUri = static::URI; + $feed = $this->getInput('feed'); + if (strpos($feed, 'downloads!') !== false) { + $feed = str_replace('downloads!', '', $feed); + $baseUri = str_replace('www.', 'downloads.', $baseUri); + } + $url = $baseUri . trim($feed, '/') . '/rss.xml'; + $limit = $this->getInput('limit') ?? 10; + $this->collectExpandableDatas($url, $limit); + } - protected function parseItem($item){ - $item = parent::parseItem($item); + protected function parseItem($item) + { + $item = parent::parseItem($item); - $article = getSimpleHTMLDOMCached($item['uri']); - if(!$article) - returnServerError('Could not request ZDNet: ' . $url); + $article = getSimpleHTMLDOMCached($item['uri']); + if (!$article) { + returnServerError('Could not request ZDNet: ' . $url); + } - $contents = $article->find('article', 0)->innertext; - foreach(array( - '<div class="shareBar"', - '<div class="shortcodeGalleryWrapper"', - '<div class="relatedContent', - '<div class="downloadNow', - '<div data-shortcode', - '<div id="sharethrough', - '<div id="inpage-video', - '<div class="share-bar-wrapper"', - ) as $div_start) { - $contents = stripRecursiveHtmlSection($contents, 'div', $div_start); - } - $contents = stripWithDelimiters($contents, '<script', '</script>'); - $contents = stripWithDelimiters($contents, '<meta itemprop="image"', '>'); - $contents = stripWithDelimiters($contents, '<svg class="svg-symbol', '</svg>'); - $contents = trim(stripWithDelimiters($contents, '<section class="sharethrough-top', '</section>')); - $item['content'] = $contents; + $contents = $article->find('article', 0)->innertext; + foreach ( + [ + '<div class="shareBar"', + '<div class="shortcodeGalleryWrapper"', + '<div class="relatedContent', + '<div class="downloadNow', + '<div data-shortcode', + '<div id="sharethrough', + '<div id="inpage-video', + '<div class="share-bar-wrapper"', + ] as $div_start + ) { + $contents = stripRecursiveHtmlSection($contents, 'div', $div_start); + } + $contents = stripWithDelimiters($contents, '<script', '</script>'); + $contents = stripWithDelimiters($contents, '<meta itemprop="image"', '>'); + $contents = stripWithDelimiters($contents, '<svg class="svg-symbol', '</svg>'); + $contents = trim(stripWithDelimiters($contents, '<section class="sharethrough-top', '</section>')); + $item['content'] = $contents; - return $item; - - } + return $item; + } } diff --git a/bridges/ZenodoBridge.php b/bridges/ZenodoBridge.php index 6d0c134b..1144c90c 100644 --- a/bridges/ZenodoBridge.php +++ b/bridges/ZenodoBridge.php @@ -1,54 +1,56 @@ <?php -class ZenodoBridge extends BridgeAbstract { - const MAINTAINER = 'theradialactive'; - const NAME = 'Zenodo'; - const URI = 'https://zenodo.org'; - const CACHE_TIMEOUT = 10; - const DESCRIPTION = 'Returns the newest content of Zenodo'; - - public function collectData(){ - $html = getSimpleHTMLDOM($this->getURI()); - - foreach($html->find('div.record-elem.row') as $element) { - $item = array(); - $item['uri'] = self::URI . $element->find('h4 > a', 0)->href; - $item['title'] = trim(htmlspecialchars_decode($element->find('h4 > a', 0)->innertext, ENT_QUOTES)); - - $authors = $element->find('p', 0); - if ($authors) { - $item['author'] = $authors->plaintext; - } - - $summary = $element->find('p.hidden-xs > a', 0); - if ($summary) { - $content = $summary->innertext . '<br>'; - } else { - $content = 'No content'; - } - - $type = '<br>Type: ' . $element->find('span.label-default', 0)->innertext; - $item['categories'] = array($element->find('span.label-default', 0)->innertext); - - $raw_date = $element->find('small.text-muted', 0)->innertext; - $clean_date = str_replace('Uploaded on ', '', $raw_date); - - $content = $content . $raw_date; - - $item['timestamp'] = $clean_date; - - $access = ''; - if ($element->find('span.label-success', 0)) { - $access = 'Open Access'; - } elseif ($element->find('span.label-warning', 0)) { - $access = 'Embargoed Access'; - } else { - $access = $element->find('span.label-error', 0)->innertext; - } - $access = '<br>Access: ' . $access; - $publication = '<br>Publication Date: ' . $element->find('span.label-info', 0)->innertext; - $item['content'] = $content . $type . $access . $publication; - $this->items[] = $item; - } - } +class ZenodoBridge extends BridgeAbstract +{ + const MAINTAINER = 'theradialactive'; + const NAME = 'Zenodo'; + const URI = 'https://zenodo.org'; + const CACHE_TIMEOUT = 10; + const DESCRIPTION = 'Returns the newest content of Zenodo'; + + public function collectData() + { + $html = getSimpleHTMLDOM($this->getURI()); + + foreach ($html->find('div.record-elem.row') as $element) { + $item = []; + $item['uri'] = self::URI . $element->find('h4 > a', 0)->href; + $item['title'] = trim(htmlspecialchars_decode($element->find('h4 > a', 0)->innertext, ENT_QUOTES)); + + $authors = $element->find('p', 0); + if ($authors) { + $item['author'] = $authors->plaintext; + } + + $summary = $element->find('p.hidden-xs > a', 0); + if ($summary) { + $content = $summary->innertext . '<br>'; + } else { + $content = 'No content'; + } + + $type = '<br>Type: ' . $element->find('span.label-default', 0)->innertext; + $item['categories'] = [$element->find('span.label-default', 0)->innertext]; + + $raw_date = $element->find('small.text-muted', 0)->innertext; + $clean_date = str_replace('Uploaded on ', '', $raw_date); + + $content = $content . $raw_date; + + $item['timestamp'] = $clean_date; + + $access = ''; + if ($element->find('span.label-success', 0)) { + $access = 'Open Access'; + } elseif ($element->find('span.label-warning', 0)) { + $access = 'Embargoed Access'; + } else { + $access = $element->find('span.label-error', 0)->innertext; + } + $access = '<br>Access: ' . $access; + $publication = '<br>Publication Date: ' . $element->find('span.label-info', 0)->innertext; + $item['content'] = $content . $type . $access . $publication; + $this->items[] = $item; + } + } } diff --git a/caches/FileCache.php b/caches/FileCache.php index 1b8ae6cd..29f4d78b 100644 --- a/caches/FileCache.php +++ b/caches/FileCache.php @@ -1,137 +1,150 @@ <?php + /** * Cache with file system */ -class FileCache implements CacheInterface { - protected $path; - protected $key; - - public function __construct() { - if (!is_writable(PATH_CACHE)) { - returnServerError( - 'RSS-Bridge does not have write permissions for ' - . PATH_CACHE . '!' - ); - } - } - - public function loadData(){ - if(file_exists($this->getCacheFile())) { - return unserialize(file_get_contents($this->getCacheFile())); - } - - return null; - } - - public function saveData($data){ - // Notice: We use plain serialize() here to reduce memory footprint on - // large input data. - $writeStream = file_put_contents($this->getCacheFile(), serialize($data)); - - if($writeStream === false) { - throw new \Exception('Cannot write the cache... Do you have the right permissions ?'); - } - - return $this; - } - - public function getTime(){ - $cacheFile = $this->getCacheFile(); - clearstatcache(false, $cacheFile); - if(file_exists($cacheFile)) { - $time = filemtime($cacheFile); - return ($time !== false) ? $time : null; - } - - return null; - } - - public function purgeCache($seconds){ - $cachePath = $this->getPath(); - if(file_exists($cachePath)) { - $cacheIterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($cachePath), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach($cacheIterator as $cacheFile) { - if(in_array($cacheFile->getBasename(), array('.', '..', '.gitkeep'))) - continue; - elseif($cacheFile->isFile()) { - if(filemtime($cacheFile->getPathname()) < time() - $seconds) - unlink($cacheFile->getPathname()); - } - } - } - } - - /** - * Set scope - * @return self - */ - public function setScope($scope){ - if(is_null($scope) || !is_string($scope)) { - throw new \Exception('The given scope is invalid!'); - } - - $this->path = PATH_CACHE . trim($scope, " \t\n\r\0\x0B\\\/") . '/'; - - return $this; - } - - /** - * Set key - * @return self - */ - public function setKey($key){ - if (!empty($key) && is_array($key)) { - $key = array_map('strtolower', $key); - } - $key = json_encode($key); - - if (!is_string($key)) { - throw new \Exception('The given key is invalid!'); - } - - $this->key = $key; - return $this; - } - - /** - * Return cache path (and create if not exist) - * @return string Cache path - */ - private function getPath(){ - if(is_null($this->path)) { - throw new \Exception('Call "setScope" first!'); - } - - if(!is_dir($this->path)) { - if (mkdir($this->path, 0755, true) !== true) { - throw new \Exception('Unable to create ' . $this->path); - } - } - - return $this->path; - } - - /** - * Get the file name use for cache store - * @return string Path to the file cache - */ - private function getCacheFile(){ - return $this->getPath() . $this->getCacheName(); - } - - /** - * Determines file name for store the cache - * return string - */ - private function getCacheName(){ - if(is_null($this->key)) { - throw new \Exception('Call "setKey" first!'); - } - - return hash('md5', $this->key) . '.cache'; - } +class FileCache implements CacheInterface +{ + protected $path; + protected $key; + + public function __construct() + { + if (!is_writable(PATH_CACHE)) { + returnServerError( + 'RSS-Bridge does not have write permissions for ' + . PATH_CACHE . '!' + ); + } + } + + public function loadData() + { + if (file_exists($this->getCacheFile())) { + return unserialize(file_get_contents($this->getCacheFile())); + } + + return null; + } + + public function saveData($data) + { + // Notice: We use plain serialize() here to reduce memory footprint on + // large input data. + $writeStream = file_put_contents($this->getCacheFile(), serialize($data)); + + if ($writeStream === false) { + throw new \Exception('Cannot write the cache... Do you have the right permissions ?'); + } + + return $this; + } + + public function getTime() + { + $cacheFile = $this->getCacheFile(); + clearstatcache(false, $cacheFile); + if (file_exists($cacheFile)) { + $time = filemtime($cacheFile); + return ($time !== false) ? $time : null; + } + + return null; + } + + public function purgeCache($seconds) + { + $cachePath = $this->getPath(); + if (file_exists($cachePath)) { + $cacheIterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($cachePath), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($cacheIterator as $cacheFile) { + if (in_array($cacheFile->getBasename(), ['.', '..', '.gitkeep'])) { + continue; + } elseif ($cacheFile->isFile()) { + if (filemtime($cacheFile->getPathname()) < time() - $seconds) { + unlink($cacheFile->getPathname()); + } + } + } + } + } + + /** + * Set scope + * @return self + */ + public function setScope($scope) + { + if (is_null($scope) || !is_string($scope)) { + throw new \Exception('The given scope is invalid!'); + } + + $this->path = PATH_CACHE . trim($scope, " \t\n\r\0\x0B\\\/") . '/'; + + return $this; + } + + /** + * Set key + * @return self + */ + public function setKey($key) + { + if (!empty($key) && is_array($key)) { + $key = array_map('strtolower', $key); + } + $key = json_encode($key); + + if (!is_string($key)) { + throw new \Exception('The given key is invalid!'); + } + + $this->key = $key; + return $this; + } + + /** + * Return cache path (and create if not exist) + * @return string Cache path + */ + private function getPath() + { + if (is_null($this->path)) { + throw new \Exception('Call "setScope" first!'); + } + + if (!is_dir($this->path)) { + if (mkdir($this->path, 0755, true) !== true) { + throw new \Exception('Unable to create ' . $this->path); + } + } + + return $this->path; + } + + /** + * Get the file name use for cache store + * @return string Path to the file cache + */ + private function getCacheFile() + { + return $this->getPath() . $this->getCacheName(); + } + + /** + * Determines file name for store the cache + * return string + */ + private function getCacheName() + { + if (is_null($this->key)) { + throw new \Exception('Call "setKey" first!'); + } + + return hash('md5', $this->key) . '.cache'; + } } diff --git a/caches/MemcachedCache.php b/caches/MemcachedCache.php index b431279a..8619c255 100644 --- a/caches/MemcachedCache.php +++ b/caches/MemcachedCache.php @@ -1,115 +1,126 @@ <?php -class MemcachedCache implements CacheInterface { - - private $scope; - private $key; - private $conn; - private $expiration = 0; - private $time = false; - private $data = null; - - public function __construct() { - if (!extension_loaded('memcached')) { - returnServerError('"memcached" extension not loaded. Please check "php.ini"'); - } - - $host = Configuration::getConfig(get_called_class(), 'host'); - $port = Configuration::getConfig(get_called_class(), 'port'); - if (empty($host) && empty($port)) { - returnServerError('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG); - } else if (empty($host)) { - returnServerError('"host" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); - } else if (empty($port)) { - returnServerError('"port" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); - } else if (!ctype_digit($port)) { - returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); - } - - $port = intval($port); - - if ($port < 1 || $port > 65535) { - returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); - } - - $conn = new Memcached(); - $conn->addServer($host, $port) or returnServerError('Could not connect to memcached server'); - $this->conn = $conn; - } - - public function loadData(){ - if ($this->data) return $this->data; - $result = $this->conn->get($this->getCacheKey()); - if ($result === false) { - return null; - } - - $this->time = $result['time']; - $this->data = $result['data']; - return $result['data']; - } - - public function saveData($datas){ - $time = time(); - $object_to_save = array( - 'data' => $datas, - 'time' => $time, - ); - $result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration); - - if($result === false) { - returnServerError('Cannot write the cache to memcached server'); - } - - $this->time = $time; - - return $this; - } - - public function getTime(){ - if ($this->time === false) { - $this->loadData(); - } - return $this->time; - } - - public function purgeCache($duration){ - // Note: does not purges cache right now - // Just sets cache expiration and leave cache purging for memcached itself - $this->expiration = $duration; - } - - /** - * Set scope - * @return self - */ - public function setScope($scope){ - $this->scope = $scope; - return $this; - } - - /** - * Set key - * @return self - */ - public function setKey($key){ - if (!empty($key) && is_array($key)) { - $key = array_map('strtolower', $key); - } - $key = json_encode($key); - - if (!is_string($key)) { - throw new \Exception('The given key is invalid!'); - } - - $this->key = $key; - return $this; - } - - private function getCacheKey(){ - if(is_null($this->key)) { - returnServerError('Call "setKey" first!'); - } - - return 'rss_bridge_cache_' . hash('md5', $this->scope . $this->key . 'A'); - } + +class MemcachedCache implements CacheInterface +{ + private $scope; + private $key; + private $conn; + private $expiration = 0; + private $time = false; + private $data = null; + + public function __construct() + { + if (!extension_loaded('memcached')) { + returnServerError('"memcached" extension not loaded. Please check "php.ini"'); + } + + $host = Configuration::getConfig(get_called_class(), 'host'); + $port = Configuration::getConfig(get_called_class(), 'port'); + if (empty($host) && empty($port)) { + returnServerError('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG); + } elseif (empty($host)) { + returnServerError('"host" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); + } elseif (empty($port)) { + returnServerError('"port" param is not set for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); + } elseif (!ctype_digit($port)) { + returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); + } + + $port = intval($port); + + if ($port < 1 || $port > 65535) { + returnServerError('"port" param is invalid for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); + } + + $conn = new Memcached(); + $conn->addServer($host, $port) or returnServerError('Could not connect to memcached server'); + $this->conn = $conn; + } + + public function loadData() + { + if ($this->data) { + return $this->data; + } + $result = $this->conn->get($this->getCacheKey()); + if ($result === false) { + return null; + } + + $this->time = $result['time']; + $this->data = $result['data']; + return $result['data']; + } + + public function saveData($datas) + { + $time = time(); + $object_to_save = [ + 'data' => $datas, + 'time' => $time, + ]; + $result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration); + + if ($result === false) { + returnServerError('Cannot write the cache to memcached server'); + } + + $this->time = $time; + + return $this; + } + + public function getTime() + { + if ($this->time === false) { + $this->loadData(); + } + return $this->time; + } + + public function purgeCache($duration) + { + // Note: does not purges cache right now + // Just sets cache expiration and leave cache purging for memcached itself + $this->expiration = $duration; + } + + /** + * Set scope + * @return self + */ + public function setScope($scope) + { + $this->scope = $scope; + return $this; + } + + /** + * Set key + * @return self + */ + public function setKey($key) + { + if (!empty($key) && is_array($key)) { + $key = array_map('strtolower', $key); + } + $key = json_encode($key); + + if (!is_string($key)) { + throw new \Exception('The given key is invalid!'); + } + + $this->key = $key; + return $this; + } + + private function getCacheKey() + { + if (is_null($this->key)) { + returnServerError('Call "setKey" first!'); + } + + return 'rss_bridge_cache_' . hash('md5', $this->scope . $this->key . 'A'); + } } diff --git a/caches/SQLiteCache.php b/caches/SQLiteCache.php index 5ec69417..e8d020a5 100644 --- a/caches/SQLiteCache.php +++ b/caches/SQLiteCache.php @@ -1,128 +1,138 @@ <?php + /** * Cache based on SQLite 3 <https://www.sqlite.org> */ -class SQLiteCache implements CacheInterface { - protected $scope; - protected $key; - - private $db = null; - - public function __construct() { - if (!extension_loaded('sqlite3')) { - die('"sqlite3" extension not loaded. Please check "php.ini"'); - } - - if (!is_writable(PATH_CACHE)) { - returnServerError( - 'RSS-Bridge does not have write permissions for ' - . PATH_CACHE . '!' - ); - } - - $file = Configuration::getConfig(get_called_class(), 'file'); - if (empty($file)) { - die('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG); - } - if (dirname($file) == '.') { - $file = PATH_CACHE . $file; - } elseif (!is_dir(dirname($file))) { - die('Invalid configuration for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); - } - - if (!is_file($file)) { - $this->db = new SQLite3($file); - $this->db->enableExceptions(true); - $this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)"); - } else { - $this->db = new SQLite3($file); - $this->db->enableExceptions(true); - } - $this->db->busyTimeout(5000); - } - - public function loadData(){ - $Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key'); - $Qselect->bindValue(':key', $this->getCacheKey()); - $result = $Qselect->execute(); - if ($result instanceof SQLite3Result) { - $data = $result->fetchArray(SQLITE3_ASSOC); - if (isset($data['value'])) { - return unserialize($data['value']); - } - } - - return null; - } - - public function saveData($data){ - $Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)'); - $Qupdate->bindValue(':key', $this->getCacheKey()); - $Qupdate->bindValue(':value', serialize($data)); - $Qupdate->bindValue(':updated', time()); - $Qupdate->execute(); - - return $this; - } - - public function getTime(){ - $Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key'); - $Qselect->bindValue(':key', $this->getCacheKey()); - $result = $Qselect->execute(); - if ($result instanceof SQLite3Result) { - $data = $result->fetchArray(SQLITE3_ASSOC); - if (isset($data['updated'])) { - return $data['updated']; - } - } - - return null; - } - - public function purgeCache($seconds){ - $Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired'); - $Qdelete->bindValue(':expired', time() - $seconds); - $Qdelete->execute(); - } - - /** - * Set scope - * @return self - */ - public function setScope($scope){ - if(is_null($scope) || !is_string($scope)) { - throw new \Exception('The given scope is invalid!'); - } - - $this->scope = $scope; - return $this; - } - - /** - * Set key - * @return self - */ - public function setKey($key){ - if (!empty($key) && is_array($key)) { - $key = array_map('strtolower', $key); - } - $key = json_encode($key); - - if (!is_string($key)) { - throw new \Exception('The given key is invalid!'); - } - - $this->key = $key; - return $this; - } - - //////////////////////////////////////////////////////////////////////////// - - private function getCacheKey(){ - if(is_null($this->key)) { - throw new \Exception('Call "setKey" first!'); - } - - return hash('sha1', $this->scope . $this->key, true); - } +class SQLiteCache implements CacheInterface +{ + protected $scope; + protected $key; + + private $db = null; + + public function __construct() + { + if (!extension_loaded('sqlite3')) { + die('"sqlite3" extension not loaded. Please check "php.ini"'); + } + + if (!is_writable(PATH_CACHE)) { + returnServerError( + 'RSS-Bridge does not have write permissions for ' + . PATH_CACHE . '!' + ); + } + + $file = Configuration::getConfig(get_called_class(), 'file'); + if (empty($file)) { + die('Configuration for ' . get_called_class() . ' missing. Please check your ' . FILE_CONFIG); + } + if (dirname($file) == '.') { + $file = PATH_CACHE . $file; + } elseif (!is_dir(dirname($file))) { + die('Invalid configuration for ' . get_called_class() . '. Please check your ' . FILE_CONFIG); + } + + if (!is_file($file)) { + $this->db = new SQLite3($file); + $this->db->enableExceptions(true); + $this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)"); + } else { + $this->db = new SQLite3($file); + $this->db->enableExceptions(true); + } + $this->db->busyTimeout(5000); + } + + public function loadData() + { + $Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key'); + $Qselect->bindValue(':key', $this->getCacheKey()); + $result = $Qselect->execute(); + if ($result instanceof SQLite3Result) { + $data = $result->fetchArray(SQLITE3_ASSOC); + if (isset($data['value'])) { + return unserialize($data['value']); + } + } + + return null; + } + + public function saveData($data) + { + $Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)'); + $Qupdate->bindValue(':key', $this->getCacheKey()); + $Qupdate->bindValue(':value', serialize($data)); + $Qupdate->bindValue(':updated', time()); + $Qupdate->execute(); + + return $this; + } + + public function getTime() + { + $Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key'); + $Qselect->bindValue(':key', $this->getCacheKey()); + $result = $Qselect->execute(); + if ($result instanceof SQLite3Result) { + $data = $result->fetchArray(SQLITE3_ASSOC); + if (isset($data['updated'])) { + return $data['updated']; + } + } + + return null; + } + + public function purgeCache($seconds) + { + $Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired'); + $Qdelete->bindValue(':expired', time() - $seconds); + $Qdelete->execute(); + } + + /** + * Set scope + * @return self + */ + public function setScope($scope) + { + if (is_null($scope) || !is_string($scope)) { + throw new \Exception('The given scope is invalid!'); + } + + $this->scope = $scope; + return $this; + } + + /** + * Set key + * @return self + */ + public function setKey($key) + { + if (!empty($key) && is_array($key)) { + $key = array_map('strtolower', $key); + } + $key = json_encode($key); + + if (!is_string($key)) { + throw new \Exception('The given key is invalid!'); + } + + $this->key = $key; + return $this; + } + + //////////////////////////////////////////////////////////////////////////// + + private function getCacheKey() + { + if (is_null($this->key)) { + throw new \Exception('Call "setKey" first!'); + } + + return hash('sha1', $this->scope . $this->key, true); + } } diff --git a/contrib/prepare_release/fetch_contributors.php b/contrib/prepare_release/fetch_contributors.php index 9659b800..76cef24f 100644 --- a/contrib/prepare_release/fetch_contributors.php +++ b/contrib/prepare_release/fetch_contributors.php @@ -1,49 +1,49 @@ <?php + /* Generate the "Contributors" list for README.md automatically utilizing the GitHub API */ require __DIR__ . '/../../lib/rssbridge.php'; $url = 'https://api.github.com/repos/rss-bridge/rss-bridge/contributors'; -$contributors = array(); +$contributors = []; $next = true; -while($next) { /* Collect all contributors */ - - $headers = [ - 'Accept: application/json', - 'Content-Type: application/json', - 'User-Agent: RSS-Bridge' - ]; - $result = _http_request($url, ['headers' => $headers]); - - foreach(json_decode($result['body']) as $contributor) - $contributors[] = $contributor; - - // Extract links to "next", "last", etc... - $links = explode(',', $result['headers']['link'][0]); - $next = false; - - // Check if there is a link with 'rel="next"' - foreach($links as $link) { - list($url, $type) = explode(';', $link, 2); - - if(trim($type) === 'rel="next"') { - $url = trim(preg_replace('/([<>])/', '', $url)); - $next = true; - break; - } - } - +while ($next) { /* Collect all contributors */ + $headers = [ + 'Accept: application/json', + 'Content-Type: application/json', + 'User-Agent: RSS-Bridge' + ]; + $result = _http_request($url, ['headers' => $headers]); + + foreach (json_decode($result['body']) as $contributor) { + $contributors[] = $contributor; + } + + // Extract links to "next", "last", etc... + $links = explode(',', $result['headers']['link'][0]); + $next = false; + + // Check if there is a link with 'rel="next"' + foreach ($links as $link) { + list($url, $type) = explode(';', $link, 2); + + if (trim($type) === 'rel="next"') { + $url = trim(preg_replace('/([<>])/', '', $url)); + $next = true; + break; + } + } } /* Example JSON data: https://api.github.com/repos/rss-bridge/rss-bridge/contributors */ // We want contributors sorted by name -usort($contributors, function($a, $b){ - return strcasecmp($a->login, $b->login); +usort($contributors, function ($a, $b) { + return strcasecmp($a->login, $b->login); }); // Export as Markdown list -foreach($contributors as $contributor) { - echo " * [{$contributor->login}]({$contributor->html_url})\n"; +foreach ($contributors as $contributor) { + echo " * [{$contributor->login}]({$contributor->html_url})\n"; } diff --git a/formats/AtomFormat.php b/formats/AtomFormat.php index 3d9b7c93..5f564266 100644 --- a/formats/AtomFormat.php +++ b/formats/AtomFormat.php @@ -1,4 +1,5 @@ <?php + /** * AtomFormat - RFC 4287: The Atom Syndication Format * https://tools.ietf.org/html/rfc4287 @@ -6,178 +7,185 @@ * Validator: * https://validator.w3.org/feed/ */ -class AtomFormat extends FormatAbstract{ - const MIME_TYPE = 'application/atom+xml'; - - protected const ATOM_NS = 'http://www.w3.org/2005/Atom'; - protected const MRSS_NS = 'http://search.yahoo.com/mrss/'; - - const LIMIT_TITLE = 140; - - public function stringify(){ - $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://'; - $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : ''; - $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : ''; - $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : ''; - - $feedUrl = $urlPrefix . $urlHost . $urlRequest; - - $extraInfos = $this->getExtraInfos(); - $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY; - - $document = new DomDocument('1.0', $this->getCharset()); - $document->formatOutput = true; - $feed = $document->createElementNS(self::ATOM_NS, 'feed'); - $document->appendChild($feed); - $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS); - - $title = $document->createElement('title'); - $feed->appendChild($title); - $title->setAttribute('type', 'text'); - $title->appendChild($document->createTextNode($extraInfos['name'])); - - $id = $document->createElement('id'); - $feed->appendChild($id); - $id->appendChild($document->createTextNode($feedUrl)); - - $uriparts = parse_url($uri); - if(!empty($extraInfos['icon'])) { - $iconUrl = $extraInfos['icon']; - } else { - $iconUrl = $uriparts['scheme'] . '://' . $uriparts['host'] . '/favicon.ico'; - } - $icon = $document->createElement('icon'); - $feed->appendChild($icon); - $icon->appendChild($document->createTextNode($iconUrl)); - - $logo = $document->createElement('logo'); - $feed->appendChild($logo); - $logo->appendChild($document->createTextNode($iconUrl)); - - $feedTimestamp = gmdate(DATE_ATOM, $this->lastModified); - $updated = $document->createElement('updated'); - $feed->appendChild($updated); - $updated->appendChild($document->createTextNode($feedTimestamp)); - - // since we can't guarantee that all items have an author, - // a global feed author is mandatory - $feedAuthor = 'RSS-Bridge'; - $author = $document->createElement('author'); - $feed->appendChild($author); - $authorName = $document->createElement('name'); - $author->appendChild($authorName); - $authorName->appendChild($document->createTextNode($feedAuthor)); - - $linkAlternate = $document->createElement('link'); - $feed->appendChild($linkAlternate); - $linkAlternate->setAttribute('rel', 'alternate'); - $linkAlternate->setAttribute('type', 'text/html'); - $linkAlternate->setAttribute('href', $uri); - - $linkSelf = $document->createElement('link'); - $feed->appendChild($linkSelf); - $linkSelf->setAttribute('rel', 'self'); - $linkSelf->setAttribute('type', 'application/atom+xml'); - $linkSelf->setAttribute('href', $feedUrl); - - foreach($this->getItems() as $item) { - $entryTimestamp = $item->getTimestamp(); - $entryTitle = $item->getTitle(); - $entryContent = $item->getContent(); - $entryUri = $item->getURI(); - $entryID = ''; - - if (!empty($item->getUid())) - $entryID = 'urn:sha1:' . $item->getUid(); - - if (empty($entryID)) // Fallback to provided URI - $entryID = $entryUri; - - if (empty($entryID)) // Fallback to title and content - $entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent); - - if (empty($entryTimestamp)) - $entryTimestamp = $this->lastModified; - - if (empty($entryTitle)) { - $entryTitle = str_replace("\n", ' ', strip_tags($entryContent)); - if (strlen($entryTitle) > self::LIMIT_TITLE) { - $wrapPos = strpos(wordwrap($entryTitle, self::LIMIT_TITLE), "\n"); - $entryTitle = substr($entryTitle, 0, $wrapPos) . '...'; - } - } - - if (empty($entryContent)) - $entryContent = ' '; - - $entry = $document->createElement('entry'); - $feed->appendChild($entry); - - $title = $document->createElement('title'); - $entry->appendChild($title); - $title->setAttribute('type', 'html'); - $title->appendChild($document->createTextNode($entryTitle)); - - $entryTimestamp = gmdate(DATE_ATOM, $entryTimestamp); - $published = $document->createElement('published'); - $entry->appendChild($published); - $published->appendChild($document->createTextNode($entryTimestamp)); - - $updated = $document->createElement('updated'); - $entry->appendChild($updated); - $updated->appendChild($document->createTextNode($entryTimestamp)); - - $id = $document->createElement('id'); - $entry->appendChild($id); - $id->appendChild($document->createTextNode($entryID)); - - if (!empty($entryUri)) { - $entryLinkAlternate = $document->createElement('link'); - $entry->appendChild($entryLinkAlternate); - $entryLinkAlternate->setAttribute('rel', 'alternate'); - $entryLinkAlternate->setAttribute('type', 'text/html'); - $entryLinkAlternate->setAttribute('href', $entryUri); - } - - if (!empty($item->getAuthor())) { - $author = $document->createElement('author'); - $entry->appendChild($author); - $authorName = $document->createElement('name'); - $author->appendChild($authorName); - $authorName->appendChild($document->createTextNode($item->getAuthor())); - } - - $content = $document->createElement('content'); - $content->setAttribute('type', 'html'); - $content->appendChild($document->createTextNode($this->sanitizeHtml($entryContent))); - $entry->appendChild($content); - - foreach($item->getEnclosures() as $enclosure) { - $entryEnclosure = $document->createElement('link'); - $entry->appendChild($entryEnclosure); - $entryEnclosure->setAttribute('rel', 'enclosure'); - $entryEnclosure->setAttribute('type', getMimeType($enclosure)); - $entryEnclosure->setAttribute('href', $enclosure); - } - - foreach($item->getCategories() as $category) { - $entryCategory = $document->createElement('category'); - $entry->appendChild($entryCategory); - $entryCategory->setAttribute('term', $category); - } - - if (!empty($item->thumbnail)) { - $thumbnail = $document->createElementNS(self::MRSS_NS, 'thumbnail'); - $entry->appendChild($thumbnail); - $thumbnail->setAttribute('url', $item->thumbnail); - } - } - - $toReturn = $document->saveXML(); - - // Remove invalid characters - ini_set('mbstring.substitute_character', 'none'); - $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8'); - return $toReturn; - } +class AtomFormat extends FormatAbstract +{ + const MIME_TYPE = 'application/atom+xml'; + + protected const ATOM_NS = 'http://www.w3.org/2005/Atom'; + protected const MRSS_NS = 'http://search.yahoo.com/mrss/'; + + const LIMIT_TITLE = 140; + + public function stringify() + { + $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://'; + $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : ''; + $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : ''; + $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : ''; + + $feedUrl = $urlPrefix . $urlHost . $urlRequest; + + $extraInfos = $this->getExtraInfos(); + $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY; + + $document = new DomDocument('1.0', $this->getCharset()); + $document->formatOutput = true; + $feed = $document->createElementNS(self::ATOM_NS, 'feed'); + $document->appendChild($feed); + $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS); + + $title = $document->createElement('title'); + $feed->appendChild($title); + $title->setAttribute('type', 'text'); + $title->appendChild($document->createTextNode($extraInfos['name'])); + + $id = $document->createElement('id'); + $feed->appendChild($id); + $id->appendChild($document->createTextNode($feedUrl)); + + $uriparts = parse_url($uri); + if (!empty($extraInfos['icon'])) { + $iconUrl = $extraInfos['icon']; + } else { + $iconUrl = $uriparts['scheme'] . '://' . $uriparts['host'] . '/favicon.ico'; + } + $icon = $document->createElement('icon'); + $feed->appendChild($icon); + $icon->appendChild($document->createTextNode($iconUrl)); + + $logo = $document->createElement('logo'); + $feed->appendChild($logo); + $logo->appendChild($document->createTextNode($iconUrl)); + + $feedTimestamp = gmdate(DATE_ATOM, $this->lastModified); + $updated = $document->createElement('updated'); + $feed->appendChild($updated); + $updated->appendChild($document->createTextNode($feedTimestamp)); + + // since we can't guarantee that all items have an author, + // a global feed author is mandatory + $feedAuthor = 'RSS-Bridge'; + $author = $document->createElement('author'); + $feed->appendChild($author); + $authorName = $document->createElement('name'); + $author->appendChild($authorName); + $authorName->appendChild($document->createTextNode($feedAuthor)); + + $linkAlternate = $document->createElement('link'); + $feed->appendChild($linkAlternate); + $linkAlternate->setAttribute('rel', 'alternate'); + $linkAlternate->setAttribute('type', 'text/html'); + $linkAlternate->setAttribute('href', $uri); + + $linkSelf = $document->createElement('link'); + $feed->appendChild($linkSelf); + $linkSelf->setAttribute('rel', 'self'); + $linkSelf->setAttribute('type', 'application/atom+xml'); + $linkSelf->setAttribute('href', $feedUrl); + + foreach ($this->getItems() as $item) { + $entryTimestamp = $item->getTimestamp(); + $entryTitle = $item->getTitle(); + $entryContent = $item->getContent(); + $entryUri = $item->getURI(); + $entryID = ''; + + if (!empty($item->getUid())) { + $entryID = 'urn:sha1:' . $item->getUid(); + } + + if (empty($entryID)) { // Fallback to provided URI + $entryID = $entryUri; + } + + if (empty($entryID)) { // Fallback to title and content + $entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent); + } + + if (empty($entryTimestamp)) { + $entryTimestamp = $this->lastModified; + } + + if (empty($entryTitle)) { + $entryTitle = str_replace("\n", ' ', strip_tags($entryContent)); + if (strlen($entryTitle) > self::LIMIT_TITLE) { + $wrapPos = strpos(wordwrap($entryTitle, self::LIMIT_TITLE), "\n"); + $entryTitle = substr($entryTitle, 0, $wrapPos) . '...'; + } + } + + if (empty($entryContent)) { + $entryContent = ' '; + } + + $entry = $document->createElement('entry'); + $feed->appendChild($entry); + + $title = $document->createElement('title'); + $entry->appendChild($title); + $title->setAttribute('type', 'html'); + $title->appendChild($document->createTextNode($entryTitle)); + + $entryTimestamp = gmdate(DATE_ATOM, $entryTimestamp); + $published = $document->createElement('published'); + $entry->appendChild($published); + $published->appendChild($document->createTextNode($entryTimestamp)); + + $updated = $document->createElement('updated'); + $entry->appendChild($updated); + $updated->appendChild($document->createTextNode($entryTimestamp)); + + $id = $document->createElement('id'); + $entry->appendChild($id); + $id->appendChild($document->createTextNode($entryID)); + + if (!empty($entryUri)) { + $entryLinkAlternate = $document->createElement('link'); + $entry->appendChild($entryLinkAlternate); + $entryLinkAlternate->setAttribute('rel', 'alternate'); + $entryLinkAlternate->setAttribute('type', 'text/html'); + $entryLinkAlternate->setAttribute('href', $entryUri); + } + + if (!empty($item->getAuthor())) { + $author = $document->createElement('author'); + $entry->appendChild($author); + $authorName = $document->createElement('name'); + $author->appendChild($authorName); + $authorName->appendChild($document->createTextNode($item->getAuthor())); + } + + $content = $document->createElement('content'); + $content->setAttribute('type', 'html'); + $content->appendChild($document->createTextNode($this->sanitizeHtml($entryContent))); + $entry->appendChild($content); + + foreach ($item->getEnclosures() as $enclosure) { + $entryEnclosure = $document->createElement('link'); + $entry->appendChild($entryEnclosure); + $entryEnclosure->setAttribute('rel', 'enclosure'); + $entryEnclosure->setAttribute('type', getMimeType($enclosure)); + $entryEnclosure->setAttribute('href', $enclosure); + } + + foreach ($item->getCategories() as $category) { + $entryCategory = $document->createElement('category'); + $entry->appendChild($entryCategory); + $entryCategory->setAttribute('term', $category); + } + + if (!empty($item->thumbnail)) { + $thumbnail = $document->createElementNS(self::MRSS_NS, 'thumbnail'); + $entry->appendChild($thumbnail); + $thumbnail->setAttribute('url', $item->thumbnail); + } + } + + $toReturn = $document->saveXML(); + + // Remove invalid characters + ini_set('mbstring.substitute_character', 'none'); + $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8'); + return $toReturn; + } } diff --git a/formats/HtmlFormat.php b/formats/HtmlFormat.php index 12b5fc3a..d60c4d81 100644 --- a/formats/HtmlFormat.php +++ b/formats/HtmlFormat.php @@ -1,96 +1,97 @@ <?php -class HtmlFormat extends FormatAbstract { - const MIME_TYPE = 'text/html'; - - public function stringify(){ - $extraInfos = $this->getExtraInfos(); - $title = htmlspecialchars($extraInfos['name']); - $uri = htmlspecialchars($extraInfos['uri']); - $donationUri = htmlspecialchars($extraInfos['donationUri']); - $donationsAllowed = Configuration::getConfig('admin', 'donations'); - - // Dynamically build buttons for all formats (except HTML) - $formatFac = new FormatFactory(); - - $buttons = ''; - $links = ''; - - foreach($formatFac->getFormatNames() as $format) { - if(strcasecmp($format, 'HTML') === 0) { - continue; - } - - $query = str_ireplace('format=Html', 'format=' . $format, htmlentities($_SERVER['QUERY_STRING'])); - $buttons .= $this->buildButton($format, $query) . PHP_EOL; - - $mime = $formatFac->create($format)->getMimeType(); - $links .= $this->buildLink($format, $query, $mime) . PHP_EOL; - } - - if($donationUri !== '' && $donationsAllowed) { - $buttons .= '<a href="' - . $donationUri - . '" target="_blank"><button class="highlight">Donate to maintainer</button></a>' - . PHP_EOL; - $links .= '<link href="' - . $donationUri - . ' target="_blank"" title="Donate to Maintainer" rel="alternate">' - . PHP_EOL; - } - - $entries = ''; - foreach($this->getItems() as $item) { - $entryAuthor = $item->getAuthor() ? '<br /><p class="author">by: ' . $item->getAuthor() . '</p>' : ''; - $entryTitle = $this->sanitizeHtml(strip_tags($item->getTitle())); - $entryUri = $item->getURI() ?: $uri; - - $entryDate = ''; - if($item->getTimestamp()) { - - $entryDate = sprintf( - '<time datetime="%s">%s</time>', - date('Y-m-d H:i:s', $item->getTimestamp()), - date('Y-m-d H:i:s', $item->getTimestamp()) - ); - } - - $entryContent = ''; - if($item->getContent()) { - $entryContent = '<div class="content">' - . $this->sanitizeHtml($item->getContent()) - . '</div>'; - } - - $entryEnclosures = ''; - if(!empty($item->getEnclosures())) { - $entryEnclosures = '<div class="attachments"><p>Attachments:</p>'; - - foreach($item->getEnclosures() as $enclosure) { - $template = '<li class="enclosure"><a href="%s" rel="noopener noreferrer nofollow">%s</a></li>'; - $url = $this->sanitizeHtml($enclosure); - $anchorText = substr($url, strrpos($url, '/') + 1); - - $entryEnclosures .= sprintf($template, $url, $anchorText); - } - - $entryEnclosures .= '</div>'; - } - - $entryCategories = ''; - if(!empty($item->getCategories())) { - $entryCategories = '<div class="categories"><p>Categories:</p>'; - - foreach($item->getCategories() as $category) { - - $entryCategories .= '<li class="category">' - . $this->sanitizeHtml($category) - . '</li>'; - } - - $entryCategories .= '</div>'; - } - - $entries .= <<<EOD + +class HtmlFormat extends FormatAbstract +{ + const MIME_TYPE = 'text/html'; + + public function stringify() + { + $extraInfos = $this->getExtraInfos(); + $title = htmlspecialchars($extraInfos['name']); + $uri = htmlspecialchars($extraInfos['uri']); + $donationUri = htmlspecialchars($extraInfos['donationUri']); + $donationsAllowed = Configuration::getConfig('admin', 'donations'); + + // Dynamically build buttons for all formats (except HTML) + $formatFac = new FormatFactory(); + + $buttons = ''; + $links = ''; + + foreach ($formatFac->getFormatNames() as $format) { + if (strcasecmp($format, 'HTML') === 0) { + continue; + } + + $query = str_ireplace('format=Html', 'format=' . $format, htmlentities($_SERVER['QUERY_STRING'])); + $buttons .= $this->buildButton($format, $query) . PHP_EOL; + + $mime = $formatFac->create($format)->getMimeType(); + $links .= $this->buildLink($format, $query, $mime) . PHP_EOL; + } + + if ($donationUri !== '' && $donationsAllowed) { + $buttons .= '<a href="' + . $donationUri + . '" target="_blank"><button class="highlight">Donate to maintainer</button></a>' + . PHP_EOL; + $links .= '<link href="' + . $donationUri + . ' target="_blank"" title="Donate to Maintainer" rel="alternate">' + . PHP_EOL; + } + + $entries = ''; + foreach ($this->getItems() as $item) { + $entryAuthor = $item->getAuthor() ? '<br /><p class="author">by: ' . $item->getAuthor() . '</p>' : ''; + $entryTitle = $this->sanitizeHtml(strip_tags($item->getTitle())); + $entryUri = $item->getURI() ?: $uri; + + $entryDate = ''; + if ($item->getTimestamp()) { + $entryDate = sprintf( + '<time datetime="%s">%s</time>', + date('Y-m-d H:i:s', $item->getTimestamp()), + date('Y-m-d H:i:s', $item->getTimestamp()) + ); + } + + $entryContent = ''; + if ($item->getContent()) { + $entryContent = '<div class="content">' + . $this->sanitizeHtml($item->getContent()) + . '</div>'; + } + + $entryEnclosures = ''; + if (!empty($item->getEnclosures())) { + $entryEnclosures = '<div class="attachments"><p>Attachments:</p>'; + + foreach ($item->getEnclosures() as $enclosure) { + $template = '<li class="enclosure"><a href="%s" rel="noopener noreferrer nofollow">%s</a></li>'; + $url = $this->sanitizeHtml($enclosure); + $anchorText = substr($url, strrpos($url, '/') + 1); + + $entryEnclosures .= sprintf($template, $url, $anchorText); + } + + $entryEnclosures .= '</div>'; + } + + $entryCategories = ''; + if (!empty($item->getCategories())) { + $entryCategories = '<div class="categories"><p>Categories:</p>'; + + foreach ($item->getCategories() as $category) { + $entryCategories .= '<li class="category">' + . $this->sanitizeHtml($category) + . '</li>'; + } + + $entryCategories .= '</div>'; + } + + $entries .= <<<EOD <section class="feeditem"> <h2><a class="itemtitle" href="{$entryUri}">{$entryTitle}</a></h2> @@ -102,12 +103,12 @@ class HtmlFormat extends FormatAbstract { </section> EOD; - } + } - $charset = $this->getCharset(); + $charset = $this->getCharset(); - /* Data are prepared, now let's begin the "MAGIE !!!" */ - $toReturn = <<<EOD + /* Data are prepared, now let's begin the "MAGIE !!!" */ + $toReturn = <<<EOD <!DOCTYPE html> <html> <head> @@ -130,22 +131,24 @@ EOD; </html> EOD; - // Remove invalid characters - ini_set('mbstring.substitute_character', 'none'); - $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8'); - return $toReturn; - } + // Remove invalid characters + ini_set('mbstring.substitute_character', 'none'); + $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8'); + return $toReturn; + } - private function buildButton($format, $query) { - return <<<EOD + private function buildButton($format, $query) + { + return <<<EOD <a href="./?{$query}"><button class="rss-feed">{$format}</button></a> EOD; - } + } - private function buildLink($format, $query, $mime) { - return <<<EOD + private function buildLink($format, $query, $mime) + { + return <<<EOD <link href="./?{$query}" title="{$format}" rel="alternate" type="{$mime}"> EOD; - } + } } diff --git a/formats/JsonFormat.php b/formats/JsonFormat.php index 1efc87fe..3b2a29ab 100644 --- a/formats/JsonFormat.php +++ b/formats/JsonFormat.php @@ -1,4 +1,5 @@ <?php + /** * JsonFormat - JSON Feed Version 1 * https://jsonfeed.org/version/1 @@ -7,122 +8,126 @@ * https://validator.jsonfeed.org * https://github.com/vigetlabs/json-feed-validator */ -class JsonFormat extends FormatAbstract { - const MIME_TYPE = 'application/json'; - - const VENDOR_EXCLUDES = array( - 'author', - 'title', - 'uri', - 'timestamp', - 'content', - 'enclosures', - 'categories', - 'uid', - ); - - public function stringify(){ - $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://'; - $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : ''; - $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : ''; - $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : ''; - - $extraInfos = $this->getExtraInfos(); - - $data = array( - 'version' => 'https://jsonfeed.org/version/1', - 'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost, - 'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY, - 'feed_url' => $urlPrefix . $urlHost . $urlRequest - ); - - if (!empty($extraInfos['icon'])) { - $data['icon'] = $extraInfos['icon']; - $data['favicon'] = $extraInfos['icon']; - } - - $items = array(); - foreach ($this->getItems() as $item) { - $entry = array(); - - $entryAuthor = $item->getAuthor(); - $entryTitle = $item->getTitle(); - $entryUri = $item->getURI(); - $entryTimestamp = $item->getTimestamp(); - $entryContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : ''; - $entryEnclosures = $item->getEnclosures(); - $entryCategories = $item->getCategories(); - - $vendorFields = $item->toArray(); - foreach (self::VENDOR_EXCLUDES as $key) { - unset($vendorFields[$key]); - } - - $entry['id'] = $item->getUid(); - - if (empty($entry['id'])) { - $entry['id'] = $entryUri; - } - - if (!empty($entryTitle)) { - $entry['title'] = $entryTitle; - } - if (!empty($entryAuthor)) { - $entry['author'] = array( - 'name' => $entryAuthor - ); - } - if (!empty($entryTimestamp)) { - $entry['date_modified'] = gmdate(DATE_ATOM, $entryTimestamp); - } - if (!empty($entryUri)) { - $entry['url'] = $entryUri; - } - if (!empty($entryContent)) { - if ($this->isHTML($entryContent)) { - $entry['content_html'] = $entryContent; - } else { - $entry['content_text'] = $entryContent; - } - } - if (!empty($entryEnclosures)) { - $entry['attachments'] = array(); - foreach ($entryEnclosures as $enclosure) { - $entry['attachments'][] = array( - 'url' => $enclosure, - 'mime_type' => getMimeType($enclosure) - ); - } - } - if (!empty($entryCategories)) { - $entry['tags'] = array(); - foreach ($entryCategories as $category) { - $entry['tags'][] = $category; - } - } - if (!empty($vendorFields)) { - $entry['_rssbridge'] = $vendorFields; - } - - if (empty($entry['id'])) - $entry['id'] = hash('sha1', $entryTitle . $entryContent); - - $items[] = $entry; - } - $data['items'] = $items; - - /** - * The intention here is to discard non-utf8 byte sequences. - * But the JSON_PARTIAL_OUTPUT_ON_ERROR also discards lots of other errors. - * So consider this a hack. - * Switch to JSON_INVALID_UTF8_IGNORE when PHP 7.2 is the latest platform requirement. - */ - $json = json_encode($data, JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR); - - return $json; - } - - private function isHTML($text) { - return (strlen(strip_tags($text)) != strlen($text)); - } +class JsonFormat extends FormatAbstract +{ + const MIME_TYPE = 'application/json'; + + const VENDOR_EXCLUDES = [ + 'author', + 'title', + 'uri', + 'timestamp', + 'content', + 'enclosures', + 'categories', + 'uid', + ]; + + public function stringify() + { + $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://'; + $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : ''; + $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : ''; + $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : ''; + + $extraInfos = $this->getExtraInfos(); + + $data = [ + 'version' => 'https://jsonfeed.org/version/1', + 'title' => (!empty($extraInfos['name'])) ? $extraInfos['name'] : $urlHost, + 'home_page_url' => (!empty($extraInfos['uri'])) ? $extraInfos['uri'] : REPOSITORY, + 'feed_url' => $urlPrefix . $urlHost . $urlRequest + ]; + + if (!empty($extraInfos['icon'])) { + $data['icon'] = $extraInfos['icon']; + $data['favicon'] = $extraInfos['icon']; + } + + $items = []; + foreach ($this->getItems() as $item) { + $entry = []; + + $entryAuthor = $item->getAuthor(); + $entryTitle = $item->getTitle(); + $entryUri = $item->getURI(); + $entryTimestamp = $item->getTimestamp(); + $entryContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : ''; + $entryEnclosures = $item->getEnclosures(); + $entryCategories = $item->getCategories(); + + $vendorFields = $item->toArray(); + foreach (self::VENDOR_EXCLUDES as $key) { + unset($vendorFields[$key]); + } + + $entry['id'] = $item->getUid(); + + if (empty($entry['id'])) { + $entry['id'] = $entryUri; + } + + if (!empty($entryTitle)) { + $entry['title'] = $entryTitle; + } + if (!empty($entryAuthor)) { + $entry['author'] = [ + 'name' => $entryAuthor + ]; + } + if (!empty($entryTimestamp)) { + $entry['date_modified'] = gmdate(DATE_ATOM, $entryTimestamp); + } + if (!empty($entryUri)) { + $entry['url'] = $entryUri; + } + if (!empty($entryContent)) { + if ($this->isHTML($entryContent)) { + $entry['content_html'] = $entryContent; + } else { + $entry['content_text'] = $entryContent; + } + } + if (!empty($entryEnclosures)) { + $entry['attachments'] = []; + foreach ($entryEnclosures as $enclosure) { + $entry['attachments'][] = [ + 'url' => $enclosure, + 'mime_type' => getMimeType($enclosure) + ]; + } + } + if (!empty($entryCategories)) { + $entry['tags'] = []; + foreach ($entryCategories as $category) { + $entry['tags'][] = $category; + } + } + if (!empty($vendorFields)) { + $entry['_rssbridge'] = $vendorFields; + } + + if (empty($entry['id'])) { + $entry['id'] = hash('sha1', $entryTitle . $entryContent); + } + + $items[] = $entry; + } + $data['items'] = $items; + + /** + * The intention here is to discard non-utf8 byte sequences. + * But the JSON_PARTIAL_OUTPUT_ON_ERROR also discards lots of other errors. + * So consider this a hack. + * Switch to JSON_INVALID_UTF8_IGNORE when PHP 7.2 is the latest platform requirement. + */ + $json = json_encode($data, JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR); + + return $json; + } + + private function isHTML($text) + { + return (strlen(strip_tags($text)) != strlen($text)); + } } diff --git a/formats/MrssFormat.php b/formats/MrssFormat.php index 386b7d37..45c2181f 100644 --- a/formats/MrssFormat.php +++ b/formats/MrssFormat.php @@ -1,4 +1,5 @@ <?php + /** * MrssFormat - RSS 2.0 + Media RSS * http://www.rssboard.org/rss-specification @@ -24,146 +25,149 @@ * - Since the Media RSS extension has its own namespace, the output is a valid * RSS 2.0 feed that works with feed readers that don't support the extension. */ -class MrssFormat extends FormatAbstract { - const MIME_TYPE = 'application/rss+xml'; - - protected const ATOM_NS = 'http://www.w3.org/2005/Atom'; - protected const MRSS_NS = 'http://search.yahoo.com/mrss/'; - - const ALLOWED_IMAGE_EXT = array( - '.gif', '.jpg', '.png' - ); - - public function stringify(){ - $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://'; - $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : ''; - $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : ''; - $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : ''; - - $feedUrl = $urlPrefix . $urlHost . $urlRequest; - - $extraInfos = $this->getExtraInfos(); - $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY; - - $document = new DomDocument('1.0', $this->getCharset()); - $document->formatOutput = true; - $feed = $document->createElement('rss'); - $document->appendChild($feed); - $feed->setAttribute('version', '2.0'); - $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:atom', self::ATOM_NS); - $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS); - - $channel = $document->createElement('channel'); - $feed->appendChild($channel); - - $title = $extraInfos['name']; - $channelTitle = $document->createElement('title'); - $channel->appendChild($channelTitle); - $channelTitle->appendChild($document->createTextNode($title)); - - $link = $document->createElement('link'); - $channel->appendChild($link); - $link->appendChild($document->createTextNode($uri)); - - $description = $document->createElement('description'); - $channel->appendChild($description); - $description->appendChild($document->createTextNode($extraInfos['name'])); - - $icon = $extraInfos['icon']; - if (!empty($icon) && in_array(substr($icon, -4), self::ALLOWED_IMAGE_EXT)) { - $feedImage = $document->createElement('image'); - $channel->appendChild($feedImage); - $iconUrl = $document->createElement('url'); - $iconUrl->appendChild($document->createTextNode($icon)); - $feedImage->appendChild($iconUrl); - $iconTitle = $document->createElement('title'); - $iconTitle->appendChild($document->createTextNode($title)); - $feedImage->appendChild($iconTitle); - $iconLink = $document->createElement('link'); - $iconLink->appendChild($document->createTextNode($uri)); - $feedImage->appendChild($iconLink); - } - - $linkAlternate = $document->createElementNS(self::ATOM_NS, 'link'); - $channel->appendChild($linkAlternate); - $linkAlternate->setAttribute('rel', 'alternate'); - $linkAlternate->setAttribute('type', 'text/html'); - $linkAlternate->setAttribute('href', $uri); - - $linkSelf = $document->createElementNS(self::ATOM_NS, 'link'); - $channel->appendChild($linkSelf); - $linkSelf->setAttribute('rel', 'self'); - $linkSelf->setAttribute('type', 'application/atom+xml'); - $linkSelf->setAttribute('href', $feedUrl); - - foreach($this->getItems() as $item) { - $itemTimestamp = $item->getTimestamp(); - $itemTitle = $item->getTitle(); - $itemUri = $item->getURI(); - $itemContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : ''; - $entryID = $item->getUid(); - $isPermaLink = 'false'; - - if (empty($entryID) && !empty($itemUri)) { // Fallback to provided URI - $entryID = $itemUri; - $isPermaLink = 'true'; - } - - if (empty($entryID)) // Fallback to title and content - $entryID = hash('sha1', $itemTitle . $itemContent); - - $entry = $document->createElement('item'); - $channel->appendChild($entry); - - if (!empty($itemTitle)) { - $entryTitle = $document->createElement('title'); - $entry->appendChild($entryTitle); - $entryTitle->appendChild($document->createTextNode($itemTitle)); - } - - if (!empty($itemUri)) { - $entryLink = $document->createElement('link'); - $entry->appendChild($entryLink); - $entryLink->appendChild($document->createTextNode($itemUri)); - } - - $entryGuid = $document->createElement('guid'); - $entryGuid->setAttribute('isPermaLink', $isPermaLink); - $entry->appendChild($entryGuid); - $entryGuid->appendChild($document->createTextNode($entryID)); - - if (!empty($itemTimestamp)) { - $entryPublished = $document->createElement('pubDate'); - $entry->appendChild($entryPublished); - $entryPublished->appendChild($document->createTextNode(gmdate(DATE_RFC2822, $itemTimestamp))); - } - - if (!empty($itemContent)) { - $entryDescription = $document->createElement('description'); - $entry->appendChild($entryDescription); - $entryDescription->appendChild($document->createTextNode($itemContent)); - } - - foreach($item->getEnclosures() as $enclosure) { - $entryEnclosure = $document->createElementNS(self::MRSS_NS, 'content'); - $entry->appendChild($entryEnclosure); - $entryEnclosure->setAttribute('url', $enclosure); - $entryEnclosure->setAttribute('type', getMimeType($enclosure)); - } - - $entryCategories = ''; - foreach($item->getCategories() as $category) { - $entryCategory = $document->createElement('category'); - $entry->appendChild($entryCategory); - $entryCategory->appendChild($document->createTextNode($category)); - } - } - - $toReturn = $document->saveXML(); - - // Remove invalid non-UTF8 characters - ini_set('mbstring.substitute_character', 'none'); - $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8'); - return $toReturn; - } +class MrssFormat extends FormatAbstract +{ + const MIME_TYPE = 'application/rss+xml'; + + protected const ATOM_NS = 'http://www.w3.org/2005/Atom'; + protected const MRSS_NS = 'http://search.yahoo.com/mrss/'; + + const ALLOWED_IMAGE_EXT = [ + '.gif', '.jpg', '.png' + ]; + + public function stringify() + { + $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://'; + $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : ''; + $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : ''; + $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : ''; + + $feedUrl = $urlPrefix . $urlHost . $urlRequest; + + $extraInfos = $this->getExtraInfos(); + $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY; + + $document = new DomDocument('1.0', $this->getCharset()); + $document->formatOutput = true; + $feed = $document->createElement('rss'); + $document->appendChild($feed); + $feed->setAttribute('version', '2.0'); + $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:atom', self::ATOM_NS); + $feed->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:media', self::MRSS_NS); + + $channel = $document->createElement('channel'); + $feed->appendChild($channel); + + $title = $extraInfos['name']; + $channelTitle = $document->createElement('title'); + $channel->appendChild($channelTitle); + $channelTitle->appendChild($document->createTextNode($title)); + + $link = $document->createElement('link'); + $channel->appendChild($link); + $link->appendChild($document->createTextNode($uri)); + + $description = $document->createElement('description'); + $channel->appendChild($description); + $description->appendChild($document->createTextNode($extraInfos['name'])); + + $icon = $extraInfos['icon']; + if (!empty($icon) && in_array(substr($icon, -4), self::ALLOWED_IMAGE_EXT)) { + $feedImage = $document->createElement('image'); + $channel->appendChild($feedImage); + $iconUrl = $document->createElement('url'); + $iconUrl->appendChild($document->createTextNode($icon)); + $feedImage->appendChild($iconUrl); + $iconTitle = $document->createElement('title'); + $iconTitle->appendChild($document->createTextNode($title)); + $feedImage->appendChild($iconTitle); + $iconLink = $document->createElement('link'); + $iconLink->appendChild($document->createTextNode($uri)); + $feedImage->appendChild($iconLink); + } + + $linkAlternate = $document->createElementNS(self::ATOM_NS, 'link'); + $channel->appendChild($linkAlternate); + $linkAlternate->setAttribute('rel', 'alternate'); + $linkAlternate->setAttribute('type', 'text/html'); + $linkAlternate->setAttribute('href', $uri); + + $linkSelf = $document->createElementNS(self::ATOM_NS, 'link'); + $channel->appendChild($linkSelf); + $linkSelf->setAttribute('rel', 'self'); + $linkSelf->setAttribute('type', 'application/atom+xml'); + $linkSelf->setAttribute('href', $feedUrl); + + foreach ($this->getItems() as $item) { + $itemTimestamp = $item->getTimestamp(); + $itemTitle = $item->getTitle(); + $itemUri = $item->getURI(); + $itemContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : ''; + $entryID = $item->getUid(); + $isPermaLink = 'false'; + + if (empty($entryID) && !empty($itemUri)) { // Fallback to provided URI + $entryID = $itemUri; + $isPermaLink = 'true'; + } + + if (empty($entryID)) { // Fallback to title and content + $entryID = hash('sha1', $itemTitle . $itemContent); + } + + $entry = $document->createElement('item'); + $channel->appendChild($entry); + + if (!empty($itemTitle)) { + $entryTitle = $document->createElement('title'); + $entry->appendChild($entryTitle); + $entryTitle->appendChild($document->createTextNode($itemTitle)); + } + + if (!empty($itemUri)) { + $entryLink = $document->createElement('link'); + $entry->appendChild($entryLink); + $entryLink->appendChild($document->createTextNode($itemUri)); + } + + $entryGuid = $document->createElement('guid'); + $entryGuid->setAttribute('isPermaLink', $isPermaLink); + $entry->appendChild($entryGuid); + $entryGuid->appendChild($document->createTextNode($entryID)); + + if (!empty($itemTimestamp)) { + $entryPublished = $document->createElement('pubDate'); + $entry->appendChild($entryPublished); + $entryPublished->appendChild($document->createTextNode(gmdate(DATE_RFC2822, $itemTimestamp))); + } + + if (!empty($itemContent)) { + $entryDescription = $document->createElement('description'); + $entry->appendChild($entryDescription); + $entryDescription->appendChild($document->createTextNode($itemContent)); + } + + foreach ($item->getEnclosures() as $enclosure) { + $entryEnclosure = $document->createElementNS(self::MRSS_NS, 'content'); + $entry->appendChild($entryEnclosure); + $entryEnclosure->setAttribute('url', $enclosure); + $entryEnclosure->setAttribute('type', getMimeType($enclosure)); + } + + $entryCategories = ''; + foreach ($item->getCategories() as $category) { + $entryCategory = $document->createElement('category'); + $entry->appendChild($entryCategory); + $entryCategory->appendChild($document->createTextNode($category)); + } + } + + $toReturn = $document->saveXML(); + + // Remove invalid non-UTF8 characters + ini_set('mbstring.substitute_character', 'none'); + $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8'); + return $toReturn; + } } diff --git a/formats/PlaintextFormat.php b/formats/PlaintextFormat.php index a1ef9e7f..a1e125c7 100644 --- a/formats/PlaintextFormat.php +++ b/formats/PlaintextFormat.php @@ -1,24 +1,27 @@ <?php + /** * Plaintext * Returns $this->items as raw php data. */ -class PlaintextFormat extends FormatAbstract { - const MIME_TYPE = 'text/plain'; +class PlaintextFormat extends FormatAbstract +{ + const MIME_TYPE = 'text/plain'; - public function stringify(){ - $items = $this->getItems(); - $data = array(); + public function stringify() + { + $items = $this->getItems(); + $data = []; - foreach($items as $item) { - $data[] = $item->toArray(); - } + foreach ($items as $item) { + $data[] = $item->toArray(); + } - $toReturn = print_r($data, true); + $toReturn = print_r($data, true); - // Remove invalid non-UTF8 characters - ini_set('mbstring.substitute_character', 'none'); - $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8'); - return $toReturn; - } + // Remove invalid non-UTF8 characters + ini_set('mbstring.substitute_character', 'none'); + $toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8'); + return $toReturn; + } } @@ -7,32 +7,32 @@ Move the CLI arguments to the $_GET array, in order to be able to use rss-bridge from the command line */ if (isset($argv)) { - parse_str(implode('&', array_slice($argv, 1)), $cliArgs); - $params = array_merge($_GET, $cliArgs); + parse_str(implode('&', array_slice($argv, 1)), $cliArgs); + $params = array_merge($_GET, $cliArgs); } else { - $params = $_GET; + $params = $_GET; } try { - $actionFac = new ActionFactory(); + $actionFac = new ActionFactory(); - if (array_key_exists('action', $params)) { - $action = $actionFac->create($params['action']); - $action->userData = $params; - $action->execute(); - } else { - $showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN); - echo BridgeList::create($showInactive); - } + if (array_key_exists('action', $params)) { + $action = $actionFac->create($params['action']); + $action->userData = $params; + $action->execute(); + } else { + $showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN); + echo BridgeList::create($showInactive); + } } catch (\Throwable $e) { - error_log($e); + error_log($e); - $code = $e->getCode(); - if ($code !== -1) { - header('Content-Type: text/plain', true, $code); - } + $code = $e->getCode(); + if ($code !== -1) { + header('Content-Type: text/plain', true, $code); + } - $message = sprintf("Uncaught Exception %s: '%s'\n", get_class($e), $e->getMessage()); + $message = sprintf("Uncaught Exception %s: '%s'\n", get_class($e), $e->getMessage()); - print $message; + print $message; } diff --git a/lib/ActionFactory.php b/lib/ActionFactory.php index bd1297b4..5a413767 100644 --- a/lib/ActionFactory.php +++ b/lib/ActionFactory.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,31 +7,31 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ class ActionFactory { - private $folder; + private $folder; - public function __construct(string $folder = PATH_LIB_ACTIONS) - { - $this->folder = $folder; - } + public function __construct(string $folder = PATH_LIB_ACTIONS) + { + $this->folder = $folder; + } - /** - * @param string $name The name of the action e.g. "Display", "List", or "Connectivity" - */ - public function create(string $name): ActionInterface - { - $name = ucfirst(strtolower($name)) . 'Action'; - $filePath = $this->folder . $name . '.php'; - if(!file_exists($filePath)) { - throw new \Exception('Invalid action'); - } - $className = '\\' . $name; - return new $className(); - } + /** + * @param string $name The name of the action e.g. "Display", "List", or "Connectivity" + */ + public function create(string $name): ActionInterface + { + $name = ucfirst(strtolower($name)) . 'Action'; + $filePath = $this->folder . $name . '.php'; + if (!file_exists($filePath)) { + throw new \Exception('Invalid action'); + } + $className = '\\' . $name; + return new $className(); + } } diff --git a/lib/ActionInterface.php b/lib/ActionInterface.php index c8684d52..78284ab4 100644 --- a/lib/ActionInterface.php +++ b/lib/ActionInterface.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,21 +7,22 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** * Interface for action objects. */ -interface ActionInterface { - /** - * Execute the action. - * - * Note: This function directly outputs data to the user. - * - * @return void - */ - public function execute(); +interface ActionInterface +{ + /** + * Execute the action. + * + * Note: This function directly outputs data to the user. + * + * @return void + */ + public function execute(); } diff --git a/lib/Authentication.php b/lib/Authentication.php index ac8ea96a..1ae26edf 100644 --- a/lib/Authentication.php +++ b/lib/Authentication.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -30,56 +31,57 @@ * @todo Add functions to detect if a user is authenticated or not. This can be * utilized for limiting access to authorized users only. */ -class Authentication { - /** - * Throw an exception when trying to create a new instance of this class. - * Use {@see Authentication::showPromptIfNeeded()} instead! - * - * @throws \LogicException if called. - */ - public function __construct(){ - throw new \LogicException('Use ' . __CLASS__ . '::showPromptIfNeeded()!'); - } - - /** - * Requests the user for login credentials if necessary. - * - * Responds to an authentication request or returns the `WWW-Authenticate` - * header if authentication is enabled in the configuration of RSS-Bridge - * (`[authentication] enable = true`). - * - * @return void - */ - public static function showPromptIfNeeded() { - - if(Configuration::getConfig('authentication', 'enable') === true) { - if(!Authentication::verifyPrompt()) { - header('WWW-Authenticate: Basic realm="RSS-Bridge"', true, 401); - die('Please authenticate in order to access this instance !'); - } - - } - - } - - /** - * Verifies if an authentication request was received and compares the - * provided username and password to the configuration of RSS-Bridge - * (`[authentication] username` and `[authentication] password`). - * - * @return bool True if authentication succeeded. - */ - public static function verifyPrompt() { +class Authentication +{ + /** + * Throw an exception when trying to create a new instance of this class. + * Use {@see Authentication::showPromptIfNeeded()} instead! + * + * @throws \LogicException if called. + */ + public function __construct() + { + throw new \LogicException('Use ' . __CLASS__ . '::showPromptIfNeeded()!'); + } - if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { - if(Configuration::getConfig('authentication', 'username') === $_SERVER['PHP_AUTH_USER'] - && Configuration::getConfig('authentication', 'password') === $_SERVER['PHP_AUTH_PW']) { - return true; - } else { - error_log('[RSS-Bridge] Failed authentication attempt from ' . $_SERVER['REMOTE_ADDR']); - } - } - return false; + /** + * Requests the user for login credentials if necessary. + * + * Responds to an authentication request or returns the `WWW-Authenticate` + * header if authentication is enabled in the configuration of RSS-Bridge + * (`[authentication] enable = true`). + * + * @return void + */ + public static function showPromptIfNeeded() + { + if (Configuration::getConfig('authentication', 'enable') === true) { + if (!Authentication::verifyPrompt()) { + header('WWW-Authenticate: Basic realm="RSS-Bridge"', true, 401); + die('Please authenticate in order to access this instance !'); + } + } + } - } + /** + * Verifies if an authentication request was received and compares the + * provided username and password to the configuration of RSS-Bridge + * (`[authentication] username` and `[authentication] password`). + * + * @return bool True if authentication succeeded. + */ + public static function verifyPrompt() + { + if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { + if ( + Configuration::getConfig('authentication', 'username') === $_SERVER['PHP_AUTH_USER'] + && Configuration::getConfig('authentication', 'password') === $_SERVER['PHP_AUTH_PW'] + ) { + return true; + } else { + error_log('[RSS-Bridge] Failed authentication attempt from ' . $_SERVER['REMOTE_ADDR']); + } + } + return false; + } } diff --git a/lib/BridgeAbstract.php b/lib/BridgeAbstract.php index 38e3da03..c479f53e 100644 --- a/lib/BridgeAbstract.php +++ b/lib/BridgeAbstract.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -24,393 +25,410 @@ * @todo Add specification for PARAMETERS () * @todo Add specification for $items */ -abstract class BridgeAbstract implements BridgeInterface { - - /** - * Name of the bridge - * - * Use {@see BridgeAbstract::getName()} to read this parameter - */ - const NAME = 'Unnamed bridge'; - - /** - * URI to the site the bridge is intended to be used for. - * - * Use {@see BridgeAbstract::getURI()} to read this parameter - */ - const URI = ''; - - /** - * Donation URI to the site the bridge is intended to be used for. - * - * Use {@see BridgeAbstract::getDonationURI()} to read this parameter - */ - const DONATION_URI = ''; - - /** - * A brief description of what the bridge can do - * - * Use {@see BridgeAbstract::getDescription()} to read this parameter - */ - const DESCRIPTION = 'No description provided'; - - /** - * The name of the maintainer. Multiple maintainers can be separated by comma - * - * Use {@see BridgeAbstract::getMaintainer()} to read this parameter - */ - const MAINTAINER = 'No maintainer'; - - /** - * The default cache timeout for the bridge - * - * Use {@see BridgeAbstract::getCacheTimeout()} to read this parameter - */ - const CACHE_TIMEOUT = 3600; - - /** - * Configuration for the bridge - * - * Use {@see BridgeAbstract::getConfiguration()} to read this parameter - */ - const CONFIGURATION = array(); - - /** - * Parameters for the bridge - * - * Use {@see BridgeAbstract::getParameters()} to read this parameter - */ - const PARAMETERS = array(); - - /** - * Test cases for detectParameters for the bridge - */ - const TEST_DETECT_PARAMETERS = array(); - - /** - * This is a convenient const for the limit option in bridge contexts. - * Can be inlined and modified if necessary. - */ - protected const LIMIT = [ - 'name' => 'Limit', - 'type' => 'number', - 'title' => 'Maximum number of items to return', - ]; - - /** - * Holds the list of items collected by the bridge - * - * Items must be collected by {@see BridgeInterface::collectData()} - * - * Use {@see BridgeAbstract::getItems()} to access items. - * - * @var array - */ - protected $items = array(); - - /** - * Holds the list of input parameters used by the bridge - * - * Do not access this parameter directly! - * Use {@see BridgeAbstract::setInputs()} and {@see BridgeAbstract::getInput()} instead! - * - * @var array - */ - protected $inputs = array(); - - /** - * Holds the name of the queried context - * - * @var string - */ - protected $queriedContext = ''; - - /** {@inheritdoc} */ - public function getItems(){ - return $this->items; - } - - /** - * Sets the input values for a given context. - * - * @param array $inputs Associative array of inputs - * @param string $queriedContext The context name - * @return void - */ - protected function setInputs(array $inputs, $queriedContext){ - // Import and assign all inputs to their context - foreach($inputs as $name => $value) { - foreach(static::PARAMETERS as $context => $set) { - if(array_key_exists($name, static::PARAMETERS[$context])) { - $this->inputs[$context][$name]['value'] = $value; - } - } - } - - // Apply default values to missing data - $contexts = array($queriedContext); - if(array_key_exists('global', static::PARAMETERS)) { - $contexts[] = 'global'; - } - - foreach($contexts as $context) { - foreach(static::PARAMETERS[$context] as $name => $properties) { - if(isset($this->inputs[$context][$name]['value'])) { - continue; - } - - $type = isset($properties['type']) ? $properties['type'] : 'text'; - - switch($type) { - case 'checkbox': - if(!isset($properties['defaultValue'])) { - $this->inputs[$context][$name]['value'] = false; - } else { - $this->inputs[$context][$name]['value'] = $properties['defaultValue']; - } - break; - case 'list': - if(!isset($properties['defaultValue'])) { - $firstItem = reset($properties['values']); - if(is_array($firstItem)) { - $firstItem = reset($firstItem); - } - $this->inputs[$context][$name]['value'] = $firstItem; - } else { - $this->inputs[$context][$name]['value'] = $properties['defaultValue']; - } - break; - default: - if(isset($properties['defaultValue'])) { - $this->inputs[$context][$name]['value'] = $properties['defaultValue']; - } - break; - } - } - } - - // Copy global parameter values to the guessed context - if(array_key_exists('global', static::PARAMETERS)) { - foreach(static::PARAMETERS['global'] as $name => $properties) { - if(isset($inputs[$name])) { - $value = $inputs[$name]; - } elseif(isset($properties['defaultValue'])) { - $value = $properties['defaultValue']; - } else { - continue; - } - $this->inputs[$queriedContext][$name]['value'] = $value; - } - } - - // Only keep guessed context parameters values - if(isset($this->inputs[$queriedContext])) { - $this->inputs = array($queriedContext => $this->inputs[$queriedContext]); - } else { - $this->inputs = array(); - } - } - - /** - * Set inputs for the bridge - * - * Returns errors and aborts execution if the provided input parameters are - * invalid. - * - * @param array List of input parameters. Each element in this list must - * relate to an item in {@see BridgeAbstract::PARAMETERS} - * @return void - */ - public function setDatas(array $inputs){ - - if(isset($inputs['context'])) { // Context hinting (optional) - $this->queriedContext = $inputs['context']; - unset($inputs['context']); - } - - if(empty(static::PARAMETERS)) { - - if(!empty($inputs)) { - returnClientError('Invalid parameters value(s)'); - } - - return; - - } - - $validator = new ParameterValidator(); - - if(!$validator->validateData($inputs, static::PARAMETERS)) { - $parameters = array_map( - function($i){ return $i['name']; }, // Just display parameter names - $validator->getInvalidParameters() - ); - - returnClientError( - 'Invalid parameters value(s): ' - . implode(', ', $parameters) - ); - } - - // Guess the context from input data - if(empty($this->queriedContext)) { - $this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS); - } - - if(is_null($this->queriedContext)) { - returnClientError('Required parameter(s) missing'); - } elseif($this->queriedContext === false) { - returnClientError('Mixed context parameters'); - } - - $this->setInputs($inputs, $this->queriedContext); - - } - - /** - * Loads configuration for the bridge - * - * Returns errors and aborts execution if the provided configuration is - * invalid. - * - * @return void - */ - public function loadConfiguration() { - foreach(static::CONFIGURATION as $optionName => $optionValue) { - - $configurationOption = Configuration::getConfig(get_class($this), $optionName); - - if($configurationOption !== null) { - $this->configuration[$optionName] = $configurationOption; - continue; - } - - if(isset($optionValue['required']) && $optionValue['required'] === true) { - returnServerError( - 'Missing configuration option: ' - . $optionName - ); - } elseif(isset($optionValue['defaultValue'])) { - $this->configuration[$optionName] = $optionValue['defaultValue']; - } - - } - } - - /** - * Returns the value for the provided input - * - * @param string $input The input name - * @return mixed|null The input value or null if the input is not defined - */ - protected function getInput($input){ - if(!isset($this->inputs[$this->queriedContext][$input]['value'])) { - return null; - } - return $this->inputs[$this->queriedContext][$input]['value']; - } - - /** - * Returns the value for the selected configuration - * - * @param string $input The option name - * @return mixed|null The option value or null if the input is not defined - */ - public function getOption($name){ - if(!isset($this->configuration[$name])) { - return null; - } - return $this->configuration[$name]; - } - - /** {@inheritdoc} */ - public function getDescription(){ - return static::DESCRIPTION; - } - - /** {@inheritdoc} */ - public function getMaintainer(){ - return static::MAINTAINER; - } - - /** {@inheritdoc} */ - public function getName(){ - return static::NAME; - } - - /** {@inheritdoc} */ - public function getIcon(){ - return static::URI . '/favicon.ico'; - } - - /** {@inheritdoc} */ - public function getConfiguration(){ - return static::CONFIGURATION; - } - - /** {@inheritdoc} */ - public function getParameters(){ - return static::PARAMETERS; - } - - /** {@inheritdoc} */ - public function getURI(){ - return static::URI; - } - - /** {@inheritdoc} */ - public function getDonationURI(){ - return static::DONATION_URI; - } - - /** {@inheritdoc} */ - public function getCacheTimeout(){ - return static::CACHE_TIMEOUT; - } - - /** {@inheritdoc} */ - public function detectParameters($url){ - $regex = '/^(https?:\/\/)?(www\.)?(.+?)(\/)?$/'; - if(empty(static::PARAMETERS) - && preg_match($regex, $url, $urlMatches) > 0 - && preg_match($regex, static::URI, $bridgeUriMatches) > 0 - && $urlMatches[3] === $bridgeUriMatches[3]) { - return array(); - } else { - return null; - } - } - - /** - * Loads a cached value for the specified key - * - * @param string $key Key name - * @param int $duration Cache duration (optional, default: 24 hours) - * @return mixed Cached value or null if the key doesn't exist or has expired - */ - protected function loadCacheValue($key, $duration = 86400){ - $cacheFac = new CacheFactory(); - - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope(get_called_class()); - $cache->setKey($key); - if($cache->getTime() < time() - $duration) - return null; - return $cache->loadData(); - } - - /** - * Stores a value to cache with the specified key - * - * @param string $key Key name - * @param mixed $value Value to cache - */ - protected function saveCacheValue($key, $value){ - $cacheFac = new CacheFactory(); - - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope(get_called_class()); - $cache->setKey($key); - $cache->saveData($value); - } +abstract class BridgeAbstract implements BridgeInterface +{ + /** + * Name of the bridge + * + * Use {@see BridgeAbstract::getName()} to read this parameter + */ + const NAME = 'Unnamed bridge'; + + /** + * URI to the site the bridge is intended to be used for. + * + * Use {@see BridgeAbstract::getURI()} to read this parameter + */ + const URI = ''; + + /** + * Donation URI to the site the bridge is intended to be used for. + * + * Use {@see BridgeAbstract::getDonationURI()} to read this parameter + */ + const DONATION_URI = ''; + + /** + * A brief description of what the bridge can do + * + * Use {@see BridgeAbstract::getDescription()} to read this parameter + */ + const DESCRIPTION = 'No description provided'; + + /** + * The name of the maintainer. Multiple maintainers can be separated by comma + * + * Use {@see BridgeAbstract::getMaintainer()} to read this parameter + */ + const MAINTAINER = 'No maintainer'; + + /** + * The default cache timeout for the bridge + * + * Use {@see BridgeAbstract::getCacheTimeout()} to read this parameter + */ + const CACHE_TIMEOUT = 3600; + + /** + * Configuration for the bridge + * + * Use {@see BridgeAbstract::getConfiguration()} to read this parameter + */ + const CONFIGURATION = []; + + /** + * Parameters for the bridge + * + * Use {@see BridgeAbstract::getParameters()} to read this parameter + */ + const PARAMETERS = []; + + /** + * Test cases for detectParameters for the bridge + */ + const TEST_DETECT_PARAMETERS = []; + + /** + * This is a convenient const for the limit option in bridge contexts. + * Can be inlined and modified if necessary. + */ + protected const LIMIT = [ + 'name' => 'Limit', + 'type' => 'number', + 'title' => 'Maximum number of items to return', + ]; + + /** + * Holds the list of items collected by the bridge + * + * Items must be collected by {@see BridgeInterface::collectData()} + * + * Use {@see BridgeAbstract::getItems()} to access items. + * + * @var array + */ + protected $items = []; + + /** + * Holds the list of input parameters used by the bridge + * + * Do not access this parameter directly! + * Use {@see BridgeAbstract::setInputs()} and {@see BridgeAbstract::getInput()} instead! + * + * @var array + */ + protected $inputs = []; + + /** + * Holds the name of the queried context + * + * @var string + */ + protected $queriedContext = ''; + + /** {@inheritdoc} */ + public function getItems() + { + return $this->items; + } + + /** + * Sets the input values for a given context. + * + * @param array $inputs Associative array of inputs + * @param string $queriedContext The context name + * @return void + */ + protected function setInputs(array $inputs, $queriedContext) + { + // Import and assign all inputs to their context + foreach ($inputs as $name => $value) { + foreach (static::PARAMETERS as $context => $set) { + if (array_key_exists($name, static::PARAMETERS[$context])) { + $this->inputs[$context][$name]['value'] = $value; + } + } + } + + // Apply default values to missing data + $contexts = [$queriedContext]; + if (array_key_exists('global', static::PARAMETERS)) { + $contexts[] = 'global'; + } + + foreach ($contexts as $context) { + foreach (static::PARAMETERS[$context] as $name => $properties) { + if (isset($this->inputs[$context][$name]['value'])) { + continue; + } + + $type = isset($properties['type']) ? $properties['type'] : 'text'; + + switch ($type) { + case 'checkbox': + if (!isset($properties['defaultValue'])) { + $this->inputs[$context][$name]['value'] = false; + } else { + $this->inputs[$context][$name]['value'] = $properties['defaultValue']; + } + break; + case 'list': + if (!isset($properties['defaultValue'])) { + $firstItem = reset($properties['values']); + if (is_array($firstItem)) { + $firstItem = reset($firstItem); + } + $this->inputs[$context][$name]['value'] = $firstItem; + } else { + $this->inputs[$context][$name]['value'] = $properties['defaultValue']; + } + break; + default: + if (isset($properties['defaultValue'])) { + $this->inputs[$context][$name]['value'] = $properties['defaultValue']; + } + break; + } + } + } + + // Copy global parameter values to the guessed context + if (array_key_exists('global', static::PARAMETERS)) { + foreach (static::PARAMETERS['global'] as $name => $properties) { + if (isset($inputs[$name])) { + $value = $inputs[$name]; + } elseif (isset($properties['defaultValue'])) { + $value = $properties['defaultValue']; + } else { + continue; + } + $this->inputs[$queriedContext][$name]['value'] = $value; + } + } + + // Only keep guessed context parameters values + if (isset($this->inputs[$queriedContext])) { + $this->inputs = [$queriedContext => $this->inputs[$queriedContext]]; + } else { + $this->inputs = []; + } + } + + /** + * Set inputs for the bridge + * + * Returns errors and aborts execution if the provided input parameters are + * invalid. + * + * @param array List of input parameters. Each element in this list must + * relate to an item in {@see BridgeAbstract::PARAMETERS} + * @return void + */ + public function setDatas(array $inputs) + { + if (isset($inputs['context'])) { // Context hinting (optional) + $this->queriedContext = $inputs['context']; + unset($inputs['context']); + } + + if (empty(static::PARAMETERS)) { + if (!empty($inputs)) { + returnClientError('Invalid parameters value(s)'); + } + + return; + } + + $validator = new ParameterValidator(); + + if (!$validator->validateData($inputs, static::PARAMETERS)) { + $parameters = array_map( + function ($i) { + return $i['name']; + }, // Just display parameter names + $validator->getInvalidParameters() + ); + + returnClientError( + 'Invalid parameters value(s): ' + . implode(', ', $parameters) + ); + } + + // Guess the context from input data + if (empty($this->queriedContext)) { + $this->queriedContext = $validator->getQueriedContext($inputs, static::PARAMETERS); + } + + if (is_null($this->queriedContext)) { + returnClientError('Required parameter(s) missing'); + } elseif ($this->queriedContext === false) { + returnClientError('Mixed context parameters'); + } + + $this->setInputs($inputs, $this->queriedContext); + } + + /** + * Loads configuration for the bridge + * + * Returns errors and aborts execution if the provided configuration is + * invalid. + * + * @return void + */ + public function loadConfiguration() + { + foreach (static::CONFIGURATION as $optionName => $optionValue) { + $configurationOption = Configuration::getConfig(get_class($this), $optionName); + + if ($configurationOption !== null) { + $this->configuration[$optionName] = $configurationOption; + continue; + } + + if (isset($optionValue['required']) && $optionValue['required'] === true) { + returnServerError( + 'Missing configuration option: ' + . $optionName + ); + } elseif (isset($optionValue['defaultValue'])) { + $this->configuration[$optionName] = $optionValue['defaultValue']; + } + } + } + + /** + * Returns the value for the provided input + * + * @param string $input The input name + * @return mixed|null The input value or null if the input is not defined + */ + protected function getInput($input) + { + if (!isset($this->inputs[$this->queriedContext][$input]['value'])) { + return null; + } + return $this->inputs[$this->queriedContext][$input]['value']; + } + + /** + * Returns the value for the selected configuration + * + * @param string $input The option name + * @return mixed|null The option value or null if the input is not defined + */ + public function getOption($name) + { + if (!isset($this->configuration[$name])) { + return null; + } + return $this->configuration[$name]; + } + + /** {@inheritdoc} */ + public function getDescription() + { + return static::DESCRIPTION; + } + + /** {@inheritdoc} */ + public function getMaintainer() + { + return static::MAINTAINER; + } + + /** {@inheritdoc} */ + public function getName() + { + return static::NAME; + } + + /** {@inheritdoc} */ + public function getIcon() + { + return static::URI . '/favicon.ico'; + } + + /** {@inheritdoc} */ + public function getConfiguration() + { + return static::CONFIGURATION; + } + + /** {@inheritdoc} */ + public function getParameters() + { + return static::PARAMETERS; + } + + /** {@inheritdoc} */ + public function getURI() + { + return static::URI; + } + + /** {@inheritdoc} */ + public function getDonationURI() + { + return static::DONATION_URI; + } + + /** {@inheritdoc} */ + public function getCacheTimeout() + { + return static::CACHE_TIMEOUT; + } + + /** {@inheritdoc} */ + public function detectParameters($url) + { + $regex = '/^(https?:\/\/)?(www\.)?(.+?)(\/)?$/'; + if ( + empty(static::PARAMETERS) + && preg_match($regex, $url, $urlMatches) > 0 + && preg_match($regex, static::URI, $bridgeUriMatches) > 0 + && $urlMatches[3] === $bridgeUriMatches[3] + ) { + return []; + } else { + return null; + } + } + + /** + * Loads a cached value for the specified key + * + * @param string $key Key name + * @param int $duration Cache duration (optional, default: 24 hours) + * @return mixed Cached value or null if the key doesn't exist or has expired + */ + protected function loadCacheValue($key, $duration = 86400) + { + $cacheFac = new CacheFactory(); + + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope(get_called_class()); + $cache->setKey($key); + if ($cache->getTime() < time() - $duration) { + return null; + } + return $cache->loadData(); + } + + /** + * Stores a value to cache with the specified key + * + * @param string $key Key name + * @param mixed $value Value to cache + */ + protected function saveCacheValue($key, $value) + { + $cacheFac = new CacheFactory(); + + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope(get_called_class()); + $cache->setKey($key); + $cache->saveData($value); + } } diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php index 22520170..78132776 100644 --- a/lib/BridgeCard.php +++ b/lib/BridgeCard.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -19,310 +20,326 @@ * * @todo Return error if a caller creates an object of this class. */ -final class BridgeCard { - /** - * Get the form header for a bridge card - * - * @param string $bridgeName The bridge name - * @param bool $isHttps If disabled, adds a warning to the form - * @return string The form header - */ - private static function getFormHeader($bridgeName, $isHttps = false, $parameterName = '') { - $form = <<<EOD +final class BridgeCard +{ + /** + * Get the form header for a bridge card + * + * @param string $bridgeName The bridge name + * @param bool $isHttps If disabled, adds a warning to the form + * @return string The form header + */ + private static function getFormHeader($bridgeName, $isHttps = false, $parameterName = '') + { + $form = <<<EOD <form method="GET" action="?"> <input type="hidden" name="action" value="display" /> <input type="hidden" name="bridge" value="{$bridgeName}" /> EOD; - if(!empty($parameterName)) { - $form .= <<<EOD + if (!empty($parameterName)) { + $form .= <<<EOD <input type="hidden" name="context" value="{$parameterName}" /> EOD; - } + } - if(!$isHttps) { - $form .= '<div class="secure-warning">Warning : + if (!$isHttps) { + $form .= '<div class="secure-warning">Warning : This bridge is not fetching its content through a secure connection</div>'; - } - - return $form; - } - - /** - * Get the form body for a bridge - * - * @param string $bridgeName The bridge name - * @param array $formats A list of supported formats - * @param bool $isActive Indicates if a bridge is enabled or not - * @param bool $isHttps Indicates if a bridge uses HTTPS or not - * @param string $parameterName Sets the bridge context for the current form - * @param array $parameters The bridge parameters - * @return string The form body - */ - private static function getForm($bridgeName, - $formats, - $isActive = false, - $isHttps = false, - $parameterName = '', - $parameters = array()) { - $form = self::getFormHeader($bridgeName, $isHttps, $parameterName); - - if(count($parameters) > 0) { - - $form .= '<div class="parameters">'; - - foreach($parameters as $id => $inputEntry) { - if(!isset($inputEntry['exampleValue'])) - $inputEntry['exampleValue'] = ''; - - if(!isset($inputEntry['defaultValue'])) - $inputEntry['defaultValue'] = ''; - - $idArg = 'arg-' - . urlencode($bridgeName) - . '-' - . urlencode($parameterName) - . '-' - . urlencode($id); - - $form .= '<label for="' - . $idArg - . '">' - . filter_var($inputEntry['name'], FILTER_SANITIZE_FULL_SPECIAL_CHARS) - . '</label>' - . PHP_EOL; - - if(!isset($inputEntry['type']) || $inputEntry['type'] === 'text') { - $form .= self::getTextInput($inputEntry, $idArg, $id); - } elseif($inputEntry['type'] === 'number') { - $form .= self::getNumberInput($inputEntry, $idArg, $id); - } else if($inputEntry['type'] === 'list') { - $form .= self::getListInput($inputEntry, $idArg, $id); - } elseif($inputEntry['type'] === 'checkbox') { - $form .= self::getCheckboxInput($inputEntry, $idArg, $id); - } - - if(isset($inputEntry['title'])) { - $title_filtered = filter_var($inputEntry['title'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $form .= '<i class="info" title="' . $title_filtered . '">i</i>'; - } else { - $form .= '<i class="no-info"></i>'; - } - } - - $form .= '</div>'; - - } - - if($isActive) { - $form .= '<button type="submit" name="format" formtarget="_blank" value="Html">Generate feed</button>'; - } else { - $form .= '<span style="font-weight: bold;">Inactive</span>'; - } - - return $form . '</form>' . PHP_EOL; - } - - /** - * Get input field attributes - * - * @param array $entry The current entry - * @return string The input field attributes - */ - private static function getInputAttributes($entry) { - $retVal = ''; - - if(isset($entry['required']) && $entry['required'] === true) - $retVal .= ' required'; - - if(isset($entry['pattern'])) - $retVal .= ' pattern="' . $entry['pattern'] . '"'; - - return $retVal; - } - - /** - * Get text input - * - * @param array $entry The current entry - * @param string $id The field ID - * @param string $name The field name - * @return string The text input field - */ - private static function getTextInput($entry, $id, $name) { - return '<input ' - . self::getInputAttributes($entry) - . ' id="' - . $id - . '" type="text" value="' - . filter_var($entry['defaultValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS) - . '" placeholder="' - . filter_var($entry['exampleValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS) - . '" name="' - . $name - . '" />' - . PHP_EOL; - } - - /** - * Get number input - * - * @param array $entry The current entry - * @param string $id The field ID - * @param string $name The field name - * @return string The number input field - */ - private static function getNumberInput($entry, $id, $name) { - return '<input ' - . self::getInputAttributes($entry) - . ' id="' - . $id - . '" type="number" value="' - . filter_var($entry['defaultValue'], FILTER_SANITIZE_NUMBER_INT) - . '" placeholder="' - . filter_var($entry['exampleValue'], FILTER_SANITIZE_NUMBER_INT) - . '" name="' - . $name - . '" />' - . PHP_EOL; - } - - /** - * Get list input - * - * @param array $entry The current entry - * @param string $id The field ID - * @param string $name The field name - * @return string The list input field - */ - private static function getListInput($entry, $id, $name) { - if(isset($entry['required']) && $entry['required'] === true) { - Debug::log('The "required" attribute is not supported for lists.'); - unset($entry['required']); - } - - $list = '<select ' - . self::getInputAttributes($entry) - . ' id="' - . $id - . '" name="' - . $name - . '" >'; - - foreach($entry['values'] as $name => $value) { - if(is_array($value)) { - $list .= '<optgroup label="' . htmlentities($name) . '">'; - foreach($value as $subname => $subvalue) { - if($entry['defaultValue'] === $subname - || $entry['defaultValue'] === $subvalue) { - $list .= '<option value="' - . $subvalue - . '" selected>' - . $subname - . '</option>'; - } else { - $list .= '<option value="' - . $subvalue - . '">' - . $subname - . '</option>'; - } - } - $list .= '</optgroup>'; - } else { - if($entry['defaultValue'] === $name - || $entry['defaultValue'] === $value) { - $list .= '<option value="' - . $value - . '" selected>' - . $name - . '</option>'; - } else { - $list .= '<option value="' - . $value - . '">' - . $name - . '</option>'; - } - } - } - - $list .= '</select>'; - - return $list; - } - - /** - * Get checkbox input - * - * @param array $entry The current entry - * @param string $id The field ID - * @param string $name The field name - * @return string The checkbox input field - */ - private static function getCheckboxInput($entry, $id, $name) { - if(isset($entry['required']) && $entry['required'] === true) { - Debug::log('The "required" attribute is not supported for checkboxes.'); - unset($entry['required']); - } - - return '<input ' - . self::getInputAttributes($entry) - . ' id="' - . $id - . '" type="checkbox" name="' - . $name - . '" ' - . ($entry['defaultValue'] === 'checked' ? 'checked' : '') - . ' />' - . PHP_EOL; - } - - /** - * Gets a single bridge card - * - * @param string $bridgeName The bridge name - * @param array $formats A list of formats - * @param bool $isActive Indicates if the bridge is active or not - * @return string The bridge card - */ - public static function displayBridgeCard($bridgeName, $formats, $isActive = true){ - - $bridgeFac = new \BridgeFactory(); - - $bridge = $bridgeFac->create($bridgeName); - - if($bridge == false) - return ''; - - $isHttps = strpos($bridge->getURI(), 'https') === 0; - - $uri = $bridge->getURI(); - $name = $bridge->getName(); - $icon = $bridge->getIcon(); - $description = $bridge->getDescription(); - $parameters = $bridge->getParameters(); - $donationUri = $bridge->getDonationURI(); - $maintainer = $bridge->getMaintainer(); - - $donationsAllowed = Configuration::getConfig('admin', 'donations'); - - if(defined('PROXY_URL') && PROXY_BYBRIDGE) { - $parameters['global']['_noproxy'] = array( - 'name' => 'Disable proxy (' . ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL) . ')', - 'type' => 'checkbox' - ); - } - - if(CUSTOM_CACHE_TIMEOUT) { - $parameters['global']['_cache_timeout'] = array( - 'name' => 'Cache timeout in seconds', - 'type' => 'number', - 'defaultValue' => $bridge->getCacheTimeout() - ); - } - - $card = <<<CARD + } + + return $form; + } + + /** + * Get the form body for a bridge + * + * @param string $bridgeName The bridge name + * @param array $formats A list of supported formats + * @param bool $isActive Indicates if a bridge is enabled or not + * @param bool $isHttps Indicates if a bridge uses HTTPS or not + * @param string $parameterName Sets the bridge context for the current form + * @param array $parameters The bridge parameters + * @return string The form body + */ + private static function getForm( + $bridgeName, + $formats, + $isActive = false, + $isHttps = false, + $parameterName = '', + $parameters = [] + ) { + $form = self::getFormHeader($bridgeName, $isHttps, $parameterName); + + if (count($parameters) > 0) { + $form .= '<div class="parameters">'; + + foreach ($parameters as $id => $inputEntry) { + if (!isset($inputEntry['exampleValue'])) { + $inputEntry['exampleValue'] = ''; + } + + if (!isset($inputEntry['defaultValue'])) { + $inputEntry['defaultValue'] = ''; + } + + $idArg = 'arg-' + . urlencode($bridgeName) + . '-' + . urlencode($parameterName) + . '-' + . urlencode($id); + + $form .= '<label for="' + . $idArg + . '">' + . filter_var($inputEntry['name'], FILTER_SANITIZE_FULL_SPECIAL_CHARS) + . '</label>' + . PHP_EOL; + + if (!isset($inputEntry['type']) || $inputEntry['type'] === 'text') { + $form .= self::getTextInput($inputEntry, $idArg, $id); + } elseif ($inputEntry['type'] === 'number') { + $form .= self::getNumberInput($inputEntry, $idArg, $id); + } elseif ($inputEntry['type'] === 'list') { + $form .= self::getListInput($inputEntry, $idArg, $id); + } elseif ($inputEntry['type'] === 'checkbox') { + $form .= self::getCheckboxInput($inputEntry, $idArg, $id); + } + + if (isset($inputEntry['title'])) { + $title_filtered = filter_var($inputEntry['title'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $form .= '<i class="info" title="' . $title_filtered . '">i</i>'; + } else { + $form .= '<i class="no-info"></i>'; + } + } + + $form .= '</div>'; + } + + if ($isActive) { + $form .= '<button type="submit" name="format" formtarget="_blank" value="Html">Generate feed</button>'; + } else { + $form .= '<span style="font-weight: bold;">Inactive</span>'; + } + + return $form . '</form>' . PHP_EOL; + } + + /** + * Get input field attributes + * + * @param array $entry The current entry + * @return string The input field attributes + */ + private static function getInputAttributes($entry) + { + $retVal = ''; + + if (isset($entry['required']) && $entry['required'] === true) { + $retVal .= ' required'; + } + + if (isset($entry['pattern'])) { + $retVal .= ' pattern="' . $entry['pattern'] . '"'; + } + + return $retVal; + } + + /** + * Get text input + * + * @param array $entry The current entry + * @param string $id The field ID + * @param string $name The field name + * @return string The text input field + */ + private static function getTextInput($entry, $id, $name) + { + return '<input ' + . self::getInputAttributes($entry) + . ' id="' + . $id + . '" type="text" value="' + . filter_var($entry['defaultValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS) + . '" placeholder="' + . filter_var($entry['exampleValue'], FILTER_SANITIZE_FULL_SPECIAL_CHARS) + . '" name="' + . $name + . '" />' + . PHP_EOL; + } + + /** + * Get number input + * + * @param array $entry The current entry + * @param string $id The field ID + * @param string $name The field name + * @return string The number input field + */ + private static function getNumberInput($entry, $id, $name) + { + return '<input ' + . self::getInputAttributes($entry) + . ' id="' + . $id + . '" type="number" value="' + . filter_var($entry['defaultValue'], FILTER_SANITIZE_NUMBER_INT) + . '" placeholder="' + . filter_var($entry['exampleValue'], FILTER_SANITIZE_NUMBER_INT) + . '" name="' + . $name + . '" />' + . PHP_EOL; + } + + /** + * Get list input + * + * @param array $entry The current entry + * @param string $id The field ID + * @param string $name The field name + * @return string The list input field + */ + private static function getListInput($entry, $id, $name) + { + if (isset($entry['required']) && $entry['required'] === true) { + Debug::log('The "required" attribute is not supported for lists.'); + unset($entry['required']); + } + + $list = '<select ' + . self::getInputAttributes($entry) + . ' id="' + . $id + . '" name="' + . $name + . '" >'; + + foreach ($entry['values'] as $name => $value) { + if (is_array($value)) { + $list .= '<optgroup label="' . htmlentities($name) . '">'; + foreach ($value as $subname => $subvalue) { + if ( + $entry['defaultValue'] === $subname + || $entry['defaultValue'] === $subvalue + ) { + $list .= '<option value="' + . $subvalue + . '" selected>' + . $subname + . '</option>'; + } else { + $list .= '<option value="' + . $subvalue + . '">' + . $subname + . '</option>'; + } + } + $list .= '</optgroup>'; + } else { + if ( + $entry['defaultValue'] === $name + || $entry['defaultValue'] === $value + ) { + $list .= '<option value="' + . $value + . '" selected>' + . $name + . '</option>'; + } else { + $list .= '<option value="' + . $value + . '">' + . $name + . '</option>'; + } + } + } + + $list .= '</select>'; + + return $list; + } + + /** + * Get checkbox input + * + * @param array $entry The current entry + * @param string $id The field ID + * @param string $name The field name + * @return string The checkbox input field + */ + private static function getCheckboxInput($entry, $id, $name) + { + if (isset($entry['required']) && $entry['required'] === true) { + Debug::log('The "required" attribute is not supported for checkboxes.'); + unset($entry['required']); + } + + return '<input ' + . self::getInputAttributes($entry) + . ' id="' + . $id + . '" type="checkbox" name="' + . $name + . '" ' + . ($entry['defaultValue'] === 'checked' ? 'checked' : '') + . ' />' + . PHP_EOL; + } + + /** + * Gets a single bridge card + * + * @param string $bridgeName The bridge name + * @param array $formats A list of formats + * @param bool $isActive Indicates if the bridge is active or not + * @return string The bridge card + */ + public static function displayBridgeCard($bridgeName, $formats, $isActive = true) + { + $bridgeFac = new \BridgeFactory(); + + $bridge = $bridgeFac->create($bridgeName); + + if ($bridge == false) { + return ''; + } + + $isHttps = strpos($bridge->getURI(), 'https') === 0; + + $uri = $bridge->getURI(); + $name = $bridge->getName(); + $icon = $bridge->getIcon(); + $description = $bridge->getDescription(); + $parameters = $bridge->getParameters(); + $donationUri = $bridge->getDonationURI(); + $maintainer = $bridge->getMaintainer(); + + $donationsAllowed = Configuration::getConfig('admin', 'donations'); + + if (defined('PROXY_URL') && PROXY_BYBRIDGE) { + $parameters['global']['_noproxy'] = [ + 'name' => 'Disable proxy (' . ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL) . ')', + 'type' => 'checkbox' + ]; + } + + if (CUSTOM_CACHE_TIMEOUT) { + $parameters['global']['_cache_timeout'] = [ + 'name' => 'Cache timeout in seconds', + 'type' => 'number', + 'defaultValue' => $bridge->getCacheTimeout() + ]; + } + + $card = <<<CARD <section id="bridge-{$bridgeName}" data-ref="{$name}"> <h2><a href="{$uri}">{$name}</a></h2> <p class="description">{$description}</p> @@ -330,38 +347,39 @@ This bridge is not fetching its content through a secure connection</div>'; <label class="showmore" for="showmore-{$bridgeName}">Show more</label> CARD; - // If we don't have any parameter for the bridge, we print a generic form to load it. - if (count($parameters) === 0) { - $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps); - - // Display form with cache timeout and/or noproxy options (if enabled) when bridge has no parameters - } else if (count($parameters) === 1 && array_key_exists('global', $parameters)) { - $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, '', $parameters['global']); - } else { - - foreach($parameters as $parameterName => $parameter) { - if(!is_numeric($parameterName) && $parameterName === 'global') - continue; - - if(array_key_exists('global', $parameters)) - $parameter = array_merge($parameter, $parameters['global']); - - if(!is_numeric($parameterName)) - $card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL; - - $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter); - } - - } - - $card .= '<label class="showless" for="showmore-' . $bridgeName . '">Show less</label>'; - if($donationUri !== '' && $donationsAllowed) { - $card .= '<p class="maintainer">' . $maintainer . ' ~ <a href="' . $donationUri . '">Donate</a></p>'; - } else { - $card .= '<p class="maintainer">' . $maintainer . '</p>'; - } - $card .= '</section>'; - - return $card; - } + // If we don't have any parameter for the bridge, we print a generic form to load it. + if (count($parameters) === 0) { + $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps); + + // Display form with cache timeout and/or noproxy options (if enabled) when bridge has no parameters + } elseif (count($parameters) === 1 && array_key_exists('global', $parameters)) { + $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, '', $parameters['global']); + } else { + foreach ($parameters as $parameterName => $parameter) { + if (!is_numeric($parameterName) && $parameterName === 'global') { + continue; + } + + if (array_key_exists('global', $parameters)) { + $parameter = array_merge($parameter, $parameters['global']); + } + + if (!is_numeric($parameterName)) { + $card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL; + } + + $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter); + } + } + + $card .= '<label class="showless" for="showmore-' . $bridgeName . '">Show less</label>'; + if ($donationUri !== '' && $donationsAllowed) { + $card .= '<p class="maintainer">' . $maintainer . ' ~ <a href="' . $donationUri . '">Donate</a></p>'; + } else { + $card .= '<p class="maintainer">' . $maintainer . '</p>'; + } + $card .= '</section>'; + + return $card; + } } diff --git a/lib/BridgeFactory.php b/lib/BridgeFactory.php index f435261c..3e355b7a 100644 --- a/lib/BridgeFactory.php +++ b/lib/BridgeFactory.php @@ -1,87 +1,87 @@ <?php -final class BridgeFactory { +final class BridgeFactory +{ + private $folder; + private $bridgeNames = []; + private $whitelist = []; - private $folder; - private $bridgeNames = []; - private $whitelist = []; + public function __construct(string $folder = PATH_LIB_BRIDGES) + { + $this->folder = $folder; - public function __construct(string $folder = PATH_LIB_BRIDGES) - { - $this->folder = $folder; + // create names + foreach (scandir($this->folder) as $file) { + if (preg_match('/^([^.]+)Bridge\.php$/U', $file, $m)) { + $this->bridgeNames[] = $m[1]; + } + } - // create names - foreach(scandir($this->folder) as $file) { - if(preg_match('/^([^.]+)Bridge\.php$/U', $file, $m)) { - $this->bridgeNames[] = $m[1]; - } - } + // create whitelist + if (file_exists(WHITELIST)) { + $contents = trim(file_get_contents(WHITELIST)); + } elseif (file_exists(WHITELIST_DEFAULT)) { + $contents = trim(file_get_contents(WHITELIST_DEFAULT)); + } else { + $contents = ''; + } + if ($contents === '*') { // Whitelist all bridges + $this->whitelist = $this->getBridgeNames(); + } else { + foreach (explode("\n", $contents) as $bridgeName) { + $this->whitelist[] = $this->sanitizeBridgeName($bridgeName); + } + } + } - // create whitelist - if (file_exists(WHITELIST)) { - $contents = trim(file_get_contents(WHITELIST)); - } elseif (file_exists(WHITELIST_DEFAULT)) { - $contents = trim(file_get_contents(WHITELIST_DEFAULT)); - } else { - $contents = ''; - } - if ($contents === '*') { // Whitelist all bridges - $this->whitelist = $this->getBridgeNames(); - } else { - foreach (explode("\n", $contents) as $bridgeName) { - $this->whitelist[] = $this->sanitizeBridgeName($bridgeName); - } - } - } + public function create(string $name): BridgeInterface + { + if (preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) { + $className = sprintf('%sBridge', $this->sanitizeBridgeName($name)); + return new $className(); + } + throw new \InvalidArgumentException('Bridge name invalid!'); + } - public function create(string $name): BridgeInterface - { - if(preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) { - $className = sprintf('%sBridge', $this->sanitizeBridgeName($name)); - return new $className(); - } - throw new \InvalidArgumentException('Bridge name invalid!'); - } + public function getBridgeNames(): array + { + return $this->bridgeNames; + } - public function getBridgeNames(): array - { - return $this->bridgeNames; - } + public function isWhitelisted($name): bool + { + return in_array($this->sanitizeBridgeName($name), $this->whitelist); + } - public function isWhitelisted($name): bool - { - return in_array($this->sanitizeBridgeName($name), $this->whitelist); - } + private function sanitizeBridgeName($name) + { + if (!is_string($name)) { + return null; + } - private function sanitizeBridgeName($name) { + // Trim trailing '.php' if exists + if (preg_match('/(.+)(?:\.php)/', $name, $matches)) { + $name = $matches[1]; + } - if(!is_string($name)) { - return null; - } + // Trim trailing 'Bridge' if exists + if (preg_match('/(.+)(?:Bridge)/i', $name, $matches)) { + $name = $matches[1]; + } - // Trim trailing '.php' if exists - if (preg_match('/(.+)(?:\.php)/', $name, $matches)) { - $name = $matches[1]; - } + // Improve performance for correctly written bridge names + if (in_array($name, $this->getBridgeNames())) { + $index = array_search($name, $this->getBridgeNames()); + return $this->getBridgeNames()[$index]; + } - // Trim trailing 'Bridge' if exists - if (preg_match('/(.+)(?:Bridge)/i', $name, $matches)) { - $name = $matches[1]; - } + // The name is valid if a corresponding bridge file is found on disk + if (in_array(strtolower($name), array_map('strtolower', $this->getBridgeNames()))) { + $index = array_search(strtolower($name), array_map('strtolower', $this->getBridgeNames())); + return $this->getBridgeNames()[$index]; + } - // Improve performance for correctly written bridge names - if (in_array($name, $this->getBridgeNames())) { - $index = array_search($name, $this->getBridgeNames()); - return $this->getBridgeNames()[$index]; - } - - // The name is valid if a corresponding bridge file is found on disk - if (in_array(strtolower($name), array_map('strtolower', $this->getBridgeNames()))) { - $index = array_search(strtolower($name), array_map('strtolower', $this->getBridgeNames())); - return $this->getBridgeNames()[$index]; - } - - Debug::log('Invalid bridge name specified: "' . $name . '"!'); - return null; - } + Debug::log('Invalid bridge name specified: "' . $name . '"!'); + return null; + } } diff --git a/lib/BridgeInterface.php b/lib/BridgeInterface.php index 70625125..6cf949c8 100644 --- a/lib/BridgeInterface.php +++ b/lib/BridgeInterface.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -52,93 +53,94 @@ * * **Cache timeout** * The default cache timeout for the bridge. */ -interface BridgeInterface { - /** - * Collects data from the site - */ - public function collectData(); +interface BridgeInterface +{ + /** + * Collects data from the site + */ + public function collectData(); - /** - * Get the user's supplied configuration for the bridge - */ - public function getConfiguration(); + /** + * Get the user's supplied configuration for the bridge + */ + public function getConfiguration(); - /** - * Returns the value for the selected configuration - * - * @param string $input The option name - * @return mixed|null The option value or null if the input is not defined - */ - public function getOption($name); + /** + * Returns the value for the selected configuration + * + * @param string $input The option name + * @return mixed|null The option value or null if the input is not defined + */ + public function getOption($name); - /** - * Returns the description - * - * @return string Description - */ - public function getDescription(); + /** + * Returns the description + * + * @return string Description + */ + public function getDescription(); - /** - * Returns an array of collected items - * - * @return array Associative array of items - */ - public function getItems(); + /** + * Returns an array of collected items + * + * @return array Associative array of items + */ + public function getItems(); - /** - * Returns the bridge maintainer - * - * @return string Bridge maintainer - */ - public function getMaintainer(); + /** + * Returns the bridge maintainer + * + * @return string Bridge maintainer + */ + public function getMaintainer(); - /** - * Returns the bridge name - * - * @return string Bridge name - */ - public function getName(); + /** + * Returns the bridge name + * + * @return string Bridge name + */ + public function getName(); - /** - * Returns the bridge icon - * - * @return string Bridge icon - */ - public function getIcon(); + /** + * Returns the bridge icon + * + * @return string Bridge icon + */ + public function getIcon(); - /** - * Returns the bridge parameters - * - * @return array Bridge parameters - */ - public function getParameters(); + /** + * Returns the bridge parameters + * + * @return array Bridge parameters + */ + public function getParameters(); - /** - * Returns the bridge URI - * - * @return string Bridge URI - */ - public function getURI(); + /** + * Returns the bridge URI + * + * @return string Bridge URI + */ + public function getURI(); - /** - * Returns the bridge Donation URI - * - * @return string Bridge Donation URI - */ - public function getDonationURI(); + /** + * Returns the bridge Donation URI + * + * @return string Bridge Donation URI + */ + public function getDonationURI(); - /** - * Returns the cache timeout - * - * @return int Cache timeout - */ - public function getCacheTimeout(); + /** + * Returns the cache timeout + * + * @return int Cache timeout + */ + public function getCacheTimeout(); - /** - * Returns parameters from given URL or null if URL is not applicable - * - * @param string $url URL to extract parameters from - * @return array|null List of bridge parameters or null if detection failed. - */ - public function detectParameters($url); + /** + * Returns parameters from given URL or null if URL is not applicable + * + * @param string $url URL to extract parameters from + * @return array|null List of bridge parameters or null if detection failed. + */ + public function detectParameters($url); } diff --git a/lib/BridgeList.php b/lib/BridgeList.php index c5082e57..921dfe50 100644 --- a/lib/BridgeList.php +++ b/lib/BridgeList.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -19,14 +20,16 @@ * * @todo Return error if a caller creates an object of this class. */ -final class BridgeList { - /** - * Get the document head - * - * @return string The document head - */ - private static function getHead() { - return <<<EOD +final class BridgeList +{ + /** + * Get the document head + * + * @return string The document head + */ + private static function getHead() + { + return <<<EOD <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> @@ -45,91 +48,87 @@ final class BridgeList { </noscript> </head> EOD; - } - - /** - * Get the document body for all bridge cards - * - * @param bool $showInactive Inactive bridges are visible on the home page if - * enabled. - * @param int $totalBridges (ref) Returns the total number of bridges. - * @param int $totalActiveBridges (ref) Returns the number of active bridges. - * @return string The document body for all bridge cards. - */ - private static function getBridges($showInactive, &$totalBridges, &$totalActiveBridges) { - - $body = ''; - $totalActiveBridges = 0; - $inactiveBridges = ''; - - $bridgeFac = new \BridgeFactory(); - $bridgeList = $bridgeFac->getBridgeNames(); - - $formatFac = new FormatFactory(); - $formats = $formatFac->getFormatNames(); - - $totalBridges = count($bridgeList); - - foreach($bridgeList as $bridgeName) { - - if($bridgeFac->isWhitelisted($bridgeName)) { - - $body .= BridgeCard::displayBridgeCard($bridgeName, $formats); - $totalActiveBridges++; - - } elseif($showInactive) { - - // inactive bridges - $inactiveBridges .= BridgeCard::displayBridgeCard($bridgeName, $formats, false) . PHP_EOL; - - } - - } - - $body .= $inactiveBridges; - - return $body; - } - - /** - * Get the document header - * - * @return string The document header - */ - private static function getHeader() { - $warning = ''; - - if(Debug::isEnabled()) { - if(!Debug::isSecure()) { - $warning .= <<<EOD + } + + /** + * Get the document body for all bridge cards + * + * @param bool $showInactive Inactive bridges are visible on the home page if + * enabled. + * @param int $totalBridges (ref) Returns the total number of bridges. + * @param int $totalActiveBridges (ref) Returns the number of active bridges. + * @return string The document body for all bridge cards. + */ + private static function getBridges($showInactive, &$totalBridges, &$totalActiveBridges) + { + $body = ''; + $totalActiveBridges = 0; + $inactiveBridges = ''; + + $bridgeFac = new \BridgeFactory(); + $bridgeList = $bridgeFac->getBridgeNames(); + + $formatFac = new FormatFactory(); + $formats = $formatFac->getFormatNames(); + + $totalBridges = count($bridgeList); + + foreach ($bridgeList as $bridgeName) { + if ($bridgeFac->isWhitelisted($bridgeName)) { + $body .= BridgeCard::displayBridgeCard($bridgeName, $formats); + $totalActiveBridges++; + } elseif ($showInactive) { + // inactive bridges + $inactiveBridges .= BridgeCard::displayBridgeCard($bridgeName, $formats, false) . PHP_EOL; + } + } + + $body .= $inactiveBridges; + + return $body; + } + + /** + * Get the document header + * + * @return string The document header + */ + private static function getHeader() + { + $warning = ''; + + if (Debug::isEnabled()) { + if (!Debug::isSecure()) { + $warning .= <<<EOD <section class="critical-warning">Warning : Debug mode is active from any location, make sure only you can access RSS-Bridge.</section> EOD; - } else { - $warning .= <<<EOD + } else { + $warning .= <<<EOD <section class="warning">Warning : Debug mode is active from your IP address, your requests will bypass the cache.</section> EOD; - } - } + } + } - return <<<EOD + return <<<EOD <header> <div class="logo"></div> {$warning} </header> EOD; - } - - /** - * Get the searchbar - * - * @return string The searchbar - */ - private static function getSearchbar() { - $query = filter_input(INPUT_GET, 'q', FILTER_SANITIZE_SPECIAL_CHARS); - - return <<<EOD + } + + /** + * Get the searchbar + * + * @return string The searchbar + */ + private static function getSearchbar() + { + $query = filter_input(INPUT_GET, 'q', FILTER_SANITIZE_SPECIAL_CHARS); + + return <<<EOD <section class="searchbar"> <h3>Search</h3> <input type="text" name="searchfield" @@ -137,46 +136,45 @@ EOD; onchange="search()" onkeyup="search()" value="{$query}"> </section> EOD; - } - - /** - * Get the document footer - * - * @param int $totalBridges The total number of bridges, shown in the footer - * @param int $totalActiveBridges The total number of active bridges, shown - * in the footer. - * @param bool $showInactive Sets the 'Show active'/'Show inactive' text in - * the footer. - * @return string The document footer - */ - private static function getFooter($totalBridges, $totalActiveBridges, $showInactive) { - $version = Configuration::getVersion(); - - $email = Configuration::getConfig('admin', 'email'); - $admininfo = ''; - if (!empty($email)) { - $admininfo = <<<EOD + } + + /** + * Get the document footer + * + * @param int $totalBridges The total number of bridges, shown in the footer + * @param int $totalActiveBridges The total number of active bridges, shown + * in the footer. + * @param bool $showInactive Sets the 'Show active'/'Show inactive' text in + * the footer. + * @return string The document footer + */ + private static function getFooter($totalBridges, $totalActiveBridges, $showInactive) + { + $version = Configuration::getVersion(); + + $email = Configuration::getConfig('admin', 'email'); + $admininfo = ''; + if (!empty($email)) { + $admininfo = <<<EOD <br /> <span> You may email the administrator of this RSS-Bridge instance at <a href="mailto:{$email}">{$email}</a> </span> EOD; - } + } - $inactive = ''; - - if($totalActiveBridges !== $totalBridges) { - - if(!$showInactive) { - $inactive = '<a href="?show_inactive=1"><button class="small">Show inactive bridges</button></a><br>'; - } else { - $inactive = '<a href="?show_inactive=0"><button class="small">Hide inactive bridges</button></a><br>'; - } + $inactive = ''; - } + if ($totalActiveBridges !== $totalBridges) { + if (!$showInactive) { + $inactive = '<a href="?show_inactive=1"><button class="small">Show inactive bridges</button></a><br>'; + } else { + $inactive = '<a href="?show_inactive=0"><button class="small">Hide inactive bridges</button></a><br>'; + } + } - return <<<EOD + return <<<EOD <section class="footer"> <a href="https://github.com/rss-bridge/rss-bridge">RSS-Bridge ~ Public Domain</a><br> <p class="version">{$version}</p> @@ -185,28 +183,27 @@ EOD; {$admininfo} </section> EOD; - } - - /** - * Create the entire home page - * - * @param bool $showInactive Inactive bridges are displayed on the home page, - * if enabled. - * @return string The home page - */ - public static function create($showInactive = true) { - - $totalBridges = 0; - $totalActiveBridges = 0; - - return '<!DOCTYPE html><html lang="en">' - . BridgeList::getHead() - . '<body onload="search()">' - . BridgeList::getHeader() - . BridgeList::getSearchbar() - . BridgeList::getBridges($showInactive, $totalBridges, $totalActiveBridges) - . BridgeList::getFooter($totalBridges, $totalActiveBridges, $showInactive) - . '</body></html>'; - - } + } + + /** + * Create the entire home page + * + * @param bool $showInactive Inactive bridges are displayed on the home page, + * if enabled. + * @return string The home page + */ + public static function create($showInactive = true) + { + $totalBridges = 0; + $totalActiveBridges = 0; + + return '<!DOCTYPE html><html lang="en">' + . BridgeList::getHead() + . '<body onload="search()">' + . BridgeList::getHeader() + . BridgeList::getSearchbar() + . BridgeList::getBridges($showInactive, $totalBridges, $totalActiveBridges) + . BridgeList::getFooter($totalBridges, $totalActiveBridges, $showInactive) + . '</body></html>'; + } } diff --git a/lib/CacheFactory.php b/lib/CacheFactory.php index 451f625f..ba1c3cb9 100644 --- a/lib/CacheFactory.php +++ b/lib/CacheFactory.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,62 +7,62 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ class CacheFactory { - private $folder; - private $cacheNames; + private $folder; + private $cacheNames; - public function __construct(string $folder = PATH_LIB_CACHES) - { - $this->folder = $folder; - // create cache names - foreach(scandir($this->folder) as $file) { - if(preg_match('/^([^.]+)Cache\.php$/U', $file, $m)) { - $this->cacheNames[] = $m[1]; - } - } - } + public function __construct(string $folder = PATH_LIB_CACHES) + { + $this->folder = $folder; + // create cache names + foreach (scandir($this->folder) as $file) { + if (preg_match('/^([^.]+)Cache\.php$/U', $file, $m)) { + $this->cacheNames[] = $m[1]; + } + } + } - /** - * @param string $name The name of the cache e.g. "File", "Memcached" or "SQLite" - */ - public function create(string $name): CacheInterface - { - $name = $this->sanitizeCacheName($name) . 'Cache'; + /** + * @param string $name The name of the cache e.g. "File", "Memcached" or "SQLite" + */ + public function create(string $name): CacheInterface + { + $name = $this->sanitizeCacheName($name) . 'Cache'; - if(! preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) { - throw new \InvalidArgumentException('Cache name invalid!'); - } + if (! preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name)) { + throw new \InvalidArgumentException('Cache name invalid!'); + } - $filePath = $this->folder . $name . '.php'; - if(!file_exists($filePath)) { - throw new \Exception('Invalid cache'); - } - $className = '\\' . $name; - return new $className(); - } + $filePath = $this->folder . $name . '.php'; + if (!file_exists($filePath)) { + throw new \Exception('Invalid cache'); + } + $className = '\\' . $name; + return new $className(); + } - protected function sanitizeCacheName(string $name) - { - // Trim trailing '.php' if exists - if (preg_match('/(.+)(?:\.php)/', $name, $matches)) { - $name = $matches[1]; - } + protected function sanitizeCacheName(string $name) + { + // Trim trailing '.php' if exists + if (preg_match('/(.+)(?:\.php)/', $name, $matches)) { + $name = $matches[1]; + } - // Trim trailing 'Cache' if exists - if (preg_match('/(.+)(?:Cache)$/i', $name, $matches)) { - $name = $matches[1]; - } + // Trim trailing 'Cache' if exists + if (preg_match('/(.+)(?:Cache)$/i', $name, $matches)) { + $name = $matches[1]; + } - if(in_array(strtolower($name), array_map('strtolower', $this->cacheNames))) { - $index = array_search(strtolower($name), array_map('strtolower', $this->cacheNames)); - return $this->cacheNames[$index]; - } - return null; - } + if (in_array(strtolower($name), array_map('strtolower', $this->cacheNames))) { + $index = array_search(strtolower($name), array_map('strtolower', $this->cacheNames)); + return $this->cacheNames[$index]; + } + return null; + } } diff --git a/lib/CacheInterface.php b/lib/CacheInterface.php index 091c5f02..67cee681 100644 --- a/lib/CacheInterface.php +++ b/lib/CacheInterface.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,61 +7,62 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** * The cache interface */ -interface CacheInterface { - /** - * Set scope of the current cache - * - * If $scope is an empty string, the cache is set to a global context. - * - * @param string $scope The scope the data is related to - */ - public function setScope($scope); +interface CacheInterface +{ + /** + * Set scope of the current cache + * + * If $scope is an empty string, the cache is set to a global context. + * + * @param string $scope The scope the data is related to + */ + public function setScope($scope); - /** - * Set key to assign the current data - * - * Since $key can be anything, the cache implementation must ensure to - * assign the related data reliably; most commonly by serializing and - * hashing the key in an appropriate way. - * - * @param array $key The key the data is related to - */ - public function setKey($key); + /** + * Set key to assign the current data + * + * Since $key can be anything, the cache implementation must ensure to + * assign the related data reliably; most commonly by serializing and + * hashing the key in an appropriate way. + * + * @param array $key The key the data is related to + */ + public function setKey($key); - /** - * Loads data from cache - * - * @return mixed The cached data or null - */ - public function loadData(); + /** + * Loads data from cache + * + * @return mixed The cached data or null + */ + public function loadData(); - /** - * Stores data to the cache - * - * @param mixed $data The data to store - * @return self The cache object - */ - public function saveData($data); + /** + * Stores data to the cache + * + * @param mixed $data The data to store + * @return self The cache object + */ + public function saveData($data); - /** - * Returns the timestamp for the curent cache data - * - * @return int Timestamp or null - */ - public function getTime(); + /** + * Returns the timestamp for the curent cache data + * + * @return int Timestamp or null + */ + public function getTime(); - /** - * Removes any data that is older than the specified age from cache - * - * @param int $seconds The cache age in seconds - */ - public function purgeCache($seconds); + /** + * Removes any data that is older than the specified age from cache + * + * @param int $seconds The cache age in seconds + */ + public function purgeCache($seconds); } diff --git a/lib/Configuration.php b/lib/Configuration.php index e97e7d6b..ce01b7df 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -16,294 +17,317 @@ * * This class implements a configuration module for RSS-Bridge. */ -final class Configuration { - - /** - * Holds the current release version of RSS-Bridge. - * - * Do not access this property directly! - * Use {@see Configuration::getVersion()} instead. - * - * @var string - * - * @todo Replace this property by a constant. - */ - public static $VERSION = 'dev.2022-06-14'; - - /** - * Holds the configuration data. - * - * Do not access this property directly! - * Use {@see Configuration::getConfig()} instead. - * - * @var array|null - */ - private static $config = null; - - /** - * Throw an exception when trying to create a new instance of this class. - * - * @throws \LogicException if called. - */ - public function __construct(){ - throw new \LogicException('Can\'t create object of this class!'); - } - - /** - * Verifies the current installation of RSS-Bridge and PHP. - * - * Returns an error message and aborts execution if the installation does - * not satisfy the requirements of RSS-Bridge. - * - * **Requirements** - * - PHP 7.1.0 or higher - * - `openssl` extension - * - `libxml` extension - * - `mbstring` extension - * - `simplexml` extension - * - `curl` extension - * - `json` extension - * - The cache folder specified by {@see PATH_CACHE} requires write permission - * - The whitelist file specified by {@see WHITELIST} requires write permission - * - * @link http://php.net/supported-versions.php PHP Supported Versions - * @link http://php.net/manual/en/book.openssl.php OpenSSL - * @link http://php.net/manual/en/book.libxml.php libxml - * @link http://php.net/manual/en/book.mbstring.php Multibyte String (mbstring) - * @link http://php.net/manual/en/book.simplexml.php SimpleXML - * @link http://php.net/manual/en/book.curl.php Client URL Library (curl) - * @link http://php.net/manual/en/book.json.php JavaScript Object Notation (json) - * - * @return void - */ - public static function verifyInstallation() { - - // Check PHP version - if(version_compare(PHP_VERSION, '7.4.0') === -1) { - self::reportError('RSS-Bridge requires at least PHP version 7.4.0!'); - } - // extensions check - if(!extension_loaded('openssl')) - self::reportError('"openssl" extension not loaded. Please check "php.ini"'); - - if(!extension_loaded('libxml')) - self::reportError('"libxml" extension not loaded. Please check "php.ini"'); - - if(!extension_loaded('mbstring')) - self::reportError('"mbstring" extension not loaded. Please check "php.ini"'); - - if(!extension_loaded('simplexml')) - self::reportError('"simplexml" extension not loaded. Please check "php.ini"'); - - // Allow RSS-Bridge to run without curl module in CLI mode without root certificates - if(!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo')))) - self::reportError('"curl" extension not loaded. Please check "php.ini"'); - - if(!extension_loaded('json')) - self::reportError('"json" extension not loaded. Please check "php.ini"'); - - } - - /** - * Loads the configuration from disk and checks if the parameters are valid. - * - * Returns an error message and aborts execution if the configuration is invalid. - * - * The RSS-Bridge configuration is split into two files: - * - {@see FILE_CONFIG_DEFAULT} The default configuration file that ships - * with every release of RSS-Bridge (do not modify this file!). - * - {@see FILE_CONFIG} The local configuration file that can be modified - * by server administrators. - * - * RSS-Bridge will first load {@see FILE_CONFIG_DEFAULT} into memory and then - * replace parameters with the contents of {@see FILE_CONFIG}. That way new - * parameters are automatically initialized with default values and custom - * configurations can be reduced to the minimum set of parametes necessary - * (only the ones that changed). - * - * The configuration files must be placed in the root folder of RSS-Bridge - * (next to `index.php`). - * - * _Notice_: The configuration is stored in {@see Configuration::$config}. - * - * @return void - */ - public static function loadConfiguration() { - - if(!file_exists(FILE_CONFIG_DEFAULT)) - self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT); - - Configuration::$config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED); - if(!Configuration::$config) - self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT); - - if(file_exists(FILE_CONFIG)) { - // Replace default configuration with custom settings - foreach(parse_ini_file(FILE_CONFIG, true, INI_SCANNER_TYPED) as $header => $section) { - foreach($section as $key => $value) { - Configuration::$config[$header][$key] = $value; - } - } - } - - foreach (getenv() as $envkey => $value) { - // Replace all settings with their respective environment variable if available - $keyArray = explode('_', $envkey); - if($keyArray[0] === 'RSSBRIDGE') { - $header = strtolower($keyArray[1]); - $key = strtolower($keyArray[2]); - if($value === 'true' || $value === 'false') { - $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); - } - Configuration::$config[$header][$key] = $value; - } - } - - if(!is_string(self::getConfig('system', 'timezone')) - || !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC))) - self::reportConfigurationError('system', 'timezone'); - - date_default_timezone_set(self::getConfig('system', 'timezone')); - - if(!is_string(self::getConfig('proxy', 'url'))) - self::reportConfigurationError('proxy', 'url', 'Is not a valid string'); - - if(!empty(self::getConfig('proxy', 'url'))) { - /** URL of the proxy server */ - define('PROXY_URL', self::getConfig('proxy', 'url')); - } - - if(!is_bool(self::getConfig('proxy', 'by_bridge'))) - self::reportConfigurationError('proxy', 'by_bridge', 'Is not a valid Boolean'); - - /** True if proxy usage can be enabled selectively for each bridge */ - define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge')); - - if(!is_string(self::getConfig('proxy', 'name'))) - self::reportConfigurationError('proxy', 'name', 'Is not a valid string'); - - /** Name of the proxy server */ - define('PROXY_NAME', self::getConfig('proxy', 'name')); - - if(!is_string(self::getConfig('cache', 'type'))) - self::reportConfigurationError('cache', 'type', 'Is not a valid string'); - - if(!is_bool(self::getConfig('cache', 'custom_timeout'))) - self::reportConfigurationError('cache', 'custom_timeout', 'Is not a valid Boolean'); - - /** True if the cache timeout can be specified by the user */ - define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout')); - - if(!is_bool(self::getConfig('authentication', 'enable'))) - self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean'); - - if(!is_string(self::getConfig('authentication', 'username'))) - self::reportConfigurationError('authentication', 'username', 'Is not a valid string'); - - if(!is_string(self::getConfig('authentication', 'password'))) - self::reportConfigurationError('authentication', 'password', 'Is not a valid string'); - - if(!empty(self::getConfig('admin', 'email')) - && !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL)) - self::reportConfigurationError('admin', 'email', 'Is not a valid email address'); - - if(!is_bool(self::getConfig('admin', 'donations'))) - self::reportConfigurationError('admin', 'donations', 'Is not a valid Boolean'); - - if(!is_string(self::getConfig('error', 'output'))) - self::reportConfigurationError('error', 'output', 'Is not a valid String'); - - if(!is_numeric(self::getConfig('error', 'report_limit')) - || self::getConfig('error', 'report_limit') < 1) - self::reportConfigurationError('admin', 'report_limit', 'Value is invalid'); - - } - - /** - * Returns the value of a parameter identified by section and key. - * - * @param string $section The section name. - * @param string $key The property name (key). - * @return mixed|null The parameter value. - */ - public static function getConfig($section, $key) { - if(array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section])) { - return self::$config[$section][$key]; - } - - return null; - } - - /** - * Returns the current version string of RSS-Bridge. - * - * This function returns the contents of {@see Configuration::$VERSION} for - * regular installations and the git branch name and commit id for instances - * running in a git environment. - * - * @return string The version string. - */ - public static function getVersion() { - - $headFile = PATH_ROOT . '.git/HEAD'; - - // '@' is used to mute open_basedir warning - if(@is_readable($headFile)) { - - $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1); - $parts = explode('/', $revisionHashFile); - - if(isset($parts[3])) { - $branchName = $parts[3]; - if(file_exists($revisionHashFile)) { - return 'git.' . $branchName . '.' . substr(file_get_contents($revisionHashFile), 0, 7); - } - } - } - - return Configuration::$VERSION; - - } - - /** - * Reports an configuration error for the specified section and key to the - * user and ends execution - * - * @param string $section The section name - * @param string $key The configuration key - * @param string $message An optional message to the user - * - * @return void - */ - private static function reportConfigurationError($section, $key, $message = '') { - - $report = "Parameter [{$section}] => \"{$key}\" is invalid!" . PHP_EOL; - - if(file_exists(FILE_CONFIG)) { - $report .= 'Please check your configuration file at ' . FILE_CONFIG . PHP_EOL; - } elseif(!file_exists(FILE_CONFIG_DEFAULT)) { - $report .= 'The default configuration file is missing at ' . FILE_CONFIG_DEFAULT . PHP_EOL; - } else { - $report .= 'The default configuration file is broken.' . PHP_EOL - . 'Restore the original file from ' . REPOSITORY . PHP_EOL; - } - - $report .= $message; - self::reportError($report); - - } - - /** - * Reports an error message to the user and ends execution - * - * @param string $message The error message - * - * @return void - */ - private static function reportError($message) { - - header('Content-Type: text/plain', true, 500); - die('Configuration error' . PHP_EOL . $message); - - } +final class Configuration +{ + /** + * Holds the current release version of RSS-Bridge. + * + * Do not access this property directly! + * Use {@see Configuration::getVersion()} instead. + * + * @var string + * + * @todo Replace this property by a constant. + */ + public static $VERSION = 'dev.2022-06-14'; + + /** + * Holds the configuration data. + * + * Do not access this property directly! + * Use {@see Configuration::getConfig()} instead. + * + * @var array|null + */ + private static $config = null; + + /** + * Throw an exception when trying to create a new instance of this class. + * + * @throws \LogicException if called. + */ + public function __construct() + { + throw new \LogicException('Can\'t create object of this class!'); + } + + /** + * Verifies the current installation of RSS-Bridge and PHP. + * + * Returns an error message and aborts execution if the installation does + * not satisfy the requirements of RSS-Bridge. + * + * **Requirements** + * - PHP 7.1.0 or higher + * - `openssl` extension + * - `libxml` extension + * - `mbstring` extension + * - `simplexml` extension + * - `curl` extension + * - `json` extension + * - The cache folder specified by {@see PATH_CACHE} requires write permission + * - The whitelist file specified by {@see WHITELIST} requires write permission + * + * @link http://php.net/supported-versions.php PHP Supported Versions + * @link http://php.net/manual/en/book.openssl.php OpenSSL + * @link http://php.net/manual/en/book.libxml.php libxml + * @link http://php.net/manual/en/book.mbstring.php Multibyte String (mbstring) + * @link http://php.net/manual/en/book.simplexml.php SimpleXML + * @link http://php.net/manual/en/book.curl.php Client URL Library (curl) + * @link http://php.net/manual/en/book.json.php JavaScript Object Notation (json) + * + * @return void + */ + public static function verifyInstallation() + { + // Check PHP version + if (version_compare(PHP_VERSION, '7.4.0') === -1) { + self::reportError('RSS-Bridge requires at least PHP version 7.4.0!'); + } + // extensions check + if (!extension_loaded('openssl')) { + self::reportError('"openssl" extension not loaded. Please check "php.ini"'); + } + + if (!extension_loaded('libxml')) { + self::reportError('"libxml" extension not loaded. Please check "php.ini"'); + } + + if (!extension_loaded('mbstring')) { + self::reportError('"mbstring" extension not loaded. Please check "php.ini"'); + } + + if (!extension_loaded('simplexml')) { + self::reportError('"simplexml" extension not loaded. Please check "php.ini"'); + } + + // Allow RSS-Bridge to run without curl module in CLI mode without root certificates + if (!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo')))) { + self::reportError('"curl" extension not loaded. Please check "php.ini"'); + } + + if (!extension_loaded('json')) { + self::reportError('"json" extension not loaded. Please check "php.ini"'); + } + } + + /** + * Loads the configuration from disk and checks if the parameters are valid. + * + * Returns an error message and aborts execution if the configuration is invalid. + * + * The RSS-Bridge configuration is split into two files: + * - {@see FILE_CONFIG_DEFAULT} The default configuration file that ships + * with every release of RSS-Bridge (do not modify this file!). + * - {@see FILE_CONFIG} The local configuration file that can be modified + * by server administrators. + * + * RSS-Bridge will first load {@see FILE_CONFIG_DEFAULT} into memory and then + * replace parameters with the contents of {@see FILE_CONFIG}. That way new + * parameters are automatically initialized with default values and custom + * configurations can be reduced to the minimum set of parametes necessary + * (only the ones that changed). + * + * The configuration files must be placed in the root folder of RSS-Bridge + * (next to `index.php`). + * + * _Notice_: The configuration is stored in {@see Configuration::$config}. + * + * @return void + */ + public static function loadConfiguration() + { + if (!file_exists(FILE_CONFIG_DEFAULT)) { + self::reportError('The default configuration file is missing at ' . FILE_CONFIG_DEFAULT); + } + + Configuration::$config = parse_ini_file(FILE_CONFIG_DEFAULT, true, INI_SCANNER_TYPED); + if (!Configuration::$config) { + self::reportError('Error parsing ' . FILE_CONFIG_DEFAULT); + } + + if (file_exists(FILE_CONFIG)) { + // Replace default configuration with custom settings + foreach (parse_ini_file(FILE_CONFIG, true, INI_SCANNER_TYPED) as $header => $section) { + foreach ($section as $key => $value) { + Configuration::$config[$header][$key] = $value; + } + } + } + + foreach (getenv() as $envkey => $value) { + // Replace all settings with their respective environment variable if available + $keyArray = explode('_', $envkey); + if ($keyArray[0] === 'RSSBRIDGE') { + $header = strtolower($keyArray[1]); + $key = strtolower($keyArray[2]); + if ($value === 'true' || $value === 'false') { + $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + Configuration::$config[$header][$key] = $value; + } + } + + if ( + !is_string(self::getConfig('system', 'timezone')) + || !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)) + ) { + self::reportConfigurationError('system', 'timezone'); + } + + date_default_timezone_set(self::getConfig('system', 'timezone')); + + if (!is_string(self::getConfig('proxy', 'url'))) { + self::reportConfigurationError('proxy', 'url', 'Is not a valid string'); + } + + if (!empty(self::getConfig('proxy', 'url'))) { + /** URL of the proxy server */ + define('PROXY_URL', self::getConfig('proxy', 'url')); + } + + if (!is_bool(self::getConfig('proxy', 'by_bridge'))) { + self::reportConfigurationError('proxy', 'by_bridge', 'Is not a valid Boolean'); + } + + /** True if proxy usage can be enabled selectively for each bridge */ + define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge')); + + if (!is_string(self::getConfig('proxy', 'name'))) { + self::reportConfigurationError('proxy', 'name', 'Is not a valid string'); + } + + /** Name of the proxy server */ + define('PROXY_NAME', self::getConfig('proxy', 'name')); + + if (!is_string(self::getConfig('cache', 'type'))) { + self::reportConfigurationError('cache', 'type', 'Is not a valid string'); + } + + if (!is_bool(self::getConfig('cache', 'custom_timeout'))) { + self::reportConfigurationError('cache', 'custom_timeout', 'Is not a valid Boolean'); + } + + /** True if the cache timeout can be specified by the user */ + define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout')); + + if (!is_bool(self::getConfig('authentication', 'enable'))) { + self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean'); + } + + if (!is_string(self::getConfig('authentication', 'username'))) { + self::reportConfigurationError('authentication', 'username', 'Is not a valid string'); + } + + if (!is_string(self::getConfig('authentication', 'password'))) { + self::reportConfigurationError('authentication', 'password', 'Is not a valid string'); + } + + if ( + !empty(self::getConfig('admin', 'email')) + && !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL) + ) { + self::reportConfigurationError('admin', 'email', 'Is not a valid email address'); + } + + if (!is_bool(self::getConfig('admin', 'donations'))) { + self::reportConfigurationError('admin', 'donations', 'Is not a valid Boolean'); + } + + if (!is_string(self::getConfig('error', 'output'))) { + self::reportConfigurationError('error', 'output', 'Is not a valid String'); + } + + if ( + !is_numeric(self::getConfig('error', 'report_limit')) + || self::getConfig('error', 'report_limit') < 1 + ) { + self::reportConfigurationError('admin', 'report_limit', 'Value is invalid'); + } + } + + /** + * Returns the value of a parameter identified by section and key. + * + * @param string $section The section name. + * @param string $key The property name (key). + * @return mixed|null The parameter value. + */ + public static function getConfig($section, $key) + { + if (array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section])) { + return self::$config[$section][$key]; + } + + return null; + } + + /** + * Returns the current version string of RSS-Bridge. + * + * This function returns the contents of {@see Configuration::$VERSION} for + * regular installations and the git branch name and commit id for instances + * running in a git environment. + * + * @return string The version string. + */ + public static function getVersion() + { + $headFile = PATH_ROOT . '.git/HEAD'; + + // '@' is used to mute open_basedir warning + if (@is_readable($headFile)) { + $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1); + $parts = explode('/', $revisionHashFile); + + if (isset($parts[3])) { + $branchName = $parts[3]; + if (file_exists($revisionHashFile)) { + return 'git.' . $branchName . '.' . substr(file_get_contents($revisionHashFile), 0, 7); + } + } + } + + return Configuration::$VERSION; + } + + /** + * Reports an configuration error for the specified section and key to the + * user and ends execution + * + * @param string $section The section name + * @param string $key The configuration key + * @param string $message An optional message to the user + * + * @return void + */ + private static function reportConfigurationError($section, $key, $message = '') + { + $report = "Parameter [{$section}] => \"{$key}\" is invalid!" . PHP_EOL; + + if (file_exists(FILE_CONFIG)) { + $report .= 'Please check your configuration file at ' . FILE_CONFIG . PHP_EOL; + } elseif (!file_exists(FILE_CONFIG_DEFAULT)) { + $report .= 'The default configuration file is missing at ' . FILE_CONFIG_DEFAULT . PHP_EOL; + } else { + $report .= 'The default configuration file is broken.' . PHP_EOL + . 'Restore the original file from ' . REPOSITORY . PHP_EOL; + } + + $report .= $message; + self::reportError($report); + } + + /** + * Reports an error message to the user and ends execution + * + * @param string $message The error message + * + * @return void + */ + private static function reportError($message) + { + header('Content-Type: text/plain', true, 500); + die('Configuration error' . PHP_EOL . $message); + } } diff --git a/lib/Debug.php b/lib/Debug.php index f912fb3b..75bf5f33 100644 --- a/lib/Debug.php +++ b/lib/Debug.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -30,92 +31,93 @@ * Warning: In debug mode your server may display sensitive information! For * security reasons it is recommended to whitelist only specific IP addresses. */ -class Debug { - - /** - * Indicates if debug mode is enabled. - * - * Do not access this property directly! - * Use {@see Debug::isEnabled()} instead. - * - * @var bool - */ - private static $enabled = false; - - /** - * Indicates if debug mode is secure. - * - * Do not access this property directly! - * Use {@see Debug::isSecure()} instead. - * - * @var bool - */ - private static $secure = false; - - /** - * Returns true if debug mode is enabled - * - * If debug mode is enabled, sets `display_errors = 1` and `error_reporting = E_ALL` - * - * @return bool True if enabled. - */ - public static function isEnabled() { - static $firstCall = true; // Initialized on first call - - if($firstCall && file_exists(PATH_ROOT . 'DEBUG')) { - - $debug_whitelist = trim(file_get_contents(PATH_ROOT . 'DEBUG')); - - self::$enabled = empty($debug_whitelist) || in_array($_SERVER['REMOTE_ADDR'], - explode("\n", str_replace("\r", '', $debug_whitelist) - ) - ); - - if(self::$enabled) { - ini_set('display_errors', '1'); - error_reporting(E_ALL); - - self::$secure = !empty($debug_whitelist); - } - - $firstCall = false; // Skip check on next call - - } - - return self::$enabled; - } - - /** - * Returns true if debug mode is enabled only for specific IP addresses. - * - * Notice: The security flag is set by {@see Debug::isEnabled()}. If this - * function is called before {@see Debug::isEnabled()}, the default value is - * false! - * - * @return bool True if debug mode is secure - */ - public static function isSecure() { - return self::$secure; - } - - /** - * Adds a debug message to error_log if debug mode is enabled - * - * @param string $text The message to add to error_log - */ - public static function log($text) { - if(!self::isEnabled()) { - return; - } - - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); - $calling = end($backtrace); - $message = $calling['file'] . ':' - . $calling['line'] . ' class ' - . (isset($calling['class']) ? $calling['class'] : '<no-class>') . '->' - . $calling['function'] . ' - ' - . $text; - - error_log($message); - } +class Debug +{ + /** + * Indicates if debug mode is enabled. + * + * Do not access this property directly! + * Use {@see Debug::isEnabled()} instead. + * + * @var bool + */ + private static $enabled = false; + + /** + * Indicates if debug mode is secure. + * + * Do not access this property directly! + * Use {@see Debug::isSecure()} instead. + * + * @var bool + */ + private static $secure = false; + + /** + * Returns true if debug mode is enabled + * + * If debug mode is enabled, sets `display_errors = 1` and `error_reporting = E_ALL` + * + * @return bool True if enabled. + */ + public static function isEnabled() + { + static $firstCall = true; // Initialized on first call + + if ($firstCall && file_exists(PATH_ROOT . 'DEBUG')) { + $debug_whitelist = trim(file_get_contents(PATH_ROOT . 'DEBUG')); + + self::$enabled = empty($debug_whitelist) || in_array( + $_SERVER['REMOTE_ADDR'], + explode("\n", str_replace("\r", '', $debug_whitelist)) + ); + + if (self::$enabled) { + ini_set('display_errors', '1'); + error_reporting(E_ALL); + + self::$secure = !empty($debug_whitelist); + } + + $firstCall = false; // Skip check on next call + } + + return self::$enabled; + } + + /** + * Returns true if debug mode is enabled only for specific IP addresses. + * + * Notice: The security flag is set by {@see Debug::isEnabled()}. If this + * function is called before {@see Debug::isEnabled()}, the default value is + * false! + * + * @return bool True if debug mode is secure + */ + public static function isSecure() + { + return self::$secure; + } + + /** + * Adds a debug message to error_log if debug mode is enabled + * + * @param string $text The message to add to error_log + */ + public static function log($text) + { + if (!self::isEnabled()) { + return; + } + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + $calling = end($backtrace); + $message = $calling['file'] . ':' + . $calling['line'] . ' class ' + . (isset($calling['class']) ? $calling['class'] : '<no-class>') . '->' + . $calling['function'] . ' - ' + . $text; + + error_log($message); + } } diff --git a/lib/Exceptions.php b/lib/Exceptions.php index a9d2365b..8cd42de5 100644 --- a/lib/Exceptions.php +++ b/lib/Exceptions.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,18 +7,19 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** * Builds a GitHub search query to find open bugs for the current bridge */ -function buildGitHubSearchQuery($bridgeName){ - return REPOSITORY - . 'issues?q=' - . urlencode('is:issue is:open ' . $bridgeName); +function buildGitHubSearchQuery($bridgeName) +{ + return REPOSITORY + . 'issues?q=' + . urlencode('is:issue is:open ' . $bridgeName); } /** @@ -33,86 +35,87 @@ function buildGitHubSearchQuery($bridgeName){ * * @todo This function belongs inside a class */ -function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null){ - if(!isset($title) || !isset($body) || empty($title) || empty($body)) { - return null; - } - - // Add title and body - $uri = REPOSITORY - . 'issues/new?title=' - . urlencode($title) - . '&body=' - . urlencode($body); - - // Add labels - if(!is_null($labels) && is_array($labels) && count($labels) > 0) { - if(count($lables) === 1) { - $uri .= '&labels=' . urlencode($labels[0]); - } else { - foreach($labels as $label) { - $uri .= '&labels[]=' . urlencode($label); - } - } - } elseif(!is_null($labels) && is_string($labels)) { - $uri .= '&labels=' . urlencode($labels); - } - - // Add maintainer - if(!empty($maintainer)) { - $uri .= '&assignee=' . urlencode($maintainer); - } - - return $uri; +function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null) +{ + if (!isset($title) || !isset($body) || empty($title) || empty($body)) { + return null; + } + + // Add title and body + $uri = REPOSITORY + . 'issues/new?title=' + . urlencode($title) + . '&body=' + . urlencode($body); + + // Add labels + if (!is_null($labels) && is_array($labels) && count($labels) > 0) { + if (count($lables) === 1) { + $uri .= '&labels=' . urlencode($labels[0]); + } else { + foreach ($labels as $label) { + $uri .= '&labels[]=' . urlencode($label); + } + } + } elseif (!is_null($labels) && is_string($labels)) { + $uri .= '&labels=' . urlencode($labels); + } + + // Add maintainer + if (!empty($maintainer)) { + $uri .= '&assignee=' . urlencode($maintainer); + } + + return $uri; } function buildBridgeException(\Throwable $e, BridgeInterface $bridge): string { - $title = $bridge->getName() . ' failed with error ' . $e->getCode(); - - // Build a GitHub compatible message - $body = 'Error message: `' - . $e->getMessage() - . "`\nQuery string: `" - . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '') - . "`\nVersion: `" - . Configuration::getVersion() - . '`'; - - $body_html = nl2br($body); - $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer()); - $searchQuery = buildGitHubSearchQuery($bridge::NAME); - - $header = buildHeader($e, $bridge); - $message = <<<EOD + $title = $bridge->getName() . ' failed with error ' . $e->getCode(); + + // Build a GitHub compatible message + $body = 'Error message: `' + . $e->getMessage() + . "`\nQuery string: `" + . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '') + . "`\nVersion: `" + . Configuration::getVersion() + . '`'; + + $body_html = nl2br($body); + $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer()); + $searchQuery = buildGitHubSearchQuery($bridge::NAME); + + $header = buildHeader($e, $bridge); + $message = <<<EOD <strong>{$bridge->getName()}</strong> was unable to receive or process the remote website's content!<br> {$body_html} EOD; - $section = buildSection($e, $bridge, $message, $link, $searchQuery); + $section = buildSection($e, $bridge, $message, $link, $searchQuery); - return $section; + return $section; } function buildTransformException(\Throwable $e, BridgeInterface $bridge): string { - $title = $bridge->getName() . ' failed with error ' . $e->getCode(); - - // Build a GitHub compatible message - $body = 'Error message: `' - . $e->getMessage() - . "`\nQuery string: `" - . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '') - . '`'; - - $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer()); - $searchQuery = buildGitHubSearchQuery($bridge::NAME); - $header = buildHeader($e, $bridge); - $message = "RSS-Bridge was unable to transform the contents returned by + $title = $bridge->getName() . ' failed with error ' . $e->getCode(); + + // Build a GitHub compatible message + $body = 'Error message: `' + . $e->getMessage() + . "`\nQuery string: `" + . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '') + . '`'; + + $link = buildGitHubIssueQuery($title, $body, 'Bridge-Broken', $bridge->getMaintainer()); + $searchQuery = buildGitHubSearchQuery($bridge::NAME); + $header = buildHeader($e, $bridge); + $message = "RSS-Bridge was unable to transform the contents returned by <strong>{$bridge->getName()}</strong>!"; - $section = buildSection($e, $bridge, $message, $link, $searchQuery); + $section = buildSection($e, $bridge, $message, $link, $searchQuery); - return buildPage($title, $header, $section); + return buildPage($title, $header, $section); } /** @@ -124,8 +127,9 @@ function buildTransformException(\Throwable $e, BridgeInterface $bridge): string * * @todo This function belongs inside a class */ -function buildHeader($e, $bridge){ - return <<<EOD +function buildHeader($e, $bridge) +{ + return <<<EOD <header> <h1>Error {$e->getCode()}</h1> <h2>{$e->getMessage()}</h2> @@ -146,8 +150,9 @@ EOD; * * @todo This function belongs inside a class */ -function buildSection($e, $bridge, $message, $link, $searchQuery){ - return <<<EOD +function buildSection($e, $bridge, $message, $link, $searchQuery) +{ + return <<<EOD <section> <p class="exception-message">{$message}</p> <div class="advice"> @@ -178,8 +183,9 @@ EOD; * * @todo This function belongs inside a class */ -function buildPage($title, $header, $section){ - return <<<EOD +function buildPage($title, $header, $section) +{ + return <<<EOD <!DOCTYPE html> <html lang="en"> <head> diff --git a/lib/FactoryAbstract.php b/lib/FactoryAbstract.php index c91ae2e0..53ffb839 100644 --- a/lib/FactoryAbstract.php +++ b/lib/FactoryAbstract.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,65 +7,67 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** * Abstract class for factories. */ -abstract class FactoryAbstract { - - /** - * Holds the working directory - * - * @var string - */ - private $workingDir = null; +abstract class FactoryAbstract +{ + /** + * Holds the working directory + * + * @var string + */ + private $workingDir = null; - /** - * Set the working directory. - * - * @param string $dir The working directory. - * @return void - */ - public function setWorkingDir($dir) { - $this->workingDir = null; + /** + * Set the working directory. + * + * @param string $dir The working directory. + * @return void + */ + public function setWorkingDir($dir) + { + $this->workingDir = null; - if(!is_string($dir)) { - throw new \InvalidArgumentException('Working directory must be a string!'); - } + if (!is_string($dir)) { + throw new \InvalidArgumentException('Working directory must be a string!'); + } - if(!file_exists($dir)) { - throw new \Exception('Working directory does not exist!'); - } + if (!file_exists($dir)) { + throw new \Exception('Working directory does not exist!'); + } - if(!is_dir($dir)) { - throw new \InvalidArgumentException($dir . ' is not a directory!'); - } + if (!is_dir($dir)) { + throw new \InvalidArgumentException($dir . ' is not a directory!'); + } - $this->workingDir = realpath($dir) . '/'; - } + $this->workingDir = realpath($dir) . '/'; + } - /** - * Get the working directory - * - * @return string The working directory. - */ - public function getWorkingDir() { - if(is_null($this->workingDir)) { - throw new \LogicException('Working directory is not set!'); - } + /** + * Get the working directory + * + * @return string The working directory. + */ + public function getWorkingDir() + { + if (is_null($this->workingDir)) { + throw new \LogicException('Working directory is not set!'); + } - return $this->workingDir; - } + return $this->workingDir; + } - /** - * Creates a new instance for the object specified by name. - * - * @param string $name The name of the object to create. - * @return object The object instance - */ - abstract public function create($name); + /** + * Creates a new instance for the object specified by name. + * + * @param string $name The name of the object to create. + * @return object The object instance + */ + abstract public function create($name); } diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php index b84c608a..b79bf3a8 100644 --- a/lib/FeedExpander.php +++ b/lib/FeedExpander.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -32,406 +33,452 @@ * @todo The parsing functions should all be private. This class is complicated * enough without having to consider children overriding functions. */ -abstract class FeedExpander extends BridgeAbstract { - - /** Indicates an RSS 1.0 feed */ - const FEED_TYPE_RSS_1_0 = 'RSS_1_0'; - - /** Indicates an RSS 2.0 feed */ - const FEED_TYPE_RSS_2_0 = 'RSS_2_0'; - - /** Indicates an Atom 1.0 feed */ - const FEED_TYPE_ATOM_1_0 = 'ATOM_1_0'; - - /** - * Holds the title of the current feed - * - * @var string - */ - private $title; - - /** - * Holds the URI of the feed - * - * @var string - */ - private $uri; - - /** - * Holds the icon of the feed - * - */ - private $icon; - - /** - * Holds the feed type during internal operations. - * - * @var string - */ - private $feedType; - - /** - * Collects data from an existing feed. - * - * Children should call this function in {@see BridgeInterface::collectData()} - * to extract a feed. - * - * @param string $url URL to the feed. - * @param int $maxItems Maximum number of items to collect from the feed - * (`-1`: no limit). - * @return self - */ - public function collectExpandableDatas($url, $maxItems = -1){ - if(empty($url)) { - returnServerError('There is no $url for this RSS expander'); - } - - Debug::log('Loading from ' . $url); - - /* Notice we do not use cache here on purpose: - * we want a fresh view of the RSS stream each time - */ - - $mimeTypes = [ - MrssFormat::MIME_TYPE, - AtomFormat::MIME_TYPE, - '*/*', - ]; - $httpHeaders = ['Accept: ' . implode(', ', $mimeTypes)]; - $content = getContents($url, $httpHeaders) - or returnServerError('Could not request ' . $url); - $rssContent = simplexml_load_string(trim($content)); - - if ($rssContent === false) { - throw new \Exception('Unable to parse string as xml'); - } - - Debug::log('Detecting feed format/version'); - switch(true) { - case isset($rssContent->item[0]): - Debug::log('Detected RSS 1.0 format'); - $this->feedType = self::FEED_TYPE_RSS_1_0; - $this->collectRss1($rssContent, $maxItems); - break; - case isset($rssContent->channel[0]): - Debug::log('Detected RSS 0.9x or 2.0 format'); - $this->feedType = self::FEED_TYPE_RSS_2_0; - $this->collectRss2($rssContent, $maxItems); - break; - case isset($rssContent->entry[0]): - Debug::log('Detected ATOM format'); - $this->feedType = self::FEED_TYPE_ATOM_1_0; - $this->collectAtom1($rssContent, $maxItems); - break; - default: - Debug::log('Unknown feed format/version'); - returnServerError('The feed format is unknown!'); - break; - } - - return $this; - } - - /** - * Collect data from a RSS 1.0 compatible feed - * - * @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0 - * - * @param string $rssContent The RSS content - * @param int $maxItems Maximum number of items to collect from the feed - * (`-1`: no limit). - * @return void - * - * @todo Instead of passing $maxItems to all functions, just add all items - * and remove excessive items later. - */ - protected function collectRss1($rssContent, $maxItems){ - $this->loadRss2Data($rssContent->channel[0]); - foreach($rssContent->item as $item) { - Debug::log('parsing item ' . var_export($item, true)); - $tmp_item = $this->parseItem($item); - if (!empty($tmp_item)) { - $this->items[] = $tmp_item; - } - if($maxItems !== -1 && count($this->items) >= $maxItems) break; - } - } - - /** - * Collect data from a RSS 2.0 compatible feed - * - * @link http://www.rssboard.org/rss-specification RSS 2.0 Specification - * - * @param object $rssContent The RSS content - * @param int $maxItems Maximum number of items to collect from the feed - * (`-1`: no limit). - * @return void - * - * @todo Instead of passing $maxItems to all functions, just add all items - * and remove excessive items later. - */ - protected function collectRss2($rssContent, $maxItems){ - $rssContent = $rssContent->channel[0]; - Debug::log('RSS content is ===========\n' - . var_export($rssContent, true) - . '==========='); - - $this->loadRss2Data($rssContent); - foreach($rssContent->item as $item) { - Debug::log('parsing item ' . var_export($item, true)); - $tmp_item = $this->parseItem($item); - if (!empty($tmp_item)) { - $this->items[] = $tmp_item; - } - if($maxItems !== -1 && count($this->items) >= $maxItems) break; - } - } - - /** - * Collect data from a Atom 1.0 compatible feed - * - * @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format - * - * @param object $content The Atom content - * @param int $maxItems Maximum number of items to collect from the feed - * (`-1`: no limit). - * @return void - * - * @todo Instead of passing $maxItems to all functions, just add all items - * and remove excessive items later. - */ - protected function collectAtom1($content, $maxItems){ - $this->loadAtomData($content); - foreach($content->entry as $item) { - Debug::log('parsing item ' . var_export($item, true)); - $tmp_item = $this->parseItem($item); - if (!empty($tmp_item)) { - $this->items[] = $tmp_item; - } - if($maxItems !== -1 && count($this->items) >= $maxItems) break; - } - } - - /** - * Load RSS 2.0 feed data into RSS-Bridge - * - * @param object $rssContent The RSS content - * @return void - * - * @todo set title, link, description, language, and so on - */ - protected function loadRss2Data($rssContent){ - $this->title = trim((string)$rssContent->title); - $this->uri = trim((string)$rssContent->link); - - if (!empty($rssContent->image)) { - $this->icon = trim((string)$rssContent->image->url); - } - } - - /** - * Load Atom feed data into RSS-Bridge - * - * @param object $content The Atom content - * @return void - */ - protected function loadAtomData($content){ - $this->title = (string)$content->title; - - // Find best link (only one, or first of 'alternate') - if(!isset($content->link)) { - $this->uri = ''; - } elseif (count($content->link) === 1) { - $this->uri = (string)$content->link[0]['href']; - } else { - $this->uri = ''; - foreach($content->link as $link) { - if(strtolower($link['rel']) === 'alternate') { - $this->uri = (string)$link['href']; - break; - } - } - } - - if(!empty($content->icon)) { - $this->icon = (string)$content->icon; - } elseif(!empty($content->logo)) { - $this->icon = (string)$content->logo; - } - } - - /** - * Parse the contents of a single Atom feed item into a RSS-Bridge item for - * further transformation. - * - * @param object $feedItem A single feed item - * @return object The RSS-Bridge item - * - * @todo To reduce confusion, the RSS-Bridge item should maybe have a class - * of its own? - */ - protected function parseATOMItem($feedItem){ - // Some ATOM entries also contain RSS 2.0 fields - $item = $this->parseRss2Item($feedItem); - - if(isset($feedItem->id)) $item['uri'] = (string)$feedItem->id; - if(isset($feedItem->title)) $item['title'] = (string)$feedItem->title; - if(isset($feedItem->updated)) $item['timestamp'] = strtotime((string)$feedItem->updated); - if(isset($feedItem->author)) $item['author'] = (string)$feedItem->author->name; - if(isset($feedItem->content)) $item['content'] = (string)$feedItem->content; - - //When "link" field is present, URL is more reliable than "id" field - if (count($feedItem->link) === 1) { - $item['uri'] = (string)$feedItem->link[0]['href']; - } else { - foreach($feedItem->link as $link) { - if(strtolower($link['rel']) === 'alternate') { - $item['uri'] = (string)$link['href']; - } - if(strtolower($link['rel']) === 'enclosure') { - $item['enclosures'][] = (string)$link['href']; - } - } - } - - return $item; - } - - /** - * Parse the contents of a single RSS 0.91 feed item into a RSS-Bridge item - * for further transformation. - * - * @param object $feedItem A single feed item - * @return object The RSS-Bridge item - * - * @todo To reduce confusion, the RSS-Bridge item should maybe have a class - * of its own? - */ - protected function parseRss091Item($feedItem){ - $item = array(); - if(isset($feedItem->link)) $item['uri'] = (string)$feedItem->link; - if(isset($feedItem->title)) $item['title'] = (string)$feedItem->title; - // rss 0.91 doesn't support timestamps - // rss 0.91 doesn't support authors - // rss 0.91 doesn't support enclosures - if(isset($feedItem->description)) $item['content'] = (string)$feedItem->description; - return $item; - } - - /** - * Parse the contents of a single RSS 1.0 feed item into a RSS-Bridge item - * for further transformation. - * - * @param object $feedItem A single feed item - * @return object The RSS-Bridge item - * - * @todo To reduce confusion, the RSS-Bridge item should maybe have a class - * of its own? - */ - protected function parseRss1Item($feedItem){ - // 1.0 adds optional elements around the 0.91 standard - $item = $this->parseRss091Item($feedItem); - - $namespaces = $feedItem->getNamespaces(true); - if(isset($namespaces['dc'])) { - $dc = $feedItem->children($namespaces['dc']); - if(isset($dc->date)) $item['timestamp'] = strtotime((string)$dc->date); - if(isset($dc->creator)) $item['author'] = (string)$dc->creator; - } - - return $item; - } - - /** - * Parse the contents of a single RSS 2.0 feed item into a RSS-Bridge item - * for further transformation. - * - * @param object $feedItem A single feed item - * @return object The RSS-Bridge item - * - * @todo To reduce confusion, the RSS-Bridge item should maybe have a class - * of its own? - */ - protected function parseRss2Item($feedItem){ - // Primary data is compatible to 0.91 with some additional data - $item = $this->parseRss091Item($feedItem); - - $namespaces = $feedItem->getNamespaces(true); - if(isset($namespaces['dc'])) $dc = $feedItem->children($namespaces['dc']); - if(isset($namespaces['media'])) $media = $feedItem->children($namespaces['media']); - - if(isset($feedItem->guid)) { - foreach($feedItem->guid->attributes() as $attribute => $value) { - if($attribute === 'isPermaLink' - && ($value === 'true' || ( - filter_var($feedItem->guid, FILTER_VALIDATE_URL) - && (empty($item['uri']) || !filter_var($item['uri'], FILTER_VALIDATE_URL)) - ) - ) - ) { - $item['uri'] = (string)$feedItem->guid; - break; - } - } - } - - if(isset($feedItem->pubDate)) { - $item['timestamp'] = strtotime((string)$feedItem->pubDate); - } elseif(isset($dc->date)) { - $item['timestamp'] = strtotime((string)$dc->date); - } - - if(isset($feedItem->author)) { - $item['author'] = (string)$feedItem->author; - } elseif (isset($feedItem->creator)) { - $item['author'] = (string)$feedItem->creator; - } elseif(isset($dc->creator)) { - $item['author'] = (string)$dc->creator; - } elseif(isset($media->credit)) { - $item['author'] = (string)$media->credit; - } - - if(isset($feedItem->enclosure) && !empty($feedItem->enclosure['url'])) { - $item['enclosures'] = array((string)$feedItem->enclosure['url']); - } - - return $item; - } - - /** - * Parse the contents of a single feed item, depending on the current feed - * type, into a RSS-Bridge item. - * - * @param object $item The current feed item - * @return object A RSS-Bridge item, with (hopefully) the whole content - */ - protected function parseItem($item){ - switch($this->feedType) { - case self::FEED_TYPE_RSS_1_0: - return $this->parseRss1Item($item); - break; - case self::FEED_TYPE_RSS_2_0: - return $this->parseRss2Item($item); - break; - case self::FEED_TYPE_ATOM_1_0: - return $this->parseATOMItem($item); - break; - default: returnClientError('Unknown version ' . $this->getInput('version') . '!'); - } - } - - /** {@inheritdoc} */ - public function getURI(){ - return !empty($this->uri) ? $this->uri : parent::getURI(); - } - - /** {@inheritdoc} */ - public function getName(){ - return !empty($this->title) ? $this->title : parent::getName(); - } - - /** {@inheritdoc} */ - public function getIcon(){ - return !empty($this->icon) ? $this->icon : parent::getIcon(); - } +abstract class FeedExpander extends BridgeAbstract +{ + /** Indicates an RSS 1.0 feed */ + const FEED_TYPE_RSS_1_0 = 'RSS_1_0'; + + /** Indicates an RSS 2.0 feed */ + const FEED_TYPE_RSS_2_0 = 'RSS_2_0'; + + /** Indicates an Atom 1.0 feed */ + const FEED_TYPE_ATOM_1_0 = 'ATOM_1_0'; + + /** + * Holds the title of the current feed + * + * @var string + */ + private $title; + + /** + * Holds the URI of the feed + * + * @var string + */ + private $uri; + + /** + * Holds the icon of the feed + * + */ + private $icon; + + /** + * Holds the feed type during internal operations. + * + * @var string + */ + private $feedType; + + /** + * Collects data from an existing feed. + * + * Children should call this function in {@see BridgeInterface::collectData()} + * to extract a feed. + * + * @param string $url URL to the feed. + * @param int $maxItems Maximum number of items to collect from the feed + * (`-1`: no limit). + * @return self + */ + public function collectExpandableDatas($url, $maxItems = -1) + { + if (empty($url)) { + returnServerError('There is no $url for this RSS expander'); + } + + Debug::log('Loading from ' . $url); + + /* Notice we do not use cache here on purpose: + * we want a fresh view of the RSS stream each time + */ + + $mimeTypes = [ + MrssFormat::MIME_TYPE, + AtomFormat::MIME_TYPE, + '*/*', + ]; + $httpHeaders = ['Accept: ' . implode(', ', $mimeTypes)]; + $content = getContents($url, $httpHeaders) + or returnServerError('Could not request ' . $url); + $rssContent = simplexml_load_string(trim($content)); + + if ($rssContent === false) { + throw new \Exception('Unable to parse string as xml'); + } + + Debug::log('Detecting feed format/version'); + switch (true) { + case isset($rssContent->item[0]): + Debug::log('Detected RSS 1.0 format'); + $this->feedType = self::FEED_TYPE_RSS_1_0; + $this->collectRss1($rssContent, $maxItems); + break; + case isset($rssContent->channel[0]): + Debug::log('Detected RSS 0.9x or 2.0 format'); + $this->feedType = self::FEED_TYPE_RSS_2_0; + $this->collectRss2($rssContent, $maxItems); + break; + case isset($rssContent->entry[0]): + Debug::log('Detected ATOM format'); + $this->feedType = self::FEED_TYPE_ATOM_1_0; + $this->collectAtom1($rssContent, $maxItems); + break; + default: + Debug::log('Unknown feed format/version'); + returnServerError('The feed format is unknown!'); + break; + } + + return $this; + } + + /** + * Collect data from a RSS 1.0 compatible feed + * + * @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0 + * + * @param string $rssContent The RSS content + * @param int $maxItems Maximum number of items to collect from the feed + * (`-1`: no limit). + * @return void + * + * @todo Instead of passing $maxItems to all functions, just add all items + * and remove excessive items later. + */ + protected function collectRss1($rssContent, $maxItems) + { + $this->loadRss2Data($rssContent->channel[0]); + foreach ($rssContent->item as $item) { + Debug::log('parsing item ' . var_export($item, true)); + $tmp_item = $this->parseItem($item); + if (!empty($tmp_item)) { + $this->items[] = $tmp_item; + } + if ($maxItems !== -1 && count($this->items) >= $maxItems) { + break; + } + } + } + + /** + * Collect data from a RSS 2.0 compatible feed + * + * @link http://www.rssboard.org/rss-specification RSS 2.0 Specification + * + * @param object $rssContent The RSS content + * @param int $maxItems Maximum number of items to collect from the feed + * (`-1`: no limit). + * @return void + * + * @todo Instead of passing $maxItems to all functions, just add all items + * and remove excessive items later. + */ + protected function collectRss2($rssContent, $maxItems) + { + $rssContent = $rssContent->channel[0]; + Debug::log('RSS content is ===========\n' + . var_export($rssContent, true) + . '==========='); + + $this->loadRss2Data($rssContent); + foreach ($rssContent->item as $item) { + Debug::log('parsing item ' . var_export($item, true)); + $tmp_item = $this->parseItem($item); + if (!empty($tmp_item)) { + $this->items[] = $tmp_item; + } + if ($maxItems !== -1 && count($this->items) >= $maxItems) { + break; + } + } + } + + /** + * Collect data from a Atom 1.0 compatible feed + * + * @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format + * + * @param object $content The Atom content + * @param int $maxItems Maximum number of items to collect from the feed + * (`-1`: no limit). + * @return void + * + * @todo Instead of passing $maxItems to all functions, just add all items + * and remove excessive items later. + */ + protected function collectAtom1($content, $maxItems) + { + $this->loadAtomData($content); + foreach ($content->entry as $item) { + Debug::log('parsing item ' . var_export($item, true)); + $tmp_item = $this->parseItem($item); + if (!empty($tmp_item)) { + $this->items[] = $tmp_item; + } + if ($maxItems !== -1 && count($this->items) >= $maxItems) { + break; + } + } + } + + /** + * Load RSS 2.0 feed data into RSS-Bridge + * + * @param object $rssContent The RSS content + * @return void + * + * @todo set title, link, description, language, and so on + */ + protected function loadRss2Data($rssContent) + { + $this->title = trim((string)$rssContent->title); + $this->uri = trim((string)$rssContent->link); + + if (!empty($rssContent->image)) { + $this->icon = trim((string)$rssContent->image->url); + } + } + + /** + * Load Atom feed data into RSS-Bridge + * + * @param object $content The Atom content + * @return void + */ + protected function loadAtomData($content) + { + $this->title = (string)$content->title; + + // Find best link (only one, or first of 'alternate') + if (!isset($content->link)) { + $this->uri = ''; + } elseif (count($content->link) === 1) { + $this->uri = (string)$content->link[0]['href']; + } else { + $this->uri = ''; + foreach ($content->link as $link) { + if (strtolower($link['rel']) === 'alternate') { + $this->uri = (string)$link['href']; + break; + } + } + } + + if (!empty($content->icon)) { + $this->icon = (string)$content->icon; + } elseif (!empty($content->logo)) { + $this->icon = (string)$content->logo; + } + } + + /** + * Parse the contents of a single Atom feed item into a RSS-Bridge item for + * further transformation. + * + * @param object $feedItem A single feed item + * @return object The RSS-Bridge item + * + * @todo To reduce confusion, the RSS-Bridge item should maybe have a class + * of its own? + */ + protected function parseATOMItem($feedItem) + { + // Some ATOM entries also contain RSS 2.0 fields + $item = $this->parseRss2Item($feedItem); + + if (isset($feedItem->id)) { + $item['uri'] = (string)$feedItem->id; + } + if (isset($feedItem->title)) { + $item['title'] = (string)$feedItem->title; + } + if (isset($feedItem->updated)) { + $item['timestamp'] = strtotime((string)$feedItem->updated); + } + if (isset($feedItem->author)) { + $item['author'] = (string)$feedItem->author->name; + } + if (isset($feedItem->content)) { + $item['content'] = (string)$feedItem->content; + } + + //When "link" field is present, URL is more reliable than "id" field + if (count($feedItem->link) === 1) { + $item['uri'] = (string)$feedItem->link[0]['href']; + } else { + foreach ($feedItem->link as $link) { + if (strtolower($link['rel']) === 'alternate') { + $item['uri'] = (string)$link['href']; + } + if (strtolower($link['rel']) === 'enclosure') { + $item['enclosures'][] = (string)$link['href']; + } + } + } + + return $item; + } + + /** + * Parse the contents of a single RSS 0.91 feed item into a RSS-Bridge item + * for further transformation. + * + * @param object $feedItem A single feed item + * @return object The RSS-Bridge item + * + * @todo To reduce confusion, the RSS-Bridge item should maybe have a class + * of its own? + */ + protected function parseRss091Item($feedItem) + { + $item = []; + if (isset($feedItem->link)) { + $item['uri'] = (string)$feedItem->link; + } + if (isset($feedItem->title)) { + $item['title'] = (string)$feedItem->title; + } + // rss 0.91 doesn't support timestamps + // rss 0.91 doesn't support authors + // rss 0.91 doesn't support enclosures + if (isset($feedItem->description)) { + $item['content'] = (string)$feedItem->description; + } + return $item; + } + + /** + * Parse the contents of a single RSS 1.0 feed item into a RSS-Bridge item + * for further transformation. + * + * @param object $feedItem A single feed item + * @return object The RSS-Bridge item + * + * @todo To reduce confusion, the RSS-Bridge item should maybe have a class + * of its own? + */ + protected function parseRss1Item($feedItem) + { + // 1.0 adds optional elements around the 0.91 standard + $item = $this->parseRss091Item($feedItem); + + $namespaces = $feedItem->getNamespaces(true); + if (isset($namespaces['dc'])) { + $dc = $feedItem->children($namespaces['dc']); + if (isset($dc->date)) { + $item['timestamp'] = strtotime((string)$dc->date); + } + if (isset($dc->creator)) { + $item['author'] = (string)$dc->creator; + } + } + + return $item; + } + + /** + * Parse the contents of a single RSS 2.0 feed item into a RSS-Bridge item + * for further transformation. + * + * @param object $feedItem A single feed item + * @return object The RSS-Bridge item + * + * @todo To reduce confusion, the RSS-Bridge item should maybe have a class + * of its own? + */ + protected function parseRss2Item($feedItem) + { + // Primary data is compatible to 0.91 with some additional data + $item = $this->parseRss091Item($feedItem); + + $namespaces = $feedItem->getNamespaces(true); + if (isset($namespaces['dc'])) { + $dc = $feedItem->children($namespaces['dc']); + } + if (isset($namespaces['media'])) { + $media = $feedItem->children($namespaces['media']); + } + + if (isset($feedItem->guid)) { + foreach ($feedItem->guid->attributes() as $attribute => $value) { + if ( + $attribute === 'isPermaLink' + && ($value === 'true' || ( + filter_var($feedItem->guid, FILTER_VALIDATE_URL) + && (empty($item['uri']) || !filter_var($item['uri'], FILTER_VALIDATE_URL)) + ) + ) + ) { + $item['uri'] = (string)$feedItem->guid; + break; + } + } + } + + if (isset($feedItem->pubDate)) { + $item['timestamp'] = strtotime((string)$feedItem->pubDate); + } elseif (isset($dc->date)) { + $item['timestamp'] = strtotime((string)$dc->date); + } + + if (isset($feedItem->author)) { + $item['author'] = (string)$feedItem->author; + } elseif (isset($feedItem->creator)) { + $item['author'] = (string)$feedItem->creator; + } elseif (isset($dc->creator)) { + $item['author'] = (string)$dc->creator; + } elseif (isset($media->credit)) { + $item['author'] = (string)$media->credit; + } + + if (isset($feedItem->enclosure) && !empty($feedItem->enclosure['url'])) { + $item['enclosures'] = [(string)$feedItem->enclosure['url']]; + } + + return $item; + } + + /** + * Parse the contents of a single feed item, depending on the current feed + * type, into a RSS-Bridge item. + * + * @param object $item The current feed item + * @return object A RSS-Bridge item, with (hopefully) the whole content + */ + protected function parseItem($item) + { + switch ($this->feedType) { + case self::FEED_TYPE_RSS_1_0: + return $this->parseRss1Item($item); + break; + case self::FEED_TYPE_RSS_2_0: + return $this->parseRss2Item($item); + break; + case self::FEED_TYPE_ATOM_1_0: + return $this->parseATOMItem($item); + break; + default: + returnClientError('Unknown version ' . $this->getInput('version') . '!'); + } + } + + /** {@inheritdoc} */ + public function getURI() + { + return !empty($this->uri) ? $this->uri : parent::getURI(); + } + + /** {@inheritdoc} */ + public function getName() + { + return !empty($this->title) ? $this->title : parent::getName(); + } + + /** {@inheritdoc} */ + public function getIcon() + { + return !empty($this->icon) ? $this->icon : parent::getIcon(); + } } diff --git a/lib/FeedItem.php b/lib/FeedItem.php index 8690eb95..2d3872f2 100644 --- a/lib/FeedItem.php +++ b/lib/FeedItem.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -33,493 +34,554 @@ * (i.e. `$feedItem = \FeedItem($item);`). Support for legacy items may be removed * in future versions of RSS-Bridge. */ -class FeedItem { - /** @var string|null URI to the full article */ - protected $uri = null; - - /** @var string|null Title of the item */ - protected $title = null; - - /** @var int|null Timestamp of when the item was first released */ - protected $timestamp = null; - - /** @var string|null Name of the author */ - protected $author = null; - - /** @var string|null Body of the feed */ - protected $content = null; - - /** @var array List of links to media objects */ - protected $enclosures = array(); - - /** @var array List of category names or tags */ - protected $categories = array(); - - /** @var string Unique ID for the current item */ - protected $uid = null; - - /** @var array Associative list of additional parameters */ - protected $misc = array(); // Custom parameters - - /** - * Create object from legacy item. - * - * The provided array must be an associative array of key-value-pairs, where - * keys may correspond to any of the properties of this class. - * - * Example use: - * - * ```PHP - * <?php - * $item = array(); - * - * $item['uri'] = 'https://www.github.com/rss-bridge/rss-bridge/'; - * $item['title'] = 'Title'; - * $item['timestamp'] = strtotime('now'); - * $item['author'] = 'Unknown author'; - * $item['content'] = 'Hello World!'; - * $item['enclosures'] = array('https://github.com/favicon.ico'); - * $item['categories'] = array('php', 'rss-bridge', 'awesome'); - * - * $feedItem = new \FeedItem($item); - * - * ``` - * - * The result of the code above is the same as the code below: - * - * ```PHP - * <?php - * $feedItem = \FeedItem(); - * - * $feedItem->uri = 'https://www.github.com/rss-bridge/rss-bridge/'; - * $feedItem->title = 'Title'; - * $feedItem->timestamp = strtotime('now'); - * $feedItem->autor = 'Unknown author'; - * $feedItem->content = 'Hello World!'; - * $feedItem->enclosures = array('https://github.com/favicon.ico'); - * $feedItem->categories = array('php', 'rss-bridge', 'awesome'); - * ``` - * - * @param array $item (optional) A legacy item (empty: no legacy support). - * @return object A new object of this class - */ - public function __construct($item = array()) { - if(!is_array($item)) - Debug::log('Item must be an array!'); - - foreach($item as $key => $value) { - $this->__set($key, $value); - } - } - - /** - * Get current URI. - * - * Use {@see FeedItem::setURI()} to set the URI. - * - * @return string|null The URI or null if it hasn't been set. - */ - public function getURI() { - return $this->uri; - } - - /** - * Set URI to the full article. - * - * Use {@see FeedItem::getURI()} to get the URI. - * - * _Note_: Removes whitespace from the beginning and end of the URI. - * - * _Remarks_: Uses the attribute "href" or "src" if the provided URI is an - * object of simple_html_dom_node. - * - * @param object|string $uri URI to the full article. - * @return self - */ - public function setURI($uri) { - $this->uri = null; // Clear previous data - - if($uri instanceof simple_html_dom_node) { - if($uri->hasAttribute('href')) { // Anchor - $uri = $uri->href; - } elseif($uri->hasAttribute('src')) { // Image - $uri = $uri->src; - } else { - Debug::log('The item provided as URI is unknown!'); - } - } - - if(!is_string($uri)) { - Debug::log('URI must be a string!'); - } elseif(!filter_var( - $uri, - FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) { - Debug::log('URI must include a scheme, host and path!'); - } else { - $scheme = parse_url($uri, PHP_URL_SCHEME); - - if($scheme !== 'http' && $scheme !== 'https') { - Debug::log('URI scheme must be "http" or "https"!'); - } else { - $this->uri = trim($uri); - } - } - - return $this; - } - - /** - * Get current title. - * - * Use {@see FeedItem::setTitle()} to set the title. - * - * @return string|null The current title or null if it hasn't been set. - */ - public function getTitle() { - return $this->title; - } - - /** - * Set title. - * - * Use {@see FeedItem::getTitle()} to get the title. - * - * _Note_: Removes whitespace from beginning and end of the title. - * - * @param string $title The title - * @return self - */ - public function setTitle($title) { - $this->title = null; // Clear previous data - - if(!is_string($title)) { - Debug::log('Title must be a string!'); - } else { - $this->title = trim($title); - } - - return $this; - } - - /** - * Get current timestamp. - * - * Use {@see FeedItem::setTimestamp()} to set the timestamp. - * - * @return int|null The current timestamp or null if it hasn't been set. - */ - public function getTimestamp() { - return $this->timestamp; - } - - /** - * Set timestamp of first release. - * - * _Note_: The timestamp should represent the number of seconds since - * January 1 1970 00:00:00 GMT (Unix time). - * - * _Remarks_: If the provided timestamp is a string (not numeric), this - * function automatically attempts to parse the string using - * [strtotime](http://php.net/manual/en/function.strtotime.php) - * - * @link http://php.net/manual/en/function.strtotime.php strtotime (PHP) - * @link https://en.wikipedia.org/wiki/Unix_time Unix time (Wikipedia) - * - * @param string|int $timestamp A timestamp of when the item was first released - * @return self - */ - public function setTimestamp($timestamp) { - $this->timestamp = null; // Clear previous data - - if(!is_numeric($timestamp) - && !$timestamp = strtotime($timestamp)) { - Debug::log('Unable to parse timestamp!'); - } - - if($timestamp <= 0) { - Debug::log('Timestamp must be greater than zero!'); - } else { - $this->timestamp = $timestamp; - } - - return $this; - } - - /** - * Get the current author name. - * - * Use {@see FeedItem::setAuthor()} to set the author. - * - * @return string|null The author or null if it hasn't been set. - */ - public function getAuthor() { - return $this->author; - } - - /** - * Set the author name. - * - * Use {@see FeedItem::getAuthor()} to get the author. - * - * @param string $author The author name. - * @return self - */ - public function setAuthor($author) { - $this->author = null; // Clear previous data - - if(!is_string($author)) { - Debug::log('Author must be a string!'); - } else { - $this->author = $author; - } - - return $this; - } - - /** - * Get item content. - * - * Use {@see FeedItem::setContent()} to set the item content. - * - * @return string|null The item content or null if it hasn't been set. - */ - public function getContent() { - return $this->content; - } - - /** - * Set item content. - * - * Note: This function casts objects of type simple_html_dom and - * simple_html_dom_node to string. - * - * Use {@see FeedItem::getContent()} to get the current item content. - * - * @param string|object $content The item content as text or simple_html_dom - * object. - * @return self - */ - public function setContent($content) { - $this->content = null; // Clear previous data - - if($content instanceof simple_html_dom - || $content instanceof simple_html_dom_node) { - $content = (string)$content; - } - - if(!is_string($content)) { - Debug::log('Content must be a string!'); - } else { - $this->content = $content; - } - - return $this; - } - - /** - * Get item enclosures. - * - * Use {@see FeedItem::setEnclosures()} to set feed enclosures. - * - * @return array Enclosures as array of enclosure URIs. - */ - public function getEnclosures() { - return $this->enclosures; - } - - /** - * Set item enclosures. - * - * Use {@see FeedItem::getEnclosures()} to get the current item enclosures. - * - * @param array $enclosures Array of enclosures, where each element links to - * one enclosure. - * @return self - */ - public function setEnclosures($enclosures) { - $this->enclosures = array(); // Clear previous data - - if(!is_array($enclosures)) { - Debug::log('Enclosures must be an array!'); - } else { - foreach($enclosures as $enclosure) { - if(!filter_var( - $enclosure, - FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED)) { - Debug::log('Each enclosure must contain a scheme, host and path!'); - } elseif(!in_array($enclosure, $this->enclosures)) { - $this->enclosures[] = $enclosure; - } - } - } - - return $this; - } - - /** - * Get item categories. - * - * Use {@see FeedItem::setCategories()} to set item categories. - * - * @param array The item categories. - */ - public function getCategories() { - return $this->categories; - } - - /** - * Set item categories. - * - * Use {@see FeedItem::getCategories()} to get the current item categories. - * - * @param array $categories Array of categories, where each element defines - * a single category name. - * @return self - */ - public function setCategories($categories) { - $this->categories = array(); // Clear previous data - - if(!is_array($categories)) { - Debug::log('Categories must be an array!'); - } else { - foreach($categories as $category) { - if(!is_string($category)) { - Debug::log('Category must be a string!'); - } else { - $this->categories[] = $category; - } - } - } - - return $this; - } - - /** - * Get unique id - * - * Use {@see FeedItem::setUid()} to set the unique id. - * - * @param string The unique id. - */ - public function getUid() { - return $this->uid; - } - - /** - * Set unique id. - * - * Use {@see FeedItem::getUid()} to get the unique id. - * - * @param string $uid A string that uniquely identifies the current item - * @return self - */ - public function setUid($uid) { - $this->uid = null; // Clear previous data - - if(!is_string($uid)) { - Debug::log('Unique id must be a string!'); - } elseif (preg_match('/^[a-f0-9]{40}$/', $uid)) { - // keep id if it already is a SHA-1 hash - $this->uid = $uid; - } else { - $this->uid = sha1($uid); - } - - return $this; - } - - /** - * Add miscellaneous elements to the item. - * - * @param string $key Name of the element. - * @param mixed $value Value of the element. - * @return self - */ - public function addMisc($key, $value) { - - if(!is_string($key)) { - Debug::log('Key must be a string!'); - } elseif(in_array($key, get_object_vars($this))) { - Debug::log('Key must be unique!'); - } else { - $this->misc[$key] = $value; - } - - return $this; - } - - /** - * Transform current object to array - * - * @return array - */ - public function toArray() { - return array_merge( - array( - 'uri' => $this->uri, - 'title' => $this->title, - 'timestamp' => $this->timestamp, - 'author' => $this->author, - 'content' => $this->content, - 'enclosures' => $this->enclosures, - 'categories' => $this->categories, - 'uid' => $this->uid, - ), $this->misc - ); - } - - /** - * Set item property - * - * Allows simple assignment to parameters. This method is slower, but easier - * to implement in some cases: - * - * ```PHP - * $item = new \FeedItem(); - * $item->content = 'Hello World!'; - * $item->my_id = 42; - * ``` - * - * @param string $name Property name - * @param mixed $value Property value - */ - public function __set($name, $value) { - switch($name) { - case 'uri': $this->setURI($value); break; - case 'title': $this->setTitle($value); break; - case 'timestamp': $this->setTimestamp($value); break; - case 'author': $this->setAuthor($value); break; - case 'content': $this->setContent($value); break; - case 'enclosures': $this->setEnclosures($value); break; - case 'categories': $this->setCategories($value); break; - case 'uid': $this->setUid($value); break; - default: $this->addMisc($name, $value); - } - } - - /** - * Get item property - * - * Allows simple assignment to parameters. This method is slower, but easier - * to implement in some cases. - * - * @param string $name Property name - * @return mixed Property value - */ - public function __get($name) { - switch($name) { - case 'uri': return $this->getURI(); - case 'title': return $this->getTitle(); - case 'timestamp': return $this->getTimestamp(); - case 'author': return $this->getAuthor(); - case 'content': return $this->getContent(); - case 'enclosures': return $this->getEnclosures(); - case 'categories': return $this->getCategories(); - case 'uid': return $this->getUid(); - default: - if(array_key_exists($name, $this->misc)) - return $this->misc[$name]; - return null; - } - } +class FeedItem +{ + /** @var string|null URI to the full article */ + protected $uri = null; + + /** @var string|null Title of the item */ + protected $title = null; + + /** @var int|null Timestamp of when the item was first released */ + protected $timestamp = null; + + /** @var string|null Name of the author */ + protected $author = null; + + /** @var string|null Body of the feed */ + protected $content = null; + + /** @var array List of links to media objects */ + protected $enclosures = []; + + /** @var array List of category names or tags */ + protected $categories = []; + + /** @var string Unique ID for the current item */ + protected $uid = null; + + /** @var array Associative list of additional parameters */ + protected $misc = []; // Custom parameters + + /** + * Create object from legacy item. + * + * The provided array must be an associative array of key-value-pairs, where + * keys may correspond to any of the properties of this class. + * + * Example use: + * + * ```PHP + * <?php + * $item = array(); + * + * $item['uri'] = 'https://www.github.com/rss-bridge/rss-bridge/'; + * $item['title'] = 'Title'; + * $item['timestamp'] = strtotime('now'); + * $item['author'] = 'Unknown author'; + * $item['content'] = 'Hello World!'; + * $item['enclosures'] = array('https://github.com/favicon.ico'); + * $item['categories'] = array('php', 'rss-bridge', 'awesome'); + * + * $feedItem = new \FeedItem($item); + * + * ``` + * + * The result of the code above is the same as the code below: + * + * ```PHP + * <?php + * $feedItem = \FeedItem(); + * + * $feedItem->uri = 'https://www.github.com/rss-bridge/rss-bridge/'; + * $feedItem->title = 'Title'; + * $feedItem->timestamp = strtotime('now'); + * $feedItem->autor = 'Unknown author'; + * $feedItem->content = 'Hello World!'; + * $feedItem->enclosures = array('https://github.com/favicon.ico'); + * $feedItem->categories = array('php', 'rss-bridge', 'awesome'); + * ``` + * + * @param array $item (optional) A legacy item (empty: no legacy support). + * @return object A new object of this class + */ + public function __construct($item = []) + { + if (!is_array($item)) { + Debug::log('Item must be an array!'); + } + + foreach ($item as $key => $value) { + $this->__set($key, $value); + } + } + + /** + * Get current URI. + * + * Use {@see FeedItem::setURI()} to set the URI. + * + * @return string|null The URI or null if it hasn't been set. + */ + public function getURI() + { + return $this->uri; + } + + /** + * Set URI to the full article. + * + * Use {@see FeedItem::getURI()} to get the URI. + * + * _Note_: Removes whitespace from the beginning and end of the URI. + * + * _Remarks_: Uses the attribute "href" or "src" if the provided URI is an + * object of simple_html_dom_node. + * + * @param object|string $uri URI to the full article. + * @return self + */ + public function setURI($uri) + { + $this->uri = null; // Clear previous data + + if ($uri instanceof simple_html_dom_node) { + if ($uri->hasAttribute('href')) { // Anchor + $uri = $uri->href; + } elseif ($uri->hasAttribute('src')) { // Image + $uri = $uri->src; + } else { + Debug::log('The item provided as URI is unknown!'); + } + } + + if (!is_string($uri)) { + Debug::log('URI must be a string!'); + } elseif ( + !filter_var( + $uri, + FILTER_VALIDATE_URL, + FILTER_FLAG_PATH_REQUIRED + ) + ) { + Debug::log('URI must include a scheme, host and path!'); + } else { + $scheme = parse_url($uri, PHP_URL_SCHEME); + + if ($scheme !== 'http' && $scheme !== 'https') { + Debug::log('URI scheme must be "http" or "https"!'); + } else { + $this->uri = trim($uri); + } + } + + return $this; + } + + /** + * Get current title. + * + * Use {@see FeedItem::setTitle()} to set the title. + * + * @return string|null The current title or null if it hasn't been set. + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set title. + * + * Use {@see FeedItem::getTitle()} to get the title. + * + * _Note_: Removes whitespace from beginning and end of the title. + * + * @param string $title The title + * @return self + */ + public function setTitle($title) + { + $this->title = null; // Clear previous data + + if (!is_string($title)) { + Debug::log('Title must be a string!'); + } else { + $this->title = trim($title); + } + + return $this; + } + + /** + * Get current timestamp. + * + * Use {@see FeedItem::setTimestamp()} to set the timestamp. + * + * @return int|null The current timestamp or null if it hasn't been set. + */ + public function getTimestamp() + { + return $this->timestamp; + } + + /** + * Set timestamp of first release. + * + * _Note_: The timestamp should represent the number of seconds since + * January 1 1970 00:00:00 GMT (Unix time). + * + * _Remarks_: If the provided timestamp is a string (not numeric), this + * function automatically attempts to parse the string using + * [strtotime](http://php.net/manual/en/function.strtotime.php) + * + * @link http://php.net/manual/en/function.strtotime.php strtotime (PHP) + * @link https://en.wikipedia.org/wiki/Unix_time Unix time (Wikipedia) + * + * @param string|int $timestamp A timestamp of when the item was first released + * @return self + */ + public function setTimestamp($timestamp) + { + $this->timestamp = null; // Clear previous data + + if ( + !is_numeric($timestamp) + && !$timestamp = strtotime($timestamp) + ) { + Debug::log('Unable to parse timestamp!'); + } + + if ($timestamp <= 0) { + Debug::log('Timestamp must be greater than zero!'); + } else { + $this->timestamp = $timestamp; + } + + return $this; + } + + /** + * Get the current author name. + * + * Use {@see FeedItem::setAuthor()} to set the author. + * + * @return string|null The author or null if it hasn't been set. + */ + public function getAuthor() + { + return $this->author; + } + + /** + * Set the author name. + * + * Use {@see FeedItem::getAuthor()} to get the author. + * + * @param string $author The author name. + * @return self + */ + public function setAuthor($author) + { + $this->author = null; // Clear previous data + + if (!is_string($author)) { + Debug::log('Author must be a string!'); + } else { + $this->author = $author; + } + + return $this; + } + + /** + * Get item content. + * + * Use {@see FeedItem::setContent()} to set the item content. + * + * @return string|null The item content or null if it hasn't been set. + */ + public function getContent() + { + return $this->content; + } + + /** + * Set item content. + * + * Note: This function casts objects of type simple_html_dom and + * simple_html_dom_node to string. + * + * Use {@see FeedItem::getContent()} to get the current item content. + * + * @param string|object $content The item content as text or simple_html_dom + * object. + * @return self + */ + public function setContent($content) + { + $this->content = null; // Clear previous data + + if ( + $content instanceof simple_html_dom + || $content instanceof simple_html_dom_node + ) { + $content = (string)$content; + } + + if (!is_string($content)) { + Debug::log('Content must be a string!'); + } else { + $this->content = $content; + } + + return $this; + } + + /** + * Get item enclosures. + * + * Use {@see FeedItem::setEnclosures()} to set feed enclosures. + * + * @return array Enclosures as array of enclosure URIs. + */ + public function getEnclosures() + { + return $this->enclosures; + } + + /** + * Set item enclosures. + * + * Use {@see FeedItem::getEnclosures()} to get the current item enclosures. + * + * @param array $enclosures Array of enclosures, where each element links to + * one enclosure. + * @return self + */ + public function setEnclosures($enclosures) + { + $this->enclosures = []; // Clear previous data + + if (!is_array($enclosures)) { + Debug::log('Enclosures must be an array!'); + } else { + foreach ($enclosures as $enclosure) { + if ( + !filter_var( + $enclosure, + FILTER_VALIDATE_URL, + FILTER_FLAG_PATH_REQUIRED + ) + ) { + Debug::log('Each enclosure must contain a scheme, host and path!'); + } elseif (!in_array($enclosure, $this->enclosures)) { + $this->enclosures[] = $enclosure; + } + } + } + + return $this; + } + + /** + * Get item categories. + * + * Use {@see FeedItem::setCategories()} to set item categories. + * + * @param array The item categories. + */ + public function getCategories() + { + return $this->categories; + } + + /** + * Set item categories. + * + * Use {@see FeedItem::getCategories()} to get the current item categories. + * + * @param array $categories Array of categories, where each element defines + * a single category name. + * @return self + */ + public function setCategories($categories) + { + $this->categories = []; // Clear previous data + + if (!is_array($categories)) { + Debug::log('Categories must be an array!'); + } else { + foreach ($categories as $category) { + if (!is_string($category)) { + Debug::log('Category must be a string!'); + } else { + $this->categories[] = $category; + } + } + } + + return $this; + } + + /** + * Get unique id + * + * Use {@see FeedItem::setUid()} to set the unique id. + * + * @param string The unique id. + */ + public function getUid() + { + return $this->uid; + } + + /** + * Set unique id. + * + * Use {@see FeedItem::getUid()} to get the unique id. + * + * @param string $uid A string that uniquely identifies the current item + * @return self + */ + public function setUid($uid) + { + $this->uid = null; // Clear previous data + + if (!is_string($uid)) { + Debug::log('Unique id must be a string!'); + } elseif (preg_match('/^[a-f0-9]{40}$/', $uid)) { + // keep id if it already is a SHA-1 hash + $this->uid = $uid; + } else { + $this->uid = sha1($uid); + } + + return $this; + } + + /** + * Add miscellaneous elements to the item. + * + * @param string $key Name of the element. + * @param mixed $value Value of the element. + * @return self + */ + public function addMisc($key, $value) + { + if (!is_string($key)) { + Debug::log('Key must be a string!'); + } elseif (in_array($key, get_object_vars($this))) { + Debug::log('Key must be unique!'); + } else { + $this->misc[$key] = $value; + } + + return $this; + } + + /** + * Transform current object to array + * + * @return array + */ + public function toArray() + { + return array_merge( + [ + 'uri' => $this->uri, + 'title' => $this->title, + 'timestamp' => $this->timestamp, + 'author' => $this->author, + 'content' => $this->content, + 'enclosures' => $this->enclosures, + 'categories' => $this->categories, + 'uid' => $this->uid, + ], + $this->misc + ); + } + + /** + * Set item property + * + * Allows simple assignment to parameters. This method is slower, but easier + * to implement in some cases: + * + * ```PHP + * $item = new \FeedItem(); + * $item->content = 'Hello World!'; + * $item->my_id = 42; + * ``` + * + * @param string $name Property name + * @param mixed $value Property value + */ + public function __set($name, $value) + { + switch ($name) { + case 'uri': + $this->setURI($value); + break; + case 'title': + $this->setTitle($value); + break; + case 'timestamp': + $this->setTimestamp($value); + break; + case 'author': + $this->setAuthor($value); + break; + case 'content': + $this->setContent($value); + break; + case 'enclosures': + $this->setEnclosures($value); + break; + case 'categories': + $this->setCategories($value); + break; + case 'uid': + $this->setUid($value); + break; + default: + $this->addMisc($name, $value); + } + } + + /** + * Get item property + * + * Allows simple assignment to parameters. This method is slower, but easier + * to implement in some cases. + * + * @param string $name Property name + * @return mixed Property value + */ + public function __get($name) + { + switch ($name) { + case 'uri': + return $this->getURI(); + case 'title': + return $this->getTitle(); + case 'timestamp': + return $this->getTimestamp(); + case 'author': + return $this->getAuthor(); + case 'content': + return $this->getContent(); + case 'enclosures': + return $this->getEnclosures(); + case 'categories': + return $this->getCategories(); + case 'uid': + return $this->getUid(); + default: + if (array_key_exists($name, $this->misc)) { + return $this->misc[$name]; + } + return null; + } + } } diff --git a/lib/FormatAbstract.php b/lib/FormatAbstract.php index 768b0157..7a4c6c92 100644 --- a/lib/FormatAbstract.php +++ b/lib/FormatAbstract.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license https://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license https://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -16,126 +17,135 @@ * * This class implements {@see FormatInterface} */ -abstract class FormatAbstract implements FormatInterface { - - /** The default charset (UTF-8) */ - const DEFAULT_CHARSET = 'UTF-8'; - - /** MIME type of format output */ - const MIME_TYPE = 'text/plain'; - - /** @var string $charset The charset */ - protected $charset; - - /** @var array $items The items */ - protected $items; - - /** - * @var int $lastModified A timestamp to indicate the last modified time of - * the output data. - */ - protected $lastModified; - - /** @var array $extraInfos The extra infos */ - protected $extraInfos; - - /** {@inheritdoc} */ - public function getMimeType(){ - return static::MIME_TYPE; - } - - /** - * {@inheritdoc} - * - * @param string $charset {@inheritdoc} - */ - public function setCharset($charset){ - $this->charset = $charset; - - return $this; - } - - /** {@inheritdoc} */ - public function getCharset(){ - $charset = $this->charset; - - return is_null($charset) ? static::DEFAULT_CHARSET : $charset; - } - - /** - * Set the last modified time - * - * @param int $lastModified The last modified time - * @return void - */ - public function setLastModified($lastModified){ - $this->lastModified = $lastModified; - } - - /** - * {@inheritdoc} - * - * @param array $items {@inheritdoc} - */ - public function setItems(array $items){ - $this->items = $items; - - return $this; - } - - /** {@inheritdoc} */ - public function getItems(){ - if(!is_array($this->items)) - throw new \LogicException('Feed the ' . get_class($this) . ' with "setItems" method before !'); - - return $this->items; - } - - /** - * {@inheritdoc} - * - * @param array $extraInfos {@inheritdoc} - */ - public function setExtraInfos(array $extraInfos = array()){ - foreach(array('name', 'uri', 'icon', 'donationUri') as $infoName) { - if(!isset($extraInfos[$infoName])) { - $extraInfos[$infoName] = ''; - } - } - - $this->extraInfos = $extraInfos; - - return $this; - } - - /** {@inheritdoc} */ - public function getExtraInfos(){ - if(is_null($this->extraInfos)) { // No extra info ? - $this->setExtraInfos(); // Define with default value - } - - return $this->extraInfos; - } - - /** - * Sanitize HTML while leaving it functional. - * - * Keeps HTML as-is (with clickable hyperlinks) while reducing annoying and - * potentially dangerous things. - * - * @param string $html The HTML content - * @return string The sanitized HTML content - * - * @todo This belongs into `html.php` - * @todo Maybe switch to http://htmlpurifier.org/ - * @todo Maybe switch to http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php - */ - protected function sanitizeHtml(string $html): string - { - $html = str_replace('<script', '<‌script', $html); // Disable scripts, but leave them visible. - $html = str_replace('<iframe', '<‌iframe', $html); - $html = str_replace('<link', '<‌link', $html); - // We leave alone object and embed so that videos can play in RSS readers. - return $html; - } +abstract class FormatAbstract implements FormatInterface +{ + /** The default charset (UTF-8) */ + const DEFAULT_CHARSET = 'UTF-8'; + + /** MIME type of format output */ + const MIME_TYPE = 'text/plain'; + + /** @var string $charset The charset */ + protected $charset; + + /** @var array $items The items */ + protected $items; + + /** + * @var int $lastModified A timestamp to indicate the last modified time of + * the output data. + */ + protected $lastModified; + + /** @var array $extraInfos The extra infos */ + protected $extraInfos; + + /** {@inheritdoc} */ + public function getMimeType() + { + return static::MIME_TYPE; + } + + /** + * {@inheritdoc} + * + * @param string $charset {@inheritdoc} + */ + public function setCharset($charset) + { + $this->charset = $charset; + + return $this; + } + + /** {@inheritdoc} */ + public function getCharset() + { + $charset = $this->charset; + + return is_null($charset) ? static::DEFAULT_CHARSET : $charset; + } + + /** + * Set the last modified time + * + * @param int $lastModified The last modified time + * @return void + */ + public function setLastModified($lastModified) + { + $this->lastModified = $lastModified; + } + + /** + * {@inheritdoc} + * + * @param array $items {@inheritdoc} + */ + public function setItems(array $items) + { + $this->items = $items; + + return $this; + } + + /** {@inheritdoc} */ + public function getItems() + { + if (!is_array($this->items)) { + throw new \LogicException('Feed the ' . get_class($this) . ' with "setItems" method before !'); + } + + return $this->items; + } + + /** + * {@inheritdoc} + * + * @param array $extraInfos {@inheritdoc} + */ + public function setExtraInfos(array $extraInfos = []) + { + foreach (['name', 'uri', 'icon', 'donationUri'] as $infoName) { + if (!isset($extraInfos[$infoName])) { + $extraInfos[$infoName] = ''; + } + } + + $this->extraInfos = $extraInfos; + + return $this; + } + + /** {@inheritdoc} */ + public function getExtraInfos() + { + if (is_null($this->extraInfos)) { // No extra info ? + $this->setExtraInfos(); // Define with default value + } + + return $this->extraInfos; + } + + /** + * Sanitize HTML while leaving it functional. + * + * Keeps HTML as-is (with clickable hyperlinks) while reducing annoying and + * potentially dangerous things. + * + * @param string $html The HTML content + * @return string The sanitized HTML content + * + * @todo This belongs into `html.php` + * @todo Maybe switch to http://htmlpurifier.org/ + * @todo Maybe switch to http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php + */ + protected function sanitizeHtml(string $html): string + { + $html = str_replace('<script', '<‌script', $html); // Disable scripts, but leave them visible. + $html = str_replace('<iframe', '<‌iframe', $html); + $html = str_replace('<link', '<‌link', $html); + // We leave alone object and embed so that videos can play in RSS readers. + return $html; + } } diff --git a/lib/FormatFactory.php b/lib/FormatFactory.php index 2044a899..e2ef52fa 100644 --- a/lib/FormatFactory.php +++ b/lib/FormatFactory.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,65 +7,66 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ class FormatFactory { - private $folder; - private $formatNames; + private $folder; + private $formatNames; - public function __construct(string $folder = PATH_LIB_FORMATS) - { - $this->folder = $folder; + public function __construct(string $folder = PATH_LIB_FORMATS) + { + $this->folder = $folder; - // create format names - foreach(scandir($this->folder) as $file) { - if(preg_match('/^([^.]+)Format\.php$/U', $file, $m)) { - $this->formatNames[] = $m[1]; - } - } - } + // create format names + foreach (scandir($this->folder) as $file) { + if (preg_match('/^([^.]+)Format\.php$/U', $file, $m)) { + $this->formatNames[] = $m[1]; + } + } + } - /** - * @throws \InvalidArgumentException - * @param string $name The name of the format e.g. "Atom", "Mrss" or "Json" - */ - public function create(string $name): FormatInterface - { - if (! preg_match('/^[a-zA-Z0-9-]*$/', $name)) { - throw new \InvalidArgumentException('Format name invalid!'); - } - $name = $this->sanitizeFormatName($name); - if ($name === null) { - throw new \InvalidArgumentException('Unknown format given!'); - } - $className = '\\' . $name . 'Format'; - return new $className; - } + /** + * @throws \InvalidArgumentException + * @param string $name The name of the format e.g. "Atom", "Mrss" or "Json" + */ + public function create(string $name): FormatInterface + { + if (! preg_match('/^[a-zA-Z0-9-]*$/', $name)) { + throw new \InvalidArgumentException('Format name invalid!'); + } + $name = $this->sanitizeFormatName($name); + if ($name === null) { + throw new \InvalidArgumentException('Unknown format given!'); + } + $className = '\\' . $name . 'Format'; + return new $className(); + } - public function getFormatNames(): array - { - return $this->formatNames; - } + public function getFormatNames(): array + { + return $this->formatNames; + } - protected function sanitizeFormatName(string $name) { - $name = ucfirst(strtolower($name)); + protected function sanitizeFormatName(string $name) + { + $name = ucfirst(strtolower($name)); - // Trim trailing '.php' if exists - if (preg_match('/(.+)(?:\.php)/', $name, $matches)) { - $name = $matches[1]; - } + // Trim trailing '.php' if exists + if (preg_match('/(.+)(?:\.php)/', $name, $matches)) { + $name = $matches[1]; + } - // Trim trailing 'Format' if exists - if (preg_match('/(.+)(?:Format)/i', $name, $matches)) { - $name = $matches[1]; - } - if (in_array($name, $this->formatNames)) { - return $name; - } - return null; - } + // Trim trailing 'Format' if exists + if (preg_match('/(.+)(?:Format)/i', $name, $matches)) { + $name = $matches[1]; + } + if (in_array($name, $this->formatNames)) { + return $name; + } + return null; + } } diff --git a/lib/FormatInterface.php b/lib/FormatInterface.php index 5fd46ef9..8f98d6e4 100644 --- a/lib/FormatInterface.php +++ b/lib/FormatInterface.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -18,66 +19,67 @@ * @todo Explain parameters and return values in more detail * @todo Return self more often (to allow call chaining) */ -interface FormatInterface { - /** - * Generate a string representation of the current data - * - * @return string The string representation - */ - public function stringify(); +interface FormatInterface +{ + /** + * Generate a string representation of the current data + * + * @return string The string representation + */ + public function stringify(); - /** - * Set items - * - * @param array $bridges The items - * @return self The format object - * - * @todo Rename parameter `$bridges` to `$items` - */ - public function setItems(array $bridges); + /** + * Set items + * + * @param array $bridges The items + * @return self The format object + * + * @todo Rename parameter `$bridges` to `$items` + */ + public function setItems(array $bridges); - /** - * Return items - * - * @throws \LogicException if the items are not set - * @return array The items - */ - public function getItems(); + /** + * Return items + * + * @throws \LogicException if the items are not set + * @return array The items + */ + public function getItems(); - /** - * Set extra information - * - * @param array $infos Extra information - * @return self The format object - */ - public function setExtraInfos(array $infos); + /** + * Set extra information + * + * @param array $infos Extra information + * @return self The format object + */ + public function setExtraInfos(array $infos); - /** - * Return extra information - * - * @return array Extra information - */ - public function getExtraInfos(); + /** + * Return extra information + * + * @return array Extra information + */ + public function getExtraInfos(); - /** - * Return MIME type - * - * @return string The MIME type - */ - public function getMimeType(); + /** + * Return MIME type + * + * @return string The MIME type + */ + public function getMimeType(); - /** - * Set charset - * - * @param string $charset The charset - * @return self The format object - */ - public function setCharset($charset); + /** + * Set charset + * + * @param string $charset The charset + * @return self The format object + */ + public function setCharset($charset); - /** - * Return current charset - * - * @return string The charset - */ - public function getCharset(); + /** + * Return current charset + * + * @return string The charset + */ + public function getCharset(); } diff --git a/lib/ParameterValidator.php b/lib/ParameterValidator.php index 12e07942..a903ff8d 100644 --- a/lib/ParameterValidator.php +++ b/lib/ParameterValidator.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,234 +7,259 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** * Validator for bridge parameters */ -class ParameterValidator { - - /** - * Holds the list of invalid parameters - * - * @var array - */ - private $invalid = array(); - - /** - * Add item to list of invalid parameters - * - * @param string $name The name of the parameter - * @param string $reason The reason for that parameter being invalid - * @return void - */ - private function addInvalidParameter($name, $reason){ - $this->invalid[] = array( - 'name' => $name, - 'reason' => $reason - ); - } - - /** - * Return list of invalid parameters. - * - * Each element is an array of 'name' and 'reason'. - * - * @return array List of invalid parameters - */ - public function getInvalidParameters() { - return $this->invalid; - } - - /** - * Validate value for a text input - * - * @param string $value The value of a text input - * @param string|null $pattern (optional) A regex pattern - * @return string|null The filtered value or null if the value is invalid - */ - private function validateTextValue($value, $pattern = null){ - if(!is_null($pattern)) { - $filteredValue = filter_var($value, - FILTER_VALIDATE_REGEXP, - array('options' => array( - 'regexp' => '/^' . $pattern . '$/' - ) - )); - } else { - $filteredValue = filter_var($value); - } - - if($filteredValue === false) - return null; - - return $filteredValue; - } - - /** - * Validate value for a number input - * - * @param int $value The value of a number input - * @return int|null The filtered value or null if the value is invalid - */ - private function validateNumberValue($value){ - $filteredValue = filter_var($value, FILTER_VALIDATE_INT); - - if($filteredValue === false) - return null; - - return $filteredValue; - } - - /** - * Validate value for a checkbox - * - * @param bool $value The value of a checkbox - * @return bool The filtered value - */ - private function validateCheckboxValue($value){ - return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); - } - - /** - * Validate value for a list - * - * @param string $value The value of a list - * @param array $expectedValues A list of expected values - * @return string|null The filtered value or null if the value is invalid - */ - private function validateListValue($value, $expectedValues){ - $filteredValue = filter_var($value); - - if($filteredValue === false) - return null; - - if(!in_array($filteredValue, $expectedValues)) { // Check sub-values? - foreach($expectedValues as $subName => $subValue) { - if(is_array($subValue) && in_array($filteredValue, $subValue)) - return $filteredValue; - } - return null; - } - - return $filteredValue; - } - - /** - * Check if all required parameters are satisfied - * - * @param array $data (ref) A list of input values - * @param array $parameters The bridge parameters - * @return bool True if all parameters are satisfied - */ - public function validateData(&$data, $parameters){ - - if(!is_array($data)) - return false; - - foreach($data as $name => $value) { - // Some RSS readers add a cache-busting parameter (_=<timestamp>) to feed URLs, detect and ignore them. - if ($name === '_') continue; - - $registered = false; - foreach($parameters as $context => $set) { - if(array_key_exists($name, $set)) { - $registered = true; - if(!isset($set[$name]['type'])) { - $set[$name]['type'] = 'text'; - } - - switch($set[$name]['type']) { - case 'number': - $data[$name] = $this->validateNumberValue($value); - break; - case 'checkbox': - $data[$name] = $this->validateCheckboxValue($value); - break; - case 'list': - $data[$name] = $this->validateListValue($value, $set[$name]['values']); - break; - default: - case 'text': - if(isset($set[$name]['pattern'])) { - $data[$name] = $this->validateTextValue($value, $set[$name]['pattern']); - } else { - $data[$name] = $this->validateTextValue($value); - } - break; - } - - if(is_null($data[$name]) && isset($set[$name]['required']) && $set[$name]['required']) { - $this->addInvalidParameter($name, 'Parameter is invalid!'); - } - } - } - - if(!$registered) { - $this->addInvalidParameter($name, 'Parameter is not registered!'); - } - } - - return empty($this->invalid); - } - - /** - * Get the name of the context matching the provided inputs - * - * @param array $data Associative array of user data - * @param array $parameters Array of bridge parameters - * @return string|null Returns the context name or null if no match was found - */ - public function getQueriedContext($data, $parameters){ - $queriedContexts = array(); - - // Detect matching context - foreach($parameters as $context => $set) { - $queriedContexts[$context] = null; - - // Ensure all user data exist in the current context - $notInContext = array_diff_key($data, $set); - if(array_key_exists('global', $parameters)) - $notInContext = array_diff_key($notInContext, $parameters['global']); - if(sizeof($notInContext) > 0) - continue; - - // Check if all parameters of the context are satisfied - foreach($set as $id => $properties) { - if(isset($data[$id]) && !empty($data[$id])) { - $queriedContexts[$context] = true; - } elseif (isset($properties['type']) - && ($properties['type'] === 'checkbox' || $properties['type'] === 'list')) { - continue; - } elseif(isset($properties['required']) && $properties['required'] === true) { - $queriedContexts[$context] = false; - break; - } - } - } - - // Abort if one of the globally required parameters is not satisfied - if(array_key_exists('global', $parameters) - && $queriedContexts['global'] === false) { - return null; - } - unset($queriedContexts['global']); - - switch(array_sum($queriedContexts)) { - case 0: // Found no match, is there a context without parameters? - if(isset($data['context'])) return $data['context']; - foreach($queriedContexts as $context => $queried) { - if(is_null($queried)) { - return $context; - } - } - return null; - case 1: // Found unique match - return array_search(true, $queriedContexts); - default: return false; - } - } +class ParameterValidator +{ + /** + * Holds the list of invalid parameters + * + * @var array + */ + private $invalid = []; + + /** + * Add item to list of invalid parameters + * + * @param string $name The name of the parameter + * @param string $reason The reason for that parameter being invalid + * @return void + */ + private function addInvalidParameter($name, $reason) + { + $this->invalid[] = [ + 'name' => $name, + 'reason' => $reason + ]; + } + + /** + * Return list of invalid parameters. + * + * Each element is an array of 'name' and 'reason'. + * + * @return array List of invalid parameters + */ + public function getInvalidParameters() + { + return $this->invalid; + } + + /** + * Validate value for a text input + * + * @param string $value The value of a text input + * @param string|null $pattern (optional) A regex pattern + * @return string|null The filtered value or null if the value is invalid + */ + private function validateTextValue($value, $pattern = null) + { + if (!is_null($pattern)) { + $filteredValue = filter_var( + $value, + FILTER_VALIDATE_REGEXP, + ['options' => [ + 'regexp' => '/^' . $pattern . '$/' + ] + ] + ); + } else { + $filteredValue = filter_var($value); + } + + if ($filteredValue === false) { + return null; + } + + return $filteredValue; + } + + /** + * Validate value for a number input + * + * @param int $value The value of a number input + * @return int|null The filtered value or null if the value is invalid + */ + private function validateNumberValue($value) + { + $filteredValue = filter_var($value, FILTER_VALIDATE_INT); + + if ($filteredValue === false) { + return null; + } + + return $filteredValue; + } + + /** + * Validate value for a checkbox + * + * @param bool $value The value of a checkbox + * @return bool The filtered value + */ + private function validateCheckboxValue($value) + { + return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + } + + /** + * Validate value for a list + * + * @param string $value The value of a list + * @param array $expectedValues A list of expected values + * @return string|null The filtered value or null if the value is invalid + */ + private function validateListValue($value, $expectedValues) + { + $filteredValue = filter_var($value); + + if ($filteredValue === false) { + return null; + } + + if (!in_array($filteredValue, $expectedValues)) { // Check sub-values? + foreach ($expectedValues as $subName => $subValue) { + if (is_array($subValue) && in_array($filteredValue, $subValue)) { + return $filteredValue; + } + } + return null; + } + + return $filteredValue; + } + + /** + * Check if all required parameters are satisfied + * + * @param array $data (ref) A list of input values + * @param array $parameters The bridge parameters + * @return bool True if all parameters are satisfied + */ + public function validateData(&$data, $parameters) + { + if (!is_array($data)) { + return false; + } + + foreach ($data as $name => $value) { + // Some RSS readers add a cache-busting parameter (_=<timestamp>) to feed URLs, detect and ignore them. + if ($name === '_') { + continue; + } + + $registered = false; + foreach ($parameters as $context => $set) { + if (array_key_exists($name, $set)) { + $registered = true; + if (!isset($set[$name]['type'])) { + $set[$name]['type'] = 'text'; + } + + switch ($set[$name]['type']) { + case 'number': + $data[$name] = $this->validateNumberValue($value); + break; + case 'checkbox': + $data[$name] = $this->validateCheckboxValue($value); + break; + case 'list': + $data[$name] = $this->validateListValue($value, $set[$name]['values']); + break; + default: + case 'text': + if (isset($set[$name]['pattern'])) { + $data[$name] = $this->validateTextValue($value, $set[$name]['pattern']); + } else { + $data[$name] = $this->validateTextValue($value); + } + break; + } + + if (is_null($data[$name]) && isset($set[$name]['required']) && $set[$name]['required']) { + $this->addInvalidParameter($name, 'Parameter is invalid!'); + } + } + } + + if (!$registered) { + $this->addInvalidParameter($name, 'Parameter is not registered!'); + } + } + + return empty($this->invalid); + } + + /** + * Get the name of the context matching the provided inputs + * + * @param array $data Associative array of user data + * @param array $parameters Array of bridge parameters + * @return string|null Returns the context name or null if no match was found + */ + public function getQueriedContext($data, $parameters) + { + $queriedContexts = []; + + // Detect matching context + foreach ($parameters as $context => $set) { + $queriedContexts[$context] = null; + + // Ensure all user data exist in the current context + $notInContext = array_diff_key($data, $set); + if (array_key_exists('global', $parameters)) { + $notInContext = array_diff_key($notInContext, $parameters['global']); + } + if (sizeof($notInContext) > 0) { + continue; + } + + // Check if all parameters of the context are satisfied + foreach ($set as $id => $properties) { + if (isset($data[$id]) && !empty($data[$id])) { + $queriedContexts[$context] = true; + } elseif ( + isset($properties['type']) + && ($properties['type'] === 'checkbox' || $properties['type'] === 'list') + ) { + continue; + } elseif (isset($properties['required']) && $properties['required'] === true) { + $queriedContexts[$context] = false; + break; + } + } + } + + // Abort if one of the globally required parameters is not satisfied + if ( + array_key_exists('global', $parameters) + && $queriedContexts['global'] === false + ) { + return null; + } + unset($queriedContexts['global']); + + switch (array_sum($queriedContexts)) { + case 0: // Found no match, is there a context without parameters? + if (isset($data['context'])) { + return $data['context']; + } + foreach ($queriedContexts as $context => $queried) { + if (is_null($queried)) { + return $context; + } + } + return null; + case 1: // Found unique match + return array_search(true, $queriedContexts); + default: + return false; + } + } } diff --git a/lib/XPathAbstract.php b/lib/XPathAbstract.php index 0ca1587b..686addf4 100644 --- a/lib/XPathAbstract.php +++ b/lib/XPathAbstract.php @@ -15,572 +15,598 @@ * This class extends {@see BridgeAbstract}, which means it incorporates and * extends all of its functionality. **/ -abstract class XPathAbstract extends BridgeAbstract { - - /** - * Source Web page URL (should provide either HTML or XML content) - * You can specify any website URL which serves data suited for display in RSS feeds - * (for example a news blog). - * - * Use {@see XPathAbstract::getSourceUrl()} to read this parameter - */ - const FEED_SOURCE_URL = ''; - - /** - * XPath expression for extracting the feed title from the source page. - * If this is left blank or does not provide any data {@see BridgeAbstract::getName()} - * is used instead as the feed's title. - * - * Use {@see XPathAbstract::getExpressionTitle()} to read this parameter - */ - const XPATH_EXPRESSION_FEED_TITLE = './/title'; - - /** - * XPath expression for extracting the feed favicon URL from the source page. - * If this is left blank or does not provide any data {@see BridgeAbstract::getIcon()} - * is used instead as the feed's favicon URL. - * - * Use {@see XPathAbstract::getExpressionIcon()} to read this parameter - */ - const XPATH_EXPRESSION_FEED_ICON = './/link[@rel="icon"]/@href'; - - /** - * XPath expression for extracting the feed items from the source page - * Enter an XPath expression matching a list of dom nodes, each node containing one - * feed article item in total (usually a surrounding <div> or <span> tag). This will - * be the context nodes for all of the following expressions. This expression usually - * starts with a single forward slash. - * - * Use {@see XPathAbstract::getExpressionItem()} to read this parameter - */ - const XPATH_EXPRESSION_ITEM = ''; - - /** - * XPath expression for extracting an item title from the item context - * This expression should match a node contained within each article item node - * containing the article headline. It should start with a dot followed by two - * forward slashes, referring to any descendant nodes of the article item node. - * - * Use {@see XPathAbstract::getExpressionItemTitle()} to read this parameter - */ - const XPATH_EXPRESSION_ITEM_TITLE = ''; - - /** - * XPath expression for extracting an item's content from the item context - * This expression should match a node contained within each article item node - * containing the article content or description. It should start with a dot - * followed by two forward slashes, referring to any descendant nodes of the - * article item node. - * - * Use {@see XPathAbstract::getExpressionItemContent()} to read this parameter - */ - const XPATH_EXPRESSION_ITEM_CONTENT = ''; - - /** - * XPath expression for extracting an item link from the item context - * This expression should match a node's attribute containing the article URL - * (usually the href attribute of an <a> tag). It should start with a dot - * followed by two forward slashes, referring to any descendant nodes of - * the article item node. Attributes can be selected by prepending an @ char - * before the attributes name. - * - * Use {@see XPathAbstract::getExpressionItemUri()} to read this parameter - */ - const XPATH_EXPRESSION_ITEM_URI = ''; - - /** - * XPath expression for extracting an item author from the item context - * This expression should match a node contained within each article item - * node containing the article author's name. It should start with a dot - * followed by two forward slashes, referring to any descendant nodes of - * the article item node. - * - * Use {@see XPathAbstract::getExpressionItemAuthor()} to read this parameter - */ - const XPATH_EXPRESSION_ITEM_AUTHOR = ''; - - /** - * XPath expression for extracting an item timestamp from the item context - * This expression should match a node or node's attribute containing the - * article timestamp or date (parsable by PHP's strtotime function). It - * should start with a dot followed by two forward slashes, referring to - * any descendant nodes of the article item node. Attributes can be - * selected by prepending an @ char before the attributes name. - * - * Use {@see XPathAbstract::getExpressionItemTimestamp()} to read this parameter - */ - const XPATH_EXPRESSION_ITEM_TIMESTAMP = ''; - - /** - * XPath expression for extracting item enclosures (media content like - * images or movies) from the item context - * This expression should match a node's attribute containing an article - * image URL (usually the src attribute of an <img> tag or a style - * attribute). It should start with a dot followed by two forward slashes, - * referring to any descendant nodes of the article item node. Attributes - * can be selected by prepending an @ char before the attributes name. - * - * Use {@see XPathAbstract::getExpressionItemEnclosures()} to read this parameter - */ - const XPATH_EXPRESSION_ITEM_ENCLOSURES = ''; - - /** - * XPath expression for extracting an item category from the item context - * This expression should match a node or node's attribute contained - * within each article item node containing the article category. This - * could be inside <div> or <span> tags or sometimes be hidden - * in a data attribute. It should start with a dot followed by two - * forward slashes, referring to any descendant nodes of the article - * item node. Attributes can be selected by prepending an @ char - * before the attributes name. - * - * Use {@see XPathAbstract::getExpressionItemCategories()} to read this parameter - */ - const XPATH_EXPRESSION_ITEM_CATEGORIES = ''; - - /** - * Fix encoding - * Set this to true for fixing feed encoding by invoking PHP's utf8_decode - * function on all extracted texts. Try this in case you see "broken" or - * "weird" characters in your feed where you'd normally expect umlauts - * or any other non-ascii characters. - * - * Use {@see XPathAbstract::getSettingFixEncoding()} to read this parameter - */ - const SETTING_FIX_ENCODING = false; - - /** - * Internal storage for resulting feed name, automatically detected - * @var string - */ - private $feedName; - - /** - * Internal storage for resulting feed name, automatically detected - * @var string - */ - private $feedUri; - - /** - * Internal storage for resulting feed favicon, automatically detected - * @var string - */ - private $feedIcon; - - public function getName(){ - return $this->feedName ?: parent::getName(); - } - - public function getURI() { - return $this->feedUri ?: parent::getURI(); - } - - public function getIcon() { - return $this->feedIcon ?: parent::getIcon(); - } - - /** - * Source Web page URL (should provide either HTML or XML content) - * @return string - */ - protected function getSourceUrl(){ - return static::FEED_SOURCE_URL; - } - - /** - * XPath expression for extracting the feed title from the source page - * @return string - */ - protected function getExpressionTitle(){ - return static::XPATH_EXPRESSION_FEED_TITLE; - } - - /** - * XPath expression for extracting the feed favicon from the source page - * @return string - */ - protected function getExpressionIcon(){ - return static::XPATH_EXPRESSION_FEED_ICON; - } - - /** - * XPath expression for extracting the feed items from the source page - * @return string - */ - protected function getExpressionItem(){ - return static::XPATH_EXPRESSION_ITEM; - } - - /** - * XPath expression for extracting an item title from the item context - * @return string - */ - protected function getExpressionItemTitle(){ - return static::XPATH_EXPRESSION_ITEM_TITLE; - } - - /** - * XPath expression for extracting an item's content from the item context - * @return string - */ - protected function getExpressionItemContent(){ - return static::XPATH_EXPRESSION_ITEM_CONTENT; - } - - /** - * XPath expression for extracting an item link from the item context - * @return string - */ - protected function getExpressionItemUri(){ - return static::XPATH_EXPRESSION_ITEM_URI; - } - - /** - * XPath expression for extracting an item author from the item context - * @return string - */ - protected function getExpressionItemAuthor(){ - return static::XPATH_EXPRESSION_ITEM_AUTHOR; - } - - /** - * XPath expression for extracting an item timestamp from the item context - * @return string - */ - protected function getExpressionItemTimestamp(){ - return static::XPATH_EXPRESSION_ITEM_TIMESTAMP; - } - - /** - * XPath expression for extracting item enclosures (media content like - * images or movies) from the item context - * @return string - */ - protected function getExpressionItemEnclosures(){ - return static::XPATH_EXPRESSION_ITEM_ENCLOSURES; - } - - /** - * XPath expression for extracting an item category from the item context - * @return string - */ - protected function getExpressionItemCategories(){ - return static::XPATH_EXPRESSION_ITEM_CATEGORIES; - } - - /** - * Fix encoding - * @return string - */ - protected function getSettingFixEncoding(){ - return static::SETTING_FIX_ENCODING; - } - - /** - * Internal helper method for quickly accessing all the user defined constants - * in derived classes - * - * @param $name - * @return bool|string - */ - private function getParam($name){ - switch($name) { - - case 'url': - return $this->getSourceUrl(); - case 'feed_title': - return $this->getExpressionTitle(); - case 'feed_icon': - return $this->getExpressionIcon(); - case 'item': - return $this->getExpressionItem(); - case 'title': - return $this->getExpressionItemTitle(); - case 'content': - return $this->getExpressionItemContent(); - case 'uri': - return $this->getExpressionItemUri(); - case 'author': - return $this->getExpressionItemAuthor(); - case 'timestamp': - return $this->getExpressionItemTimestamp(); - case 'enclosures': - return $this->getExpressionItemEnclosures(); - case 'categories': - return $this->getExpressionItemCategories(); - case 'fix_encoding': - return $this->getSettingFixEncoding(); - } - } - - /** - * Should provide the source website HTML content - * can be easily overwritten for example if special headers or auth infos are required - * @return string - */ - protected function provideWebsiteContent() { - return getContents($this->feedUri); - } - - /** - * Should provide the feeds title - * - * @param DOMXPath $xpath - * @return string - */ - protected function provideFeedTitle(DOMXPath $xpath) { - $title = $xpath->query($this->getParam('feed_title')); - if(count($title) === 1) { - return $this->getItemValueOrNodeValue($title); - } - } - - /** - * Should provide the URL of the feed's favicon - * - * @param DOMXPath $xpath - * @return string - */ - protected function provideFeedIcon(DOMXPath $xpath) { - $icon = $xpath->query($this->getParam('feed_icon')); - if(count($icon) === 1) { - return $this->cleanMediaUrl($this->getItemValueOrNodeValue($icon)); - } - } - - /** - * Should provide the feed's items. - * - * @param DOMXPath $xpath - * @return DOMNodeList - */ - protected function provideFeedItems(DOMXPath $xpath) { - return @$xpath->query($this->getParam('item')); - } - - public function collectData() { - - $this->feedUri = $this->getParam('url'); - - $webPageHtml = new DOMDocument(); - libxml_use_internal_errors(true); - $webPageHtml->loadHTML($this->provideWebsiteContent()); - libxml_clear_errors(); - libxml_use_internal_errors(false); - - $xpath = new DOMXPath($webPageHtml); - - $this->feedName = $this->provideFeedTitle($xpath); - $this->feedIcon = $this->provideFeedIcon($xpath); - - $entries = $this->provideFeedItems($xpath); - if($entries === false) { - return; - } - - foreach ($entries as $entry) { - $item = new \FeedItem(); - foreach(array('title', 'content', 'uri', 'author', 'timestamp', 'enclosures', 'categories') as $param) { - - $expression = $this->getParam($param); - if('' === $expression) { - continue; - } - - //can be a string or DOMNodeList, depending on the expression result - $typedResult = @$xpath->evaluate($expression, $entry); - if ($typedResult === false || ($typedResult instanceof DOMNodeList && count($typedResult) === 0) - || (is_string($typedResult) && strlen(trim($typedResult)) === 0)) { - continue; - } - - $item->__set($param, $this->formatParamValue($param, $this->getItemValueOrNodeValue($typedResult))); - - } - - $itemId = $this->generateItemId($item); - if(null !== $itemId) { - $item->setUid($itemId); - } - - $this->items[] = $item; - } - - } - - /** - * @param $param - * @param $value - * @return string|array - */ - protected function formatParamValue($param, $value) - { - $value = $this->fixEncoding($value); - switch ($param) { - case 'title': - return $this->formatItemTitle($value); - case 'content': - return $this->formatItemContent($value); - case 'uri': - return $this->formatItemUri($value); - case 'author': - return $this->formatItemAuthor($value); - case 'timestamp': - return $this->formatItemTimestamp($value); - case 'enclosures': - return $this->formatItemEnclosures($value); - case 'categories': - return $this->formatItemCategories($value); - } - return $value; - } - - /** - * Formats the title of a feed item. Takes extracted raw title and returns it formatted - * as string. - * Can be easily overwritten for in case the value needs to be transformed into something - * else. - * @param string $value - * @return string - */ - protected function formatItemTitle($value) { - return $value; - } - - /** - * Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix - * timestamp as integer. - * Can be easily overwritten for example if a special format has to be expected on the - * source website. - * @param string $value - * @return string - */ - protected function formatItemContent($value) { - return $value; - } - - /** - * Formats the URI of a feed item. Takes extracted raw URI and returns it formatted - * as string. - * Can be easily overwritten for in case the value needs to be transformed into something - * else. - * @param string $value - * @return string - */ - protected function formatItemUri($value) { - if(strlen($value) === 0) { - return ''; - } - if(strpos($value, 'http://') === 0 || strpos($value, 'https://') === 0) { - return $value; - } - - return urljoin($this->feedUri, $value); - } - - /** - * Formats the author of a feed item. Takes extracted raw author and returns it formatted - * as string. - * Can be easily overwritten for in case the value needs to be transformed into something - * else. - * @param string $value - * @return string - */ - protected function formatItemAuthor($value) { - return $value; - } - - /** - * Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix - * timestamp as integer. - * Can be easily overwritten for example if a special format has to be expected on the - * source website. - * @param string $value - * @return false|int - */ - protected function formatItemTimestamp($value) { - return strtotime($value); - } - - /** - * Formats the enclosures of a feed item. Takes extracted raw enclosures and returns them - * formatted as array. - * Can be easily overwritten for in case the values need to be transformed into something - * else. - * @param string $value - * @return array - */ - protected function formatItemEnclosures($value) { - return array($this->cleanMediaUrl($value)); - } - - /** - * Formats the categories of a feed item. Takes extracted raw categories and returns them - * formatted as array. - * Can be easily overwritten for in case the values need to be transformed into something - * else. - * @param string $value - * @return array - */ - protected function formatItemCategories($value) { - return array($value); - } - - /** - * @param $mediaUrl - * @return string|void - */ - protected function cleanMediaUrl($mediaUrl) - { - $pattern = '~(?:http(?:s)?:)?[\/a-zA-Z0-9\-=_,\.\%]+\.(?:jpg|gif|png|jpeg|ico|mp3|webp){1}~i'; - $result = preg_match($pattern, $mediaUrl, $matches); - if(1 !== $result) { - return; - } - return urljoin($this->feedUri, $matches[0]); - } - - /** - * @param $typedResult - * @return string - */ - protected function getItemValueOrNodeValue($typedResult) - { - if($typedResult instanceof DOMNodeList) { - $item = $typedResult->item(0); - if ($item instanceof DOMElement) { - return trim($item->nodeValue); - } elseif ($item instanceof DOMAttr) { - return trim($item->value); - } elseif ($item instanceof DOMText) { - return trim($item->wholeText); - } - } elseif(is_string($typedResult) && strlen($typedResult) > 0) { - return trim($typedResult); - } - returnServerError('Unknown type of XPath expression result.'); - } - - /** - * Fixes feed encoding by invoking PHP's utf8_decode function on extracted texts. - * Useful in case of "broken" or "weird" characters in the feed where you'd normally - * expect umlauts. - * - * @param $input - * @return string - */ - protected function fixEncoding($input) - { - return $this->getParam('fix_encoding') ? utf8_decode($input) : $input; - } - - /** - * Allows overriding default mechanism determining items Uid's - * - * @param FeedItem $item - * @return string|null - */ - protected function generateItemId(\FeedItem $item) { - return null; //auto generation - } +abstract class XPathAbstract extends BridgeAbstract +{ + /** + * Source Web page URL (should provide either HTML or XML content) + * You can specify any website URL which serves data suited for display in RSS feeds + * (for example a news blog). + * + * Use {@see XPathAbstract::getSourceUrl()} to read this parameter + */ + const FEED_SOURCE_URL = ''; + + /** + * XPath expression for extracting the feed title from the source page. + * If this is left blank or does not provide any data {@see BridgeAbstract::getName()} + * is used instead as the feed's title. + * + * Use {@see XPathAbstract::getExpressionTitle()} to read this parameter + */ + const XPATH_EXPRESSION_FEED_TITLE = './/title'; + + /** + * XPath expression for extracting the feed favicon URL from the source page. + * If this is left blank or does not provide any data {@see BridgeAbstract::getIcon()} + * is used instead as the feed's favicon URL. + * + * Use {@see XPathAbstract::getExpressionIcon()} to read this parameter + */ + const XPATH_EXPRESSION_FEED_ICON = './/link[@rel="icon"]/@href'; + + /** + * XPath expression for extracting the feed items from the source page + * Enter an XPath expression matching a list of dom nodes, each node containing one + * feed article item in total (usually a surrounding <div> or <span> tag). This will + * be the context nodes for all of the following expressions. This expression usually + * starts with a single forward slash. + * + * Use {@see XPathAbstract::getExpressionItem()} to read this parameter + */ + const XPATH_EXPRESSION_ITEM = ''; + + /** + * XPath expression for extracting an item title from the item context + * This expression should match a node contained within each article item node + * containing the article headline. It should start with a dot followed by two + * forward slashes, referring to any descendant nodes of the article item node. + * + * Use {@see XPathAbstract::getExpressionItemTitle()} to read this parameter + */ + const XPATH_EXPRESSION_ITEM_TITLE = ''; + + /** + * XPath expression for extracting an item's content from the item context + * This expression should match a node contained within each article item node + * containing the article content or description. It should start with a dot + * followed by two forward slashes, referring to any descendant nodes of the + * article item node. + * + * Use {@see XPathAbstract::getExpressionItemContent()} to read this parameter + */ + const XPATH_EXPRESSION_ITEM_CONTENT = ''; + + /** + * XPath expression for extracting an item link from the item context + * This expression should match a node's attribute containing the article URL + * (usually the href attribute of an <a> tag). It should start with a dot + * followed by two forward slashes, referring to any descendant nodes of + * the article item node. Attributes can be selected by prepending an @ char + * before the attributes name. + * + * Use {@see XPathAbstract::getExpressionItemUri()} to read this parameter + */ + const XPATH_EXPRESSION_ITEM_URI = ''; + + /** + * XPath expression for extracting an item author from the item context + * This expression should match a node contained within each article item + * node containing the article author's name. It should start with a dot + * followed by two forward slashes, referring to any descendant nodes of + * the article item node. + * + * Use {@see XPathAbstract::getExpressionItemAuthor()} to read this parameter + */ + const XPATH_EXPRESSION_ITEM_AUTHOR = ''; + + /** + * XPath expression for extracting an item timestamp from the item context + * This expression should match a node or node's attribute containing the + * article timestamp or date (parsable by PHP's strtotime function). It + * should start with a dot followed by two forward slashes, referring to + * any descendant nodes of the article item node. Attributes can be + * selected by prepending an @ char before the attributes name. + * + * Use {@see XPathAbstract::getExpressionItemTimestamp()} to read this parameter + */ + const XPATH_EXPRESSION_ITEM_TIMESTAMP = ''; + + /** + * XPath expression for extracting item enclosures (media content like + * images or movies) from the item context + * This expression should match a node's attribute containing an article + * image URL (usually the src attribute of an <img> tag or a style + * attribute). It should start with a dot followed by two forward slashes, + * referring to any descendant nodes of the article item node. Attributes + * can be selected by prepending an @ char before the attributes name. + * + * Use {@see XPathAbstract::getExpressionItemEnclosures()} to read this parameter + */ + const XPATH_EXPRESSION_ITEM_ENCLOSURES = ''; + + /** + * XPath expression for extracting an item category from the item context + * This expression should match a node or node's attribute contained + * within each article item node containing the article category. This + * could be inside <div> or <span> tags or sometimes be hidden + * in a data attribute. It should start with a dot followed by two + * forward slashes, referring to any descendant nodes of the article + * item node. Attributes can be selected by prepending an @ char + * before the attributes name. + * + * Use {@see XPathAbstract::getExpressionItemCategories()} to read this parameter + */ + const XPATH_EXPRESSION_ITEM_CATEGORIES = ''; + + /** + * Fix encoding + * Set this to true for fixing feed encoding by invoking PHP's utf8_decode + * function on all extracted texts. Try this in case you see "broken" or + * "weird" characters in your feed where you'd normally expect umlauts + * or any other non-ascii characters. + * + * Use {@see XPathAbstract::getSettingFixEncoding()} to read this parameter + */ + const SETTING_FIX_ENCODING = false; + + /** + * Internal storage for resulting feed name, automatically detected + * @var string + */ + private $feedName; + + /** + * Internal storage for resulting feed name, automatically detected + * @var string + */ + private $feedUri; + + /** + * Internal storage for resulting feed favicon, automatically detected + * @var string + */ + private $feedIcon; + + public function getName() + { + return $this->feedName ?: parent::getName(); + } + + public function getURI() + { + return $this->feedUri ?: parent::getURI(); + } + + public function getIcon() + { + return $this->feedIcon ?: parent::getIcon(); + } + + /** + * Source Web page URL (should provide either HTML or XML content) + * @return string + */ + protected function getSourceUrl() + { + return static::FEED_SOURCE_URL; + } + + /** + * XPath expression for extracting the feed title from the source page + * @return string + */ + protected function getExpressionTitle() + { + return static::XPATH_EXPRESSION_FEED_TITLE; + } + + /** + * XPath expression for extracting the feed favicon from the source page + * @return string + */ + protected function getExpressionIcon() + { + return static::XPATH_EXPRESSION_FEED_ICON; + } + + /** + * XPath expression for extracting the feed items from the source page + * @return string + */ + protected function getExpressionItem() + { + return static::XPATH_EXPRESSION_ITEM; + } + + /** + * XPath expression for extracting an item title from the item context + * @return string + */ + protected function getExpressionItemTitle() + { + return static::XPATH_EXPRESSION_ITEM_TITLE; + } + + /** + * XPath expression for extracting an item's content from the item context + * @return string + */ + protected function getExpressionItemContent() + { + return static::XPATH_EXPRESSION_ITEM_CONTENT; + } + + /** + * XPath expression for extracting an item link from the item context + * @return string + */ + protected function getExpressionItemUri() + { + return static::XPATH_EXPRESSION_ITEM_URI; + } + + /** + * XPath expression for extracting an item author from the item context + * @return string + */ + protected function getExpressionItemAuthor() + { + return static::XPATH_EXPRESSION_ITEM_AUTHOR; + } + + /** + * XPath expression for extracting an item timestamp from the item context + * @return string + */ + protected function getExpressionItemTimestamp() + { + return static::XPATH_EXPRESSION_ITEM_TIMESTAMP; + } + + /** + * XPath expression for extracting item enclosures (media content like + * images or movies) from the item context + * @return string + */ + protected function getExpressionItemEnclosures() + { + return static::XPATH_EXPRESSION_ITEM_ENCLOSURES; + } + + /** + * XPath expression for extracting an item category from the item context + * @return string + */ + protected function getExpressionItemCategories() + { + return static::XPATH_EXPRESSION_ITEM_CATEGORIES; + } + + /** + * Fix encoding + * @return string + */ + protected function getSettingFixEncoding() + { + return static::SETTING_FIX_ENCODING; + } + + /** + * Internal helper method for quickly accessing all the user defined constants + * in derived classes + * + * @param $name + * @return bool|string + */ + private function getParam($name) + { + switch ($name) { + case 'url': + return $this->getSourceUrl(); + case 'feed_title': + return $this->getExpressionTitle(); + case 'feed_icon': + return $this->getExpressionIcon(); + case 'item': + return $this->getExpressionItem(); + case 'title': + return $this->getExpressionItemTitle(); + case 'content': + return $this->getExpressionItemContent(); + case 'uri': + return $this->getExpressionItemUri(); + case 'author': + return $this->getExpressionItemAuthor(); + case 'timestamp': + return $this->getExpressionItemTimestamp(); + case 'enclosures': + return $this->getExpressionItemEnclosures(); + case 'categories': + return $this->getExpressionItemCategories(); + case 'fix_encoding': + return $this->getSettingFixEncoding(); + } + } + + /** + * Should provide the source website HTML content + * can be easily overwritten for example if special headers or auth infos are required + * @return string + */ + protected function provideWebsiteContent() + { + return getContents($this->feedUri); + } + + /** + * Should provide the feeds title + * + * @param DOMXPath $xpath + * @return string + */ + protected function provideFeedTitle(DOMXPath $xpath) + { + $title = $xpath->query($this->getParam('feed_title')); + if (count($title) === 1) { + return $this->getItemValueOrNodeValue($title); + } + } + + /** + * Should provide the URL of the feed's favicon + * + * @param DOMXPath $xpath + * @return string + */ + protected function provideFeedIcon(DOMXPath $xpath) + { + $icon = $xpath->query($this->getParam('feed_icon')); + if (count($icon) === 1) { + return $this->cleanMediaUrl($this->getItemValueOrNodeValue($icon)); + } + } + + /** + * Should provide the feed's items. + * + * @param DOMXPath $xpath + * @return DOMNodeList + */ + protected function provideFeedItems(DOMXPath $xpath) + { + return @$xpath->query($this->getParam('item')); + } + + public function collectData() + { + $this->feedUri = $this->getParam('url'); + + $webPageHtml = new DOMDocument(); + libxml_use_internal_errors(true); + $webPageHtml->loadHTML($this->provideWebsiteContent()); + libxml_clear_errors(); + libxml_use_internal_errors(false); + + $xpath = new DOMXPath($webPageHtml); + + $this->feedName = $this->provideFeedTitle($xpath); + $this->feedIcon = $this->provideFeedIcon($xpath); + + $entries = $this->provideFeedItems($xpath); + if ($entries === false) { + return; + } + + foreach ($entries as $entry) { + $item = new \FeedItem(); + foreach (['title', 'content', 'uri', 'author', 'timestamp', 'enclosures', 'categories'] as $param) { + $expression = $this->getParam($param); + if ('' === $expression) { + continue; + } + + //can be a string or DOMNodeList, depending on the expression result + $typedResult = @$xpath->evaluate($expression, $entry); + if ( + $typedResult === false || ($typedResult instanceof DOMNodeList && count($typedResult) === 0) + || (is_string($typedResult) && strlen(trim($typedResult)) === 0) + ) { + continue; + } + + $item->__set($param, $this->formatParamValue($param, $this->getItemValueOrNodeValue($typedResult))); + } + + $itemId = $this->generateItemId($item); + if (null !== $itemId) { + $item->setUid($itemId); + } + + $this->items[] = $item; + } + } + + /** + * @param $param + * @param $value + * @return string|array + */ + protected function formatParamValue($param, $value) + { + $value = $this->fixEncoding($value); + switch ($param) { + case 'title': + return $this->formatItemTitle($value); + case 'content': + return $this->formatItemContent($value); + case 'uri': + return $this->formatItemUri($value); + case 'author': + return $this->formatItemAuthor($value); + case 'timestamp': + return $this->formatItemTimestamp($value); + case 'enclosures': + return $this->formatItemEnclosures($value); + case 'categories': + return $this->formatItemCategories($value); + } + return $value; + } + + /** + * Formats the title of a feed item. Takes extracted raw title and returns it formatted + * as string. + * Can be easily overwritten for in case the value needs to be transformed into something + * else. + * @param string $value + * @return string + */ + protected function formatItemTitle($value) + { + return $value; + } + + /** + * Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix + * timestamp as integer. + * Can be easily overwritten for example if a special format has to be expected on the + * source website. + * @param string $value + * @return string + */ + protected function formatItemContent($value) + { + return $value; + } + + /** + * Formats the URI of a feed item. Takes extracted raw URI and returns it formatted + * as string. + * Can be easily overwritten for in case the value needs to be transformed into something + * else. + * @param string $value + * @return string + */ + protected function formatItemUri($value) + { + if (strlen($value) === 0) { + return ''; + } + if (strpos($value, 'http://') === 0 || strpos($value, 'https://') === 0) { + return $value; + } + + return urljoin($this->feedUri, $value); + } + + /** + * Formats the author of a feed item. Takes extracted raw author and returns it formatted + * as string. + * Can be easily overwritten for in case the value needs to be transformed into something + * else. + * @param string $value + * @return string + */ + protected function formatItemAuthor($value) + { + return $value; + } + + /** + * Formats the timestamp of a feed item. Takes extracted raw timestamp and returns unix + * timestamp as integer. + * Can be easily overwritten for example if a special format has to be expected on the + * source website. + * @param string $value + * @return false|int + */ + protected function formatItemTimestamp($value) + { + return strtotime($value); + } + + /** + * Formats the enclosures of a feed item. Takes extracted raw enclosures and returns them + * formatted as array. + * Can be easily overwritten for in case the values need to be transformed into something + * else. + * @param string $value + * @return array + */ + protected function formatItemEnclosures($value) + { + return [$this->cleanMediaUrl($value)]; + } + + /** + * Formats the categories of a feed item. Takes extracted raw categories and returns them + * formatted as array. + * Can be easily overwritten for in case the values need to be transformed into something + * else. + * @param string $value + * @return array + */ + protected function formatItemCategories($value) + { + return [$value]; + } + + /** + * @param $mediaUrl + * @return string|void + */ + protected function cleanMediaUrl($mediaUrl) + { + $pattern = '~(?:http(?:s)?:)?[\/a-zA-Z0-9\-=_,\.\%]+\.(?:jpg|gif|png|jpeg|ico|mp3|webp){1}~i'; + $result = preg_match($pattern, $mediaUrl, $matches); + if (1 !== $result) { + return; + } + return urljoin($this->feedUri, $matches[0]); + } + + /** + * @param $typedResult + * @return string + */ + protected function getItemValueOrNodeValue($typedResult) + { + if ($typedResult instanceof DOMNodeList) { + $item = $typedResult->item(0); + if ($item instanceof DOMElement) { + return trim($item->nodeValue); + } elseif ($item instanceof DOMAttr) { + return trim($item->value); + } elseif ($item instanceof DOMText) { + return trim($item->wholeText); + } + } elseif (is_string($typedResult) && strlen($typedResult) > 0) { + return trim($typedResult); + } + returnServerError('Unknown type of XPath expression result.'); + } + + /** + * Fixes feed encoding by invoking PHP's utf8_decode function on extracted texts. + * Useful in case of "broken" or "weird" characters in the feed where you'd normally + * expect umlauts. + * + * @param $input + * @return string + */ + protected function fixEncoding($input) + { + return $this->getParam('fix_encoding') ? utf8_decode($input) : $input; + } + + /** + * Allows overriding default mechanism determining items Uid's + * + * @param FeedItem $item + * @return string|null + */ + protected function generateItemId(\FeedItem $item) + { + return null; //auto generation + } } diff --git a/lib/contents.php b/lib/contents.php index cc80248b..a01d81e1 100644 --- a/lib/contents.php +++ b/lib/contents.php @@ -1,48 +1,50 @@ <?php -final class HttpException extends \Exception {} +final class HttpException extends \Exception +{ +} // todo: move this somewhere useful, possibly into a function const RSSBRIDGE_HTTP_STATUS_CODES = [ - '100' => 'Continue', - '101' => 'Switching Protocols', - '200' => 'OK', - '201' => 'Created', - '202' => 'Accepted', - '203' => 'Non-Authoritative Information', - '204' => 'No Content', - '205' => 'Reset Content', - '206' => 'Partial Content', - '300' => 'Multiple Choices', - '302' => 'Found', - '303' => 'See Other', - '304' => 'Not Modified', - '305' => 'Use Proxy', - '400' => 'Bad Request', - '401' => 'Unauthorized', - '402' => 'Payment Required', - '403' => 'Forbidden', - '404' => 'Not Found', - '405' => 'Method Not Allowed', - '406' => 'Not Acceptable', - '407' => 'Proxy Authentication Required', - '408' => 'Request Timeout', - '409' => 'Conflict', - '410' => 'Gone', - '411' => 'Length Required', - '412' => 'Precondition Failed', - '413' => 'Request Entity Too Large', - '414' => 'Request-URI Too Long', - '415' => 'Unsupported Media Type', - '416' => 'Requested Range Not Satisfiable', - '417' => 'Expectation Failed', - '429' => 'Too Many Requests', - '500' => 'Internal Server Error', - '501' => 'Not Implemented', - '502' => 'Bad Gateway', - '503' => 'Service Unavailable', - '504' => 'Gateway Timeout', - '505' => 'HTTP Version Not Supported' + '100' => 'Continue', + '101' => 'Switching Protocols', + '200' => 'OK', + '201' => 'Created', + '202' => 'Accepted', + '203' => 'Non-Authoritative Information', + '204' => 'No Content', + '205' => 'Reset Content', + '206' => 'Partial Content', + '300' => 'Multiple Choices', + '302' => 'Found', + '303' => 'See Other', + '304' => 'Not Modified', + '305' => 'Use Proxy', + '400' => 'Bad Request', + '401' => 'Unauthorized', + '402' => 'Payment Required', + '403' => 'Forbidden', + '404' => 'Not Found', + '405' => 'Method Not Allowed', + '406' => 'Not Acceptable', + '407' => 'Proxy Authentication Required', + '408' => 'Request Timeout', + '409' => 'Conflict', + '410' => 'Gone', + '411' => 'Length Required', + '412' => 'Precondition Failed', + '413' => 'Request Entity Too Large', + '414' => 'Request-URI Too Long', + '415' => 'Unsupported Media Type', + '416' => 'Requested Range Not Satisfiable', + '417' => 'Expectation Failed', + '429' => 'Too Many Requests', + '500' => 'Internal Server Error', + '501' => 'Not Implemented', + '502' => 'Bad Gateway', + '503' => 'Service Unavailable', + '504' => 'Gateway Timeout', + '505' => 'HTTP Version Not Supported' ]; /** @@ -61,70 +63,70 @@ const RSSBRIDGE_HTTP_STATUS_CODES = [ * @return string|array */ function getContents( - string $url, - array $httpHeaders = [], - array $curlOptions = [], - bool $returnFull = false + string $url, + array $httpHeaders = [], + array $curlOptions = [], + bool $returnFull = false ) { - $cacheFactory = new CacheFactory(); + $cacheFactory = new CacheFactory(); - $cache = $cacheFactory->create(Configuration::getConfig('cache', 'type')); - $cache->setScope('server'); - $cache->purgeCache(86400); // 24 hours (forced) - $cache->setKey([$url]); + $cache = $cacheFactory->create(Configuration::getConfig('cache', 'type')); + $cache->setScope('server'); + $cache->purgeCache(86400); // 24 hours (forced) + $cache->setKey([$url]); - $config = [ - 'headers' => $httpHeaders, - 'curl_options' => $curlOptions, - ]; - if (defined('PROXY_URL') && !defined('NOPROXY')) { - $config['proxy'] = PROXY_URL; - } - if(!Debug::isEnabled() && $cache->getTime()) { - $config['if_not_modified_since'] = $cache->getTime(); - } + $config = [ + 'headers' => $httpHeaders, + 'curl_options' => $curlOptions, + ]; + if (defined('PROXY_URL') && !defined('NOPROXY')) { + $config['proxy'] = PROXY_URL; + } + if (!Debug::isEnabled() && $cache->getTime()) { + $config['if_not_modified_since'] = $cache->getTime(); + } - $result = _http_request($url, $config); - $response = [ - 'code' => $result['code'], - 'status_lines' => $result['status_lines'], - 'header' => $result['headers'], - 'content' => $result['body'], - ]; + $result = _http_request($url, $config); + $response = [ + 'code' => $result['code'], + 'status_lines' => $result['status_lines'], + 'header' => $result['headers'], + 'content' => $result['body'], + ]; - switch($result['code']) { - case 200: - case 201: - case 202: - if(isset($result['headers']['cache-control'])) { - $cachecontrol = $result['headers']['cache-control']; - $lastValue = array_pop($cachecontrol); - $directives = explode(',', $lastValue); - $directives = array_map('trim', $directives); - if(in_array('no-cache', $directives) || in_array('no-store', $directives)) { - // Don't cache as instructed by the server - break; - } - } - $cache->saveData($result['body']); - break; - case 304: // Not Modified - $response['content'] = $cache->loadData(); - break; - default: - throw new HttpException( - sprintf( - '%s %s', - $result['code'], - RSSBRIDGE_HTTP_STATUS_CODES[$result['code']] ?? '' - ), - $result['code'] - ); - } - if ($returnFull === true) { - return $response; - } - return $response['content']; + switch ($result['code']) { + case 200: + case 201: + case 202: + if (isset($result['headers']['cache-control'])) { + $cachecontrol = $result['headers']['cache-control']; + $lastValue = array_pop($cachecontrol); + $directives = explode(',', $lastValue); + $directives = array_map('trim', $directives); + if (in_array('no-cache', $directives) || in_array('no-store', $directives)) { + // Don't cache as instructed by the server + break; + } + } + $cache->saveData($result['body']); + break; + case 304: // Not Modified + $response['content'] = $cache->loadData(); + break; + default: + throw new HttpException( + sprintf( + '%s %s', + $result['code'], + RSSBRIDGE_HTTP_STATUS_CODES[$result['code']] ?? '' + ), + $result['code'] + ); + } + if ($returnFull === true) { + return $response; + } + return $response['content']; } /** @@ -136,85 +138,85 @@ function getContents( */ function _http_request(string $url, array $config = []): array { - $defaults = [ - 'useragent' => Configuration::getConfig('http', 'useragent'), - 'timeout' => Configuration::getConfig('http', 'timeout'), - 'headers' => [], - 'proxy' => null, - 'curl_options' => [], - 'if_not_modified_since' => null, - 'retries' => 3, - ]; - $config = array_merge($defaults, $config); + $defaults = [ + 'useragent' => Configuration::getConfig('http', 'useragent'), + 'timeout' => Configuration::getConfig('http', 'timeout'), + 'headers' => [], + 'proxy' => null, + 'curl_options' => [], + 'if_not_modified_since' => null, + 'retries' => 3, + ]; + $config = array_merge($defaults, $config); - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_MAXREDIRS, 5); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $config['headers']); - curl_setopt($ch, CURLOPT_USERAGENT, $config['useragent']); - curl_setopt($ch, CURLOPT_TIMEOUT, $config['timeout']); - curl_setopt($ch, CURLOPT_ENCODING, ''); - curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); - if($config['proxy']) { - curl_setopt($ch, CURLOPT_PROXY, $config['proxy']); - } - if (curl_setopt_array($ch, $config['curl_options']) === false) { - throw new \Exception('Tried to set an illegal curl option'); - } + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, 5); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $config['headers']); + curl_setopt($ch, CURLOPT_USERAGENT, $config['useragent']); + curl_setopt($ch, CURLOPT_TIMEOUT, $config['timeout']); + curl_setopt($ch, CURLOPT_ENCODING, ''); + curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + if ($config['proxy']) { + curl_setopt($ch, CURLOPT_PROXY, $config['proxy']); + } + if (curl_setopt_array($ch, $config['curl_options']) === false) { + throw new \Exception('Tried to set an illegal curl option'); + } - if ($config['if_not_modified_since']) { - curl_setopt($ch, CURLOPT_TIMEVALUE, $config['if_not_modified_since']); - curl_setopt($ch, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE); - } + if ($config['if_not_modified_since']) { + curl_setopt($ch, CURLOPT_TIMEVALUE, $config['if_not_modified_since']); + curl_setopt($ch, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE); + } - $responseStatusLines = []; - $responseHeaders = []; - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $rawHeader) use (&$responseHeaders, &$responseStatusLines) { - $len = strlen($rawHeader); - if ($rawHeader === "\r\n") { - return $len; - } - if (preg_match('#^HTTP/(2|1.1|1.0)#', $rawHeader)) { - $responseStatusLines[] = $rawHeader; - return $len; - } - $header = explode(':', $rawHeader); - if (count($header) === 1) { - return $len; - } - $name = mb_strtolower(trim($header[0])); - $value = trim(implode(':', array_slice($header, 1))); - if (!isset($responseHeaders[$name])) { - $responseHeaders[$name] = []; - } - $responseHeaders[$name][] = $value; - return $len; - }); + $responseStatusLines = []; + $responseHeaders = []; + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $rawHeader) use (&$responseHeaders, &$responseStatusLines) { + $len = strlen($rawHeader); + if ($rawHeader === "\r\n") { + return $len; + } + if (preg_match('#^HTTP/(2|1.1|1.0)#', $rawHeader)) { + $responseStatusLines[] = $rawHeader; + return $len; + } + $header = explode(':', $rawHeader); + if (count($header) === 1) { + return $len; + } + $name = mb_strtolower(trim($header[0])); + $value = trim(implode(':', array_slice($header, 1))); + if (!isset($responseHeaders[$name])) { + $responseHeaders[$name] = []; + } + $responseHeaders[$name][] = $value; + return $len; + }); - $attempts = 0; - while(true) { - $attempts++; - $data = curl_exec($ch); - if ($data !== false) { - // The network call was successful, so break out of the loop - break; - } - if ($attempts > $config['retries']) { - // Finally give up - throw new HttpException(sprintf('%s (%s)', curl_error($ch), curl_errno($ch))); - } - } + $attempts = 0; + while (true) { + $attempts++; + $data = curl_exec($ch); + if ($data !== false) { + // The network call was successful, so break out of the loop + break; + } + if ($attempts > $config['retries']) { + // Finally give up + throw new HttpException(sprintf('%s (%s)', curl_error($ch), curl_errno($ch))); + } + } - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - return [ - 'code' => $statusCode, - 'status_lines' => $responseStatusLines, - 'headers' => $responseHeaders, - 'body' => $data, - ]; + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + return [ + 'code' => $statusCode, + 'status_lines' => $responseStatusLines, + 'headers' => $responseHeaders, + 'body' => $data, + ]; } /** @@ -243,28 +245,31 @@ function _http_request(string $url, array $config = []): array * tags when returning plaintext. * @return false|simple_html_dom Contents as simplehtmldom object. */ -function getSimpleHTMLDOM($url, - $header = array(), - $opts = array(), - $lowercase = true, - $forceTagsClosed = true, - $target_charset = DEFAULT_TARGET_CHARSET, - $stripRN = true, - $defaultBRText = DEFAULT_BR_TEXT, - $defaultSpanText = DEFAULT_SPAN_TEXT){ - - $content = getContents( - $url, - $header ?? [], - $opts ?? [] - ); - return str_get_html($content, - $lowercase, - $forceTagsClosed, - $target_charset, - $stripRN, - $defaultBRText, - $defaultSpanText); +function getSimpleHTMLDOM( + $url, + $header = [], + $opts = [], + $lowercase = true, + $forceTagsClosed = true, + $target_charset = DEFAULT_TARGET_CHARSET, + $stripRN = true, + $defaultBRText = DEFAULT_BR_TEXT, + $defaultSpanText = DEFAULT_SPAN_TEXT +) { + $content = getContents( + $url, + $header ?? [], + $opts ?? [] + ); + return str_get_html( + $content, + $lowercase, + $forceTagsClosed, + $target_charset, + $stripRN, + $defaultBRText, + $defaultSpanText + ); } /** @@ -297,53 +302,58 @@ function getSimpleHTMLDOM($url, * tags when returning plaintext. * @return false|simple_html_dom Contents as simplehtmldom object. */ -function getSimpleHTMLDOMCached($url, - $duration = 86400, - $header = array(), - $opts = array(), - $lowercase = true, - $forceTagsClosed = true, - $target_charset = DEFAULT_TARGET_CHARSET, - $stripRN = true, - $defaultBRText = DEFAULT_BR_TEXT, - $defaultSpanText = DEFAULT_SPAN_TEXT){ - - Debug::log('Caching url ' . $url . ', duration ' . $duration); +function getSimpleHTMLDOMCached( + $url, + $duration = 86400, + $header = [], + $opts = [], + $lowercase = true, + $forceTagsClosed = true, + $target_charset = DEFAULT_TARGET_CHARSET, + $stripRN = true, + $defaultBRText = DEFAULT_BR_TEXT, + $defaultSpanText = DEFAULT_SPAN_TEXT +) { + Debug::log('Caching url ' . $url . ', duration ' . $duration); - // Initialize cache - $cacheFac = new CacheFactory(); + // Initialize cache + $cacheFac = new CacheFactory(); - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope('pages'); - $cache->purgeCache(86400); // 24 hours (forced) + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope('pages'); + $cache->purgeCache(86400); // 24 hours (forced) - $params = array($url); - $cache->setKey($params); + $params = [$url]; + $cache->setKey($params); - // Determine if cached file is within duration - $time = $cache->getTime(); - if($time !== false - && (time() - $duration < $time) - && !Debug::isEnabled()) { // Contents within duration - $content = $cache->loadData(); - } else { // Content not within duration - $content = getContents( - $url, - $header ?? [], - $opts ?? [] - ); - if($content !== false) { - $cache->saveData($content); - } - } + // Determine if cached file is within duration + $time = $cache->getTime(); + if ( + $time !== false + && (time() - $duration < $time) + && !Debug::isEnabled() + ) { // Contents within duration + $content = $cache->loadData(); + } else { // Content not within duration + $content = getContents( + $url, + $header ?? [], + $opts ?? [] + ); + if ($content !== false) { + $cache->saveData($content); + } + } - return str_get_html($content, - $lowercase, - $forceTagsClosed, - $target_charset, - $stripRN, - $defaultBRText, - $defaultSpanText); + return str_get_html( + $content, + $lowercase, + $forceTagsClosed, + $target_charset, + $stripRN, + $defaultBRText, + $defaultSpanText + ); } /** @@ -360,49 +370,53 @@ function getSimpleHTMLDOMCached($url, * @param string $url The URL or path to the file. * @return string The MIME type of the file. */ -function getMimeType($url) { - static $mime = null; +function getMimeType($url) +{ + static $mime = null; - if (is_null($mime)) { - // Default values, overriden by /etc/mime.types when present - $mime = array( - 'jpg' => 'image/jpeg', - 'gif' => 'image/gif', - 'png' => 'image/png', - 'image' => 'image/*', - 'mp3' => 'audio/mpeg', - ); - // '@' is used to mute open_basedir warning, see issue #818 - if (@is_readable('/etc/mime.types')) { - $file = fopen('/etc/mime.types', 'r'); - while(($line = fgets($file)) !== false) { - $line = trim(preg_replace('/#.*/', '', $line)); - if(!$line) - continue; - $parts = preg_split('/\s+/', $line); - if(count($parts) == 1) - continue; - $type = array_shift($parts); - foreach($parts as $part) - $mime[$part] = $type; - } - fclose($file); - } - } + if (is_null($mime)) { + // Default values, overriden by /etc/mime.types when present + $mime = [ + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'image' => 'image/*', + 'mp3' => 'audio/mpeg', + ]; + // '@' is used to mute open_basedir warning, see issue #818 + if (@is_readable('/etc/mime.types')) { + $file = fopen('/etc/mime.types', 'r'); + while (($line = fgets($file)) !== false) { + $line = trim(preg_replace('/#.*/', '', $line)); + if (!$line) { + continue; + } + $parts = preg_split('/\s+/', $line); + if (count($parts) == 1) { + continue; + } + $type = array_shift($parts); + foreach ($parts as $part) { + $mime[$part] = $type; + } + } + fclose($file); + } + } - if (strpos($url, '?') !== false) { - $url_temp = substr($url, 0, strpos($url, '?')); - if (strpos($url, '#') !== false) { - $anchor = substr($url, strpos($url, '#')); - $url_temp .= $anchor; - } - $url = $url_temp; - } + if (strpos($url, '?') !== false) { + $url_temp = substr($url, 0, strpos($url, '?')); + if (strpos($url, '#') !== false) { + $anchor = substr($url, strpos($url, '#')); + $url_temp .= $anchor; + } + $url = $url_temp; + } - $ext = strtolower(pathinfo($url, PATHINFO_EXTENSION)); - if (!empty($mime[$ext])) { - return $mime[$ext]; - } + $ext = strtolower(pathinfo($url, PATHINFO_EXTENSION)); + if (!empty($mime[$ext])) { + return $mime[$ext]; + } - return 'application/octet-stream'; + return 'application/octet-stream'; } diff --git a/lib/error.php b/lib/error.php index c2f26247..f9950cea 100644 --- a/lib/error.php +++ b/lib/error.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -20,8 +21,9 @@ * @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes List of HTTP * status codes */ -function returnError($message, $code){ - throw new \Exception($message, $code); +function returnError($message, $code) +{ + throw new \Exception($message, $code); } /** @@ -29,8 +31,9 @@ function returnError($message, $code){ * * @param string $message The error message */ -function returnClientError($message){ - returnError($message, 400); +function returnClientError($message) +{ + returnError($message, 400); } /** @@ -38,8 +41,9 @@ function returnClientError($message){ * * @param string $message The error message */ -function returnServerError($message){ - returnError($message, 500); +function returnServerError($message) +{ + returnError($message, 500); } /** @@ -50,27 +54,28 @@ function returnServerError($message){ * * @return int The total number the same error has appeared */ -function logBridgeError($bridgeName, $code) { - $cacheFac = new CacheFactory(); +function logBridgeError($bridgeName, $code) +{ + $cacheFac = new CacheFactory(); - $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); - $cache->setScope('error_reporting'); - $cache->setkey($bridgeName . '_' . $code); - $cache->purgeCache(86400); // 24 hours + $cache = $cacheFac->create(Configuration::getConfig('cache', 'type')); + $cache->setScope('error_reporting'); + $cache->setkey($bridgeName . '_' . $code); + $cache->purgeCache(86400); // 24 hours - if($report = $cache->loadData()) { - $report = json_decode($report, true); - $report['time'] = time(); - $report['count']++; - } else { - $report = array( - 'error' => $code, - 'time' => time(), - 'count' => 1, - ); - } + if ($report = $cache->loadData()) { + $report = json_decode($report, true); + $report['time'] = time(); + $report['count']++; + } else { + $report = [ + 'error' => $code, + 'time' => time(), + 'count' => 1, + ]; + } - $cache->saveData(json_encode($report)); + $cache->saveData(json_encode($report)); - return $report['count']; + return $report['count']; } diff --git a/lib/html.php b/lib/html.php index 69bd1424..e82d5e0e 100644 --- a/lib/html.php +++ b/lib/html.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** @@ -25,27 +26,29 @@ * @todo Check if this implementation is still necessary, because simplehtmldom * already removes some of the tags (search for `remove_noise` in simple_html_dom.php). */ -function sanitize($html, - $tags_to_remove = array('script', 'iframe', 'input', 'form'), - $attributes_to_keep = array('title', 'href', 'src'), - $text_to_keep = array()){ - - $htmlContent = str_get_html($html); - - foreach($htmlContent->find('*') as $element) { - if(in_array($element->tag, $text_to_keep)) { - $element->outertext = $element->plaintext; - } elseif(in_array($element->tag, $tags_to_remove)) { - $element->outertext = ''; - } else { - foreach($element->getAllAttributes() as $attributeName => $attribute) { - if(!in_array($attributeName, $attributes_to_keep)) - $element->removeAttribute($attributeName); - } - } - } - - return $htmlContent; +function sanitize( + $html, + $tags_to_remove = ['script', 'iframe', 'input', 'form'], + $attributes_to_keep = ['title', 'href', 'src'], + $text_to_keep = [] +) { + $htmlContent = str_get_html($html); + + foreach ($htmlContent->find('*') as $element) { + if (in_array($element->tag, $text_to_keep)) { + $element->outertext = $element->plaintext; + } elseif (in_array($element->tag, $tags_to_remove)) { + $element->outertext = ''; + } else { + foreach ($element->getAllAttributes() as $attributeName => $attribute) { + if (!in_array($attributeName, $attributes_to_keep)) { + $element->removeAttribute($attributeName); + } + } + } + } + + return $htmlContent; } /** @@ -74,23 +77,18 @@ function sanitize($html, * @param string $htmlContent The HTML content * @return string The HTML content with all ocurrences replaced */ -function backgroundToImg($htmlContent) { - - $regex = '/background-image[ ]{0,}:[ ]{0,}url\([\'"]{0,}(.*?)[\'"]{0,}\)/'; - $htmlContent = str_get_html($htmlContent); - - foreach($htmlContent->find('*') as $element) { - - if(preg_match($regex, $element->style, $matches) > 0) { - - $element->outertext = '<img style="display:block;" src="' . $matches[1] . '" />'; - - } - - } - - return $htmlContent; - +function backgroundToImg($htmlContent) +{ + $regex = '/background-image[ ]{0,}:[ ]{0,}url\([\'"]{0,}(.*?)[\'"]{0,}\)/'; + $htmlContent = str_get_html($htmlContent); + + foreach ($htmlContent->find('*') as $element) { + if (preg_match($regex, $element->style, $matches) > 0) { + $element->outertext = '<img style="display:block;" src="' . $matches[1] . '" />'; + } + } + + return $htmlContent; } /** @@ -104,26 +102,27 @@ function backgroundToImg($htmlContent) { * @param string $server Fully qualified URL to the page containing relative links * @return object Content with fixed URLs. */ -function defaultLinkTo($content, $server){ - $string_convert = false; - if (is_string($content)) { - $string_convert = true; - $content = str_get_html($content); - } - - foreach($content->find('img') as $image) { - $image->src = urljoin($server, $image->src); - } - - foreach($content->find('a') as $anchor) { - $anchor->href = urljoin($server, $anchor->href); - } - - if ($string_convert) { - $content = $content->outertext; - } - - return $content; +function defaultLinkTo($content, $server) +{ + $string_convert = false; + if (is_string($content)) { + $string_convert = true; + $content = str_get_html($content); + } + + foreach ($content->find('img') as $image) { + $image->src = urljoin($server, $image->src); + } + + foreach ($content->find('a') as $anchor) { + $anchor->href = urljoin($server, $anchor->href); + } + + if ($string_convert) { + $content = $content->outertext; + } + + return $content; } /** @@ -135,12 +134,13 @@ function defaultLinkTo($content, $server){ * @return string|bool Extracted string, e.g. `John Doe`, or false if the * delimiters were not found. */ -function extractFromDelimiters($string, $start, $end) { - if (strpos($string, $start) !== false) { - $section_retrieved = substr($string, strpos($string, $start) + strlen($start)); - $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end)); - return $section_retrieved; - } return false; +function extractFromDelimiters($string, $start, $end) +{ + if (strpos($string, $start) !== false) { + $section_retrieved = substr($string, strpos($string, $start) + strlen($start)); + $section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end)); + return $section_retrieved; + } return false; } /** @@ -151,13 +151,14 @@ function extractFromDelimiters($string, $start, $end) { * @param string $end End delimiter, e.g. `</script>` * @return string Cleaned string, e.g. `foobar` */ -function stripWithDelimiters($string, $start, $end) { - while(strpos($string, $start) !== false) { - $section_to_remove = substr($string, strpos($string, $start)); - $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end)); - $string = str_replace($section_to_remove, '', $string); - } - return $string; +function stripWithDelimiters($string, $start, $end) +{ + while (strpos($string, $start) !== false) { + $section_to_remove = substr($string, strpos($string, $start)); + $section_to_remove = substr($section_to_remove, 0, strpos($section_to_remove, $end) + strlen($end)); + $string = str_replace($section_to_remove, '', $string); + } + return $string; } /** @@ -170,28 +171,29 @@ function stripWithDelimiters($string, $start, $end) { * * @todo This function needs more documentation to make it maintainable. */ -function stripRecursiveHTMLSection($string, $tag_name, $tag_start){ - $open_tag = '<' . $tag_name; - $close_tag = '</' . $tag_name . '>'; - $close_tag_length = strlen($close_tag); - if(strpos($tag_start, $open_tag) === 0) { - while(strpos($string, $tag_start) !== false) { - $max_recursion = 100; - $section_to_remove = null; - $section_start = strpos($string, $tag_start); - $search_offset = $section_start; - do { - $max_recursion--; - $section_end = strpos($string, $close_tag, $search_offset); - $search_offset = $section_end + $close_tag_length; - $section_to_remove = substr($string, $section_start, $section_end - $section_start + $close_tag_length); - $open_tag_count = substr_count($section_to_remove, $open_tag); - $close_tag_count = substr_count($section_to_remove, $close_tag); - } while ($open_tag_count > $close_tag_count && $max_recursion > 0); - $string = str_replace($section_to_remove, '', $string); - } - } - return $string; +function stripRecursiveHTMLSection($string, $tag_name, $tag_start) +{ + $open_tag = '<' . $tag_name; + $close_tag = '</' . $tag_name . '>'; + $close_tag_length = strlen($close_tag); + if (strpos($tag_start, $open_tag) === 0) { + while (strpos($string, $tag_start) !== false) { + $max_recursion = 100; + $section_to_remove = null; + $section_start = strpos($string, $tag_start); + $search_offset = $section_start; + do { + $max_recursion--; + $section_end = strpos($string, $close_tag, $search_offset); + $search_offset = $section_end + $close_tag_length; + $section_to_remove = substr($string, $section_start, $section_end - $section_start + $close_tag_length); + $open_tag_count = substr_count($section_to_remove, $open_tag); + $close_tag_count = substr_count($section_to_remove, $close_tag); + } while ($open_tag_count > $close_tag_count && $max_recursion > 0); + $string = str_replace($section_to_remove, '', $string); + } + } + return $string; } /** @@ -202,8 +204,8 @@ function stripRecursiveHTMLSection($string, $tag_name, $tag_start){ * @param string $string Input string in Markdown format * @return string output string in HTML format */ -function markdownToHtml($string) { - - $Parsedown = new Parsedown(); - return $Parsedown->text($string); +function markdownToHtml($string) +{ + $Parsedown = new Parsedown(); + return $Parsedown->text($string); } diff --git a/lib/php8backports.php b/lib/php8backports.php index 3b2bb966..30dfdbd9 100644 --- a/lib/php8backports.php +++ b/lib/php8backports.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ // based on https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Str.php @@ -34,19 +35,22 @@ // THE SOFTWARE. if (!function_exists('str_starts_with')) { - function str_starts_with($haystack, $needle) { - return (string)$needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0; - } + function str_starts_with($haystack, $needle) + { + return (string)$needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0; + } } if (!function_exists('str_ends_with')) { - function str_ends_with($haystack, $needle) { - return $needle !== '' && substr($haystack, -strlen($needle)) === (string)$needle; - } + function str_ends_with($haystack, $needle) + { + return $needle !== '' && substr($haystack, -strlen($needle)) === (string)$needle; + } } if (!function_exists('str_contains')) { - function str_contains($haystack, $needle) { - return $needle !== '' && mb_strpos($haystack, $needle) !== false; - } + function str_contains($haystack, $needle) + { + return $needle !== '' && mb_strpos($haystack, $needle) !== false; + } } diff --git a/lib/rssbridge.php b/lib/rssbridge.php index cd156fe8..560c0fe4 100644 --- a/lib/rssbridge.php +++ b/lib/rssbridge.php @@ -1,4 +1,5 @@ <?php + /** * This file is part of RSS-Bridge, a PHP project capable of generating RSS and * Atom feeds for websites that don't have one. @@ -6,9 +7,9 @@ * For the full license information, please view the UNLICENSE file distributed * with this source code. * - * @package Core - * @license http://unlicense.org/ UNLICENSE - * @link https://github.com/rss-bridge/rss-bridge + * @package Core + * @license http://unlicense.org/ UNLICENSE + * @link https://github.com/rss-bridge/rss-bridge */ /** Path to the root folder of RSS-Bridge (where index.php is located) */ @@ -64,19 +65,19 @@ require_once PATH_LIB_VENDOR . 'php-urljoin/src/urljoin.php'; require_once PATH_LIB_VENDOR . 'simplehtmldom/simple_html_dom.php'; spl_autoload_register(function ($className) { - $folders = [ - __DIR__ . '/../actions/', - __DIR__ . '/../bridges/', - __DIR__ . '/../caches/', - __DIR__ . '/../formats/', - __DIR__ . '/../lib/', - ]; - foreach ($folders as $folder) { - $file = $folder . $className . '.php'; - if (is_file($file)) { - require $file; - } - } + $folders = [ + __DIR__ . '/../actions/', + __DIR__ . '/../bridges/', + __DIR__ . '/../caches/', + __DIR__ . '/../formats/', + __DIR__ . '/../lib/', + ]; + foreach ($folders as $folder) { + $file = $folder . $className . '.php'; + if (is_file($file)) { + require $file; + } + } }); Configuration::verifyInstallation(); @@ -5,10 +5,28 @@ <exclude-pattern>./vendor</exclude-pattern> <exclude-pattern>./config.default.ini.php</exclude-pattern> <exclude-pattern>./config.ini.php</exclude-pattern> + + <rule ref="PSR12"> + <exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace"/> + <exclude name="PSR1.Files.SideEffects.FoundWithSymbols"/> + <exclude name="PSR12.Properties.ConstantVisibility.NotFound"/> + </rule> + + <rule ref="Generic.Arrays.DisallowLongArraySyntax" /> + + <rule ref="Squiz.WhiteSpace.FunctionOpeningBraceSpace" /> + + <rule ref="Generic.Files.LineLength"> + <properties> + <property name="lineLimit" value="140"/> + <property name="absoluteLineLimit" value="140"/> + <property name="ignoreComments" value="true"/> + </properties> + </rule> + <!-- Duplicate class names are not allowed --> <rule ref="Generic.Classes.DuplicateClassName"/> - <!-- Statements must not be empty --> - <rule ref="Generic.CodeAnalysis.EmptyStatement"/> + <!-- Unconditional if-statements are not allowed --> <rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/> <!-- Do not use final statements inside final classes --> @@ -24,15 +42,7 @@ <property name="ignoreNewlines" value="true"/> </properties> </rule> - <!-- One line should not have more than 80 characters --> - <!-- One line must never exceed 120 characters --> - <rule ref="Generic.Files.LineLength"> - <properties> - <property name="lineLimit" value="80"/> - <property name="absoluteLineLimit" value="120"/> - <property name="ignoreComments" value="true"/> - </properties> - </rule> + <!-- When calling a function: --> <!-- Do not add a space before the opening parenthesis --> <!-- Do not add a space after the opening parenthesis --> @@ -46,37 +56,7 @@ <rule ref="Generic.PHP.LowerCaseConstant"/> <!-- Use a single string instead of concating --> <rule ref="Generic.Strings.UnnecessaryStringConcat"/> - <!-- Use tabs for indentation --> - <rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/> - <!-- Parameters with default values must appear last in functions --> - <rule ref="PEAR.Functions.ValidDefaultValue"/> - <!-- Use PascalCase for class names --> - <rule ref="PEAR.NamingConventions.ValidClassName"/> - <!-- abstract and final declarations MUST precede the visibility declaration --> - <!-- static declaration MUST come after the visibility declaration --> - <rule ref="PSR2.Methods.MethodDeclaration" /> - <!-- Use 'elseif' instead of 'else if' --> - <rule ref="PSR2.ControlStructures.ElseIfDeclaration"/> - <!-- Do not add spaces after opening or before closing bracket --> - <rule ref="PSR2.ControlStructures.ControlStructureSpacing"/> - <!-- Add a new line at the end of a file --> - <rule ref="PSR2.Files.EndFileNewline"/> - <!-- Add space after closing parenthesis --> - <!-- Add body into new line --> - <!-- Close body in new line --> - <rule ref="Squiz.ControlStructures.ControlSignature"> - <!-- No space after keyword (before opening parenthesis) --> - <exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterKeyword"/> - </rule> - <!-- When declaring a function: --> - <!-- Do not add a space before a comma --> - <!-- Add a space after a comma --> - <!-- Add a space before and after an equal sign --> - <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing"> - <properties> - <property name="equalsSpacing" value="1"/> - </properties> - </rule> + <!-- Do not add spaces when casting --> <rule ref="Squiz.WhiteSpace.CastSpacing"/> <!-- Operators must have a space around them --> @@ -93,13 +73,7 @@ <property name="ignoreBlankLines" value="false"/> </properties> </rule> - <rule ref="Squiz.WhiteSpace.FunctionSpacing"> - <properties> - <property name="spacing" value="1" /> - <property name="spacingBeforeFirst" value="0" /> - <property name="spacingAfterLast" value="0" /> - </properties> - </rule> + <!-- Whenever possible use single quote strings --> <rule ref="Squiz.Strings.DoubleQuoteUsage"> <exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar" /> diff --git a/tests/Actions/ActionImplementationTest.php b/tests/Actions/ActionImplementationTest.php index 0caf6d80..3f063682 100644 --- a/tests/Actions/ActionImplementationTest.php +++ b/tests/Actions/ActionImplementationTest.php @@ -5,54 +5,60 @@ namespace RssBridge\Tests\Actions; use ActionInterface; use PHPUnit\Framework\TestCase; -class ActionImplementationTest extends TestCase { - private $class; - private $obj; - - /** - * @dataProvider dataActionsProvider - */ - public function testClassName($path) { - $this->setAction($path); - $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character'); - $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces'); - $this->assertStringEndsWith('Action', $this->class, 'class name must end with "Action"'); - } - - /** - * @dataProvider dataActionsProvider - */ - public function testClassType($path) { - $this->setAction($path); - $this->assertInstanceOf(ActionInterface::class, $this->obj); - } - - /** - * @dataProvider dataActionsProvider - */ - public function testVisibleMethods($path) { - $allowedMethods = get_class_methods(ActionInterface::class); - sort($allowedMethods); - - $this->setAction($path); - - $methods = get_class_methods($this->obj); - sort($methods); - - $this->assertEquals($allowedMethods, $methods); - } - - public function dataActionsProvider() { - $actions = array(); - foreach (glob(PATH_LIB_ACTIONS . '*.php') as $path) { - $actions[basename($path, '.php')] = array($path); - } - return $actions; - } - - private function setAction($path) { - $this->class = '\\' . basename($path, '.php'); - $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist'); - $this->obj = new $this->class(); - } +class ActionImplementationTest extends TestCase +{ + private $class; + private $obj; + + /** + * @dataProvider dataActionsProvider + */ + public function testClassName($path) + { + $this->setAction($path); + $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character'); + $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces'); + $this->assertStringEndsWith('Action', $this->class, 'class name must end with "Action"'); + } + + /** + * @dataProvider dataActionsProvider + */ + public function testClassType($path) + { + $this->setAction($path); + $this->assertInstanceOf(ActionInterface::class, $this->obj); + } + + /** + * @dataProvider dataActionsProvider + */ + public function testVisibleMethods($path) + { + $allowedMethods = get_class_methods(ActionInterface::class); + sort($allowedMethods); + + $this->setAction($path); + + $methods = get_class_methods($this->obj); + sort($methods); + + $this->assertEquals($allowedMethods, $methods); + } + + public function dataActionsProvider() + { + $actions = []; + foreach (glob(PATH_LIB_ACTIONS . '*.php') as $path) { + $actions[basename($path, '.php')] = [$path]; + } + return $actions; + } + + private function setAction($path) + { + $this->class = '\\' . basename($path, '.php'); + $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist'); + $this->obj = new $this->class(); + } } diff --git a/tests/Actions/ListActionTest.php b/tests/Actions/ListActionTest.php index 1ecf50ed..2eb2049d 100644 --- a/tests/Actions/ListActionTest.php +++ b/tests/Actions/ListActionTest.php @@ -6,85 +6,88 @@ use ActionFactory; use BridgeFactory; use PHPUnit\Framework\TestCase; -class ListActionTest extends TestCase { - - private $data; - - /** - * @runInSeparateProcess - * @requires function xdebug_get_headers - */ - public function testHeaders() { - $this->initAction(); - - $this->assertContains( - 'Content-Type: application/json', - xdebug_get_headers() - ); - } - - /** - * @runInSeparateProcess - */ - public function testOutput() { - $this->initAction(); - - $items = json_decode($this->data, true); - - $this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg()); - - $this->assertArrayHasKey('total', $items, 'Missing "total" parameter'); - $this->assertIsInt($items['total'], 'Invalid type'); - - $this->assertArrayHasKey('bridges', $items, 'Missing "bridges" array'); - - $this->assertEquals( - $items['total'], - count($items['bridges']), - 'Item count doesn\'t match' - ); - - $bridgeFac = new BridgeFactory(); - - $this->assertEquals( - count($bridgeFac->getBridgeNames()), - count($items['bridges']), - 'Number of bridges doesn\'t match' - ); - - $expectedKeys = array( - 'status', - 'uri', - 'name', - 'icon', - 'parameters', - 'maintainer', - 'description' - ); - - $allowedStatus = array( - 'active', - 'inactive' - ); - - foreach($items['bridges'] as $bridge) { - foreach($expectedKeys as $key) { - $this->assertArrayHasKey($key, $bridge, 'Missing key "' . $key . '"'); - } - - $this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value'); - } - } - - private function initAction() { - $actionFac = new ActionFactory(); - - $action = $actionFac->create('list'); - - ob_start(); - $action->execute(); - $this->data = ob_get_contents(); - ob_clean(); - ob_end_flush(); - } +class ListActionTest extends TestCase +{ + private $data; + + /** + * @runInSeparateProcess + * @requires function xdebug_get_headers + */ + public function testHeaders() + { + $this->initAction(); + + $this->assertContains( + 'Content-Type: application/json', + xdebug_get_headers() + ); + } + + /** + * @runInSeparateProcess + */ + public function testOutput() + { + $this->initAction(); + + $items = json_decode($this->data, true); + + $this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg()); + + $this->assertArrayHasKey('total', $items, 'Missing "total" parameter'); + $this->assertIsInt($items['total'], 'Invalid type'); + + $this->assertArrayHasKey('bridges', $items, 'Missing "bridges" array'); + + $this->assertEquals( + $items['total'], + count($items['bridges']), + 'Item count doesn\'t match' + ); + + $bridgeFac = new BridgeFactory(); + + $this->assertEquals( + count($bridgeFac->getBridgeNames()), + count($items['bridges']), + 'Number of bridges doesn\'t match' + ); + + $expectedKeys = [ + 'status', + 'uri', + 'name', + 'icon', + 'parameters', + 'maintainer', + 'description' + ]; + + $allowedStatus = [ + 'active', + 'inactive' + ]; + + foreach ($items['bridges'] as $bridge) { + foreach ($expectedKeys as $key) { + $this->assertArrayHasKey($key, $bridge, 'Missing key "' . $key . '"'); + } + + $this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value'); + } + } + + private function initAction() + { + $actionFac = new ActionFactory(); + + $action = $actionFac->create('list'); + + ob_start(); + $action->execute(); + $this->data = ob_get_contents(); + ob_clean(); + ob_end_flush(); + } } diff --git a/tests/Bridges/BridgeImplementationTest.php b/tests/Bridges/BridgeImplementationTest.php index e0e095a6..60f94d4a 100644 --- a/tests/Bridges/BridgeImplementationTest.php +++ b/tests/Bridges/BridgeImplementationTest.php @@ -7,223 +7,236 @@ use BridgeInterface; use FeedExpander; use PHPUnit\Framework\TestCase; -class BridgeImplementationTest extends TestCase { - private $class; - private $obj; - - /** - * @dataProvider dataBridgesProvider - */ - public function testClassName($path) { - $this->setBridge($path); - $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character'); - $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces'); - $this->assertStringEndsWith('Bridge', $this->class, 'class name must end with "Bridge"'); - } - - /** - * @dataProvider dataBridgesProvider - */ - public function testClassType($path) { - $this->setBridge($path); - $this->assertInstanceOf(BridgeInterface::class, $this->obj); - } - - /** - * @dataProvider dataBridgesProvider - */ - public function testConstants($path) { - $this->setBridge($path); - - $this->assertIsString($this->obj::NAME, 'class::NAME'); - $this->assertNotEmpty($this->obj::NAME, 'class::NAME'); - $this->assertIsString($this->obj::URI, 'class::URI'); - $this->assertNotEmpty($this->obj::URI, 'class::URI'); - $this->assertIsString($this->obj::DESCRIPTION, 'class::DESCRIPTION'); - $this->assertNotEmpty($this->obj::DESCRIPTION, 'class::DESCRIPTION'); - $this->assertIsString($this->obj::MAINTAINER, 'class::MAINTAINER'); - $this->assertNotEmpty($this->obj::MAINTAINER, 'class::MAINTAINER'); - - $this->assertIsArray($this->obj::PARAMETERS, 'class::PARAMETERS'); - $this->assertIsInt($this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT'); - $this->assertGreaterThanOrEqual(0, $this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT'); - } - - /** - * @dataProvider dataBridgesProvider - */ - public function testParameters($path) { - $this->setBridge($path); - - $multiMinimum = 2; - if (isset($this->obj::PARAMETERS['global'])) { - ++$multiMinimum; - } - $multiContexts = (count($this->obj::PARAMETERS) >= $multiMinimum); - $paramsSeen = array(); - - $allowedTypes = array( - 'text', - 'number', - 'list', - 'checkbox' - ); - - foreach($this->obj::PARAMETERS as $context => $params) { - if ($multiContexts) { - $this->assertIsString($context, 'invalid context name'); - - $this->assertNotEmpty($context, 'The context name cannot be empty'); - } - - if (empty($params)) { - continue; - } - - foreach ($paramsSeen as $seen) { - $this->assertNotEquals($seen, $params, 'same set of parameters not allowed'); - } - $paramsSeen[] = $params; - - foreach ($params as $field => $options) { - $this->assertIsString($field, $field . ': invalid id'); - $this->assertNotEmpty($field, $field . ':empty id'); - - $this->assertIsString($options['name'], $field . ': invalid name'); - $this->assertNotEmpty($options['name'], $field . ': empty name'); - - if (isset($options['type'])) { - $this->assertIsString($options['type'], $field . ': invalid type'); - $this->assertContains($options['type'], $allowedTypes, $field . ': unknown type'); - - if ($options['type'] == 'list') { - $this->assertArrayHasKey('values', $options, $field . ': missing list values'); - $this->assertIsArray($options['values'], $field . ': invalid list values'); - $this->assertNotEmpty($options['values'], $field . ': empty list values'); - - foreach ($options['values'] as $valueName => $value) { - $this->assertIsString($valueName, $field . ': invalid value name'); - } - } - } - - if (isset($options['required'])) { - $this->assertIsBool($options['required'], $field . ': invalid required'); - - if($options['required'] === true && isset($options['type'])) { - switch($options['type']) { - case 'list': - case 'checkbox': - $this->assertArrayNotHasKey( - 'required', - $options, - $field . ': "required" attribute not supported for ' . $options['type'] - ); - break; - } - } - } - - if (isset($options['title'])) { - $this->assertIsString($options['title'], $field . ': invalid title'); - $this->assertNotEmpty($options['title'], $field . ': empty title'); - } - - if (isset($options['pattern'])) { - $this->assertIsString($options['pattern'], $field . ': invalid pattern'); - $this->assertNotEquals('', $options['pattern'], $field . ': empty pattern'); - } - - if (isset($options['exampleValue'])) { - if (is_string($options['exampleValue'])) - $this->assertNotEquals('', $options['exampleValue'], $field . ': empty exampleValue'); - } - - if (isset($options['defaultValue'])) { - if (is_string($options['defaultValue'])) - $this->assertNotEquals('', $options['defaultValue'], $field . ': empty defaultValue'); - } - } - } - - foreach($this->obj::TEST_DETECT_PARAMETERS as $url => $params) { - $this->assertEquals($this->obj->detectParameters($url), $params); - } - - $this->assertTrue(true); - } - - /** - * @dataProvider dataBridgesProvider - */ - public function testVisibleMethods($path) { - $allowedBridgeAbstract = get_class_methods(BridgeAbstract::class); - sort($allowedBridgeAbstract); - $allowedFeedExpander = get_class_methods(FeedExpander::class); - sort($allowedFeedExpander); - - $this->setBridge($path); - - $methods = get_class_methods($this->obj); - sort($methods); - if ($this->obj instanceof FeedExpander) { - $this->assertEquals($allowedFeedExpander, $methods); - } else { - $this->assertEquals($allowedBridgeAbstract, $methods); - } - } - - /** - * @dataProvider dataBridgesProvider - */ - public function testMethodValues($path) { - $this->setBridge($path); - - $value = $this->obj->getDescription(); - $this->assertIsString($value, '$class->getDescription()'); - $this->assertNotEmpty($value, '$class->getDescription()'); - - $value = $this->obj->getMaintainer(); - $this->assertIsString($value, '$class->getMaintainer()'); - $this->assertNotEmpty($value, '$class->getMaintainer()'); - - $value = $this->obj->getName(); - $this->assertIsString($value, '$class->getName()'); - $this->assertNotEmpty($value, '$class->getName()'); - - $value = $this->obj->getURI(); - $this->assertIsString($value, '$class->getURI()'); - $this->assertNotEmpty($value, '$class->getURI()'); - - $value = $this->obj->getIcon(); - $this->assertIsString($value, '$class->getIcon()'); - } - - /** - * @dataProvider dataBridgesProvider - */ - public function testUri($path) { - $this->setBridge($path); - - $this->checkUrl($this->obj::URI); - $this->checkUrl($this->obj->getURI()); - } - - public function dataBridgesProvider() { - $bridges = array(); - foreach (glob(PATH_LIB_BRIDGES . '*Bridge.php') as $path) { - $bridges[basename($path, '.php')] = array($path); - } - return $bridges; - } - - private function setBridge($path) { - $this->class = '\\' . basename($path, '.php'); - $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist'); - $this->obj = new $this->class(); - } - - private function checkUrl($url) { - $this->assertNotFalse(filter_var($url, FILTER_VALIDATE_URL), 'no valid URL: ' . $url); - } +class BridgeImplementationTest extends TestCase +{ + private $class; + private $obj; + + /** + * @dataProvider dataBridgesProvider + */ + public function testClassName($path) + { + $this->setBridge($path); + $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character'); + $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces'); + $this->assertStringEndsWith('Bridge', $this->class, 'class name must end with "Bridge"'); + } + + /** + * @dataProvider dataBridgesProvider + */ + public function testClassType($path) + { + $this->setBridge($path); + $this->assertInstanceOf(BridgeInterface::class, $this->obj); + } + + /** + * @dataProvider dataBridgesProvider + */ + public function testConstants($path) + { + $this->setBridge($path); + + $this->assertIsString($this->obj::NAME, 'class::NAME'); + $this->assertNotEmpty($this->obj::NAME, 'class::NAME'); + $this->assertIsString($this->obj::URI, 'class::URI'); + $this->assertNotEmpty($this->obj::URI, 'class::URI'); + $this->assertIsString($this->obj::DESCRIPTION, 'class::DESCRIPTION'); + $this->assertNotEmpty($this->obj::DESCRIPTION, 'class::DESCRIPTION'); + $this->assertIsString($this->obj::MAINTAINER, 'class::MAINTAINER'); + $this->assertNotEmpty($this->obj::MAINTAINER, 'class::MAINTAINER'); + + $this->assertIsArray($this->obj::PARAMETERS, 'class::PARAMETERS'); + $this->assertIsInt($this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT'); + $this->assertGreaterThanOrEqual(0, $this->obj::CACHE_TIMEOUT, 'class::CACHE_TIMEOUT'); + } + + /** + * @dataProvider dataBridgesProvider + */ + public function testParameters($path) + { + $this->setBridge($path); + + $multiMinimum = 2; + if (isset($this->obj::PARAMETERS['global'])) { + ++$multiMinimum; + } + $multiContexts = (count($this->obj::PARAMETERS) >= $multiMinimum); + $paramsSeen = []; + + $allowedTypes = [ + 'text', + 'number', + 'list', + 'checkbox' + ]; + + foreach ($this->obj::PARAMETERS as $context => $params) { + if ($multiContexts) { + $this->assertIsString($context, 'invalid context name'); + + $this->assertNotEmpty($context, 'The context name cannot be empty'); + } + + if (empty($params)) { + continue; + } + + foreach ($paramsSeen as $seen) { + $this->assertNotEquals($seen, $params, 'same set of parameters not allowed'); + } + $paramsSeen[] = $params; + + foreach ($params as $field => $options) { + $this->assertIsString($field, $field . ': invalid id'); + $this->assertNotEmpty($field, $field . ':empty id'); + + $this->assertIsString($options['name'], $field . ': invalid name'); + $this->assertNotEmpty($options['name'], $field . ': empty name'); + + if (isset($options['type'])) { + $this->assertIsString($options['type'], $field . ': invalid type'); + $this->assertContains($options['type'], $allowedTypes, $field . ': unknown type'); + + if ($options['type'] == 'list') { + $this->assertArrayHasKey('values', $options, $field . ': missing list values'); + $this->assertIsArray($options['values'], $field . ': invalid list values'); + $this->assertNotEmpty($options['values'], $field . ': empty list values'); + + foreach ($options['values'] as $valueName => $value) { + $this->assertIsString($valueName, $field . ': invalid value name'); + } + } + } + + if (isset($options['required'])) { + $this->assertIsBool($options['required'], $field . ': invalid required'); + + if ($options['required'] === true && isset($options['type'])) { + switch ($options['type']) { + case 'list': + case 'checkbox': + $this->assertArrayNotHasKey( + 'required', + $options, + $field . ': "required" attribute not supported for ' . $options['type'] + ); + break; + } + } + } + + if (isset($options['title'])) { + $this->assertIsString($options['title'], $field . ': invalid title'); + $this->assertNotEmpty($options['title'], $field . ': empty title'); + } + + if (isset($options['pattern'])) { + $this->assertIsString($options['pattern'], $field . ': invalid pattern'); + $this->assertNotEquals('', $options['pattern'], $field . ': empty pattern'); + } + + if (isset($options['exampleValue'])) { + if (is_string($options['exampleValue'])) { + $this->assertNotEquals('', $options['exampleValue'], $field . ': empty exampleValue'); + } + } + + if (isset($options['defaultValue'])) { + if (is_string($options['defaultValue'])) { + $this->assertNotEquals('', $options['defaultValue'], $field . ': empty defaultValue'); + } + } + } + } + + foreach ($this->obj::TEST_DETECT_PARAMETERS as $url => $params) { + $this->assertEquals($this->obj->detectParameters($url), $params); + } + + $this->assertTrue(true); + } + + /** + * @dataProvider dataBridgesProvider + */ + public function testVisibleMethods($path) + { + $allowedBridgeAbstract = get_class_methods(BridgeAbstract::class); + sort($allowedBridgeAbstract); + $allowedFeedExpander = get_class_methods(FeedExpander::class); + sort($allowedFeedExpander); + + $this->setBridge($path); + + $methods = get_class_methods($this->obj); + sort($methods); + if ($this->obj instanceof FeedExpander) { + $this->assertEquals($allowedFeedExpander, $methods); + } else { + $this->assertEquals($allowedBridgeAbstract, $methods); + } + } + + /** + * @dataProvider dataBridgesProvider + */ + public function testMethodValues($path) + { + $this->setBridge($path); + + $value = $this->obj->getDescription(); + $this->assertIsString($value, '$class->getDescription()'); + $this->assertNotEmpty($value, '$class->getDescription()'); + + $value = $this->obj->getMaintainer(); + $this->assertIsString($value, '$class->getMaintainer()'); + $this->assertNotEmpty($value, '$class->getMaintainer()'); + + $value = $this->obj->getName(); + $this->assertIsString($value, '$class->getName()'); + $this->assertNotEmpty($value, '$class->getName()'); + + $value = $this->obj->getURI(); + $this->assertIsString($value, '$class->getURI()'); + $this->assertNotEmpty($value, '$class->getURI()'); + + $value = $this->obj->getIcon(); + $this->assertIsString($value, '$class->getIcon()'); + } + + /** + * @dataProvider dataBridgesProvider + */ + public function testUri($path) + { + $this->setBridge($path); + + $this->checkUrl($this->obj::URI); + $this->checkUrl($this->obj->getURI()); + } + + public function dataBridgesProvider() + { + $bridges = []; + foreach (glob(PATH_LIB_BRIDGES . '*Bridge.php') as $path) { + $bridges[basename($path, '.php')] = [$path]; + } + return $bridges; + } + + private function setBridge($path) + { + $this->class = '\\' . basename($path, '.php'); + $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist'); + $this->obj = new $this->class(); + } + + private function checkUrl($url) + { + $this->assertNotFalse(filter_var($url, FILTER_VALIDATE_URL), 'no valid URL: ' . $url); + } } diff --git a/tests/Caches/CacheImplementationTest.php b/tests/Caches/CacheImplementationTest.php index 12018685..a3ad5f79 100644 --- a/tests/Caches/CacheImplementationTest.php +++ b/tests/Caches/CacheImplementationTest.php @@ -5,39 +5,44 @@ namespace RssBridge\Tests\Caches; use CacheInterface; use PHPUnit\Framework\TestCase; -class CacheImplementationTest extends TestCase { - private $class; - - /** - * @dataProvider dataCachesProvider - */ - public function testClassName($path) { - $this->setCache($path); - $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character'); - $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces'); - $this->assertStringEndsWith('Cache', $this->class, 'class name must end with "Cache"'); - } - - /** - * @dataProvider dataCachesProvider - */ - public function testClassType($path) { - $this->setCache($path); - $this->assertTrue(is_subclass_of($this->class, CacheInterface::class), 'class must be subclass of CacheInterface'); - } - - //////////////////////////////////////////////////////////////////////////// - - public function dataCachesProvider() { - $caches = array(); - foreach (glob(PATH_LIB_CACHES . '*.php') as $path) { - $caches[basename($path, '.php')] = array($path); - } - return $caches; - } - - private function setCache($path) { - $this->class = '\\' . basename($path, '.php'); - $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist'); - } +class CacheImplementationTest extends TestCase +{ + private $class; + + /** + * @dataProvider dataCachesProvider + */ + public function testClassName($path) + { + $this->setCache($path); + $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character'); + $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces'); + $this->assertStringEndsWith('Cache', $this->class, 'class name must end with "Cache"'); + } + + /** + * @dataProvider dataCachesProvider + */ + public function testClassType($path) + { + $this->setCache($path); + $this->assertTrue(is_subclass_of($this->class, CacheInterface::class), 'class must be subclass of CacheInterface'); + } + + //////////////////////////////////////////////////////////////////////////// + + public function dataCachesProvider() + { + $caches = []; + foreach (glob(PATH_LIB_CACHES . '*.php') as $path) { + $caches[basename($path, '.php')] = [$path]; + } + return $caches; + } + + private function setCache($path) + { + $this->class = '\\' . basename($path, '.php'); + $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist'); + } } diff --git a/tests/Formats/AtomFormatTest.php b/tests/Formats/AtomFormatTest.php index a871ea86..77bb9cbc 100644 --- a/tests/Formats/AtomFormatTest.php +++ b/tests/Formats/AtomFormatTest.php @@ -1,4 +1,5 @@ <?php + /** * AtomFormat - RFC 4287: The Atom Syndication Format * https://tools.ietf.org/html/rfc4287 @@ -10,18 +11,20 @@ require_once __DIR__ . '/BaseFormatTest.php'; use PHPUnit\Framework\TestCase; -class AtomFormatTest extends BaseFormatTest { - private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedAtomFormat/'; +class AtomFormatTest extends BaseFormatTest +{ + private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedAtomFormat/'; - /** - * @dataProvider sampleProvider - * @runInSeparateProcess - */ - public function testOutput(string $name, string $path) { - $data = $this->formatData('Atom', $this->loadSample($path)); - $this->assertNotFalse(simplexml_load_string($data)); + /** + * @dataProvider sampleProvider + * @runInSeparateProcess + */ + public function testOutput(string $name, string $path) + { + $data = $this->formatData('Atom', $this->loadSample($path)); + $this->assertNotFalse(simplexml_load_string($data)); - $expected = self::PATH_EXPECTED . $name . '.xml'; - $this->assertXmlStringEqualsXmlFile($expected, $data); - } + $expected = self::PATH_EXPECTED . $name . '.xml'; + $this->assertXmlStringEqualsXmlFile($expected, $data); + } } diff --git a/tests/Formats/BaseFormatTest.php b/tests/Formats/BaseFormatTest.php index 94da7b04..ace4d3ea 100644 --- a/tests/Formats/BaseFormatTest.php +++ b/tests/Formats/BaseFormatTest.php @@ -5,59 +5,65 @@ namespace RssBridge\Tests\Formats; use PHPUnit\Framework\TestCase; use FormatFactory; -abstract class BaseFormatTest extends TestCase { - protected const PATH_SAMPLES = __DIR__ . '/samples/'; - - /** - * @return array<string, array{string, string}> - */ - public function sampleProvider() { - $samples = []; - foreach (glob(self::PATH_SAMPLES . '*.json') as $path) { - $name = basename($path, '.json'); - $samples[$name] = [ - $name, - $path, - ]; - } - return $samples; - } - - /** - * Cannot be part of the sample returned by sampleProvider since this modifies $_SERVER - * and thus needs to be run in a separate process to avoid side effects. - */ - protected function loadSample(string $path): \stdClass { - $data = json_decode(file_get_contents($path), true); - if (isset($data['meta']) && isset($data['items'])) { - if (!empty($data['server'])) - $this->setServerVars($data['server']); - - $items = array(); - foreach($data['items'] as $item) { - $items[] = new \FeedItem($item); - } - - return (object)array( - 'meta' => $data['meta'], - 'items' => $items, - ); - } else { - $this->fail('invalid test sample: ' . basename($path, '.json')); - } - } - - private function setServerVars(array $list): void { - $_SERVER = array_merge($_SERVER, $list); - } - - protected function formatData(string $formatName, \stdClass $sample): string { - $formatFac = new FormatFactory(); - $format = $formatFac->create($formatName); - $format->setItems($sample->items); - $format->setExtraInfos($sample->meta); - $format->setLastModified(strtotime('2000-01-01 12:00:00 UTC')); - - return $format->stringify(); - } +abstract class BaseFormatTest extends TestCase +{ + protected const PATH_SAMPLES = __DIR__ . '/samples/'; + + /** + * @return array<string, array{string, string}> + */ + public function sampleProvider() + { + $samples = []; + foreach (glob(self::PATH_SAMPLES . '*.json') as $path) { + $name = basename($path, '.json'); + $samples[$name] = [ + $name, + $path, + ]; + } + return $samples; + } + + /** + * Cannot be part of the sample returned by sampleProvider since this modifies $_SERVER + * and thus needs to be run in a separate process to avoid side effects. + */ + protected function loadSample(string $path): \stdClass + { + $data = json_decode(file_get_contents($path), true); + if (isset($data['meta']) && isset($data['items'])) { + if (!empty($data['server'])) { + $this->setServerVars($data['server']); + } + + $items = []; + foreach ($data['items'] as $item) { + $items[] = new \FeedItem($item); + } + + return (object)[ + 'meta' => $data['meta'], + 'items' => $items, + ]; + } else { + $this->fail('invalid test sample: ' . basename($path, '.json')); + } + } + + private function setServerVars(array $list): void + { + $_SERVER = array_merge($_SERVER, $list); + } + + protected function formatData(string $formatName, \stdClass $sample): string + { + $formatFac = new FormatFactory(); + $format = $formatFac->create($formatName); + $format->setItems($sample->items); + $format->setExtraInfos($sample->meta); + $format->setLastModified(strtotime('2000-01-01 12:00:00 UTC')); + + return $format->stringify(); + } } diff --git a/tests/Formats/FormatImplementationTest.php b/tests/Formats/FormatImplementationTest.php index e4501d68..55c6335f 100644 --- a/tests/Formats/FormatImplementationTest.php +++ b/tests/Formats/FormatImplementationTest.php @@ -2,39 +2,44 @@ use PHPUnit\Framework\TestCase; -class FormatImplementationTest extends TestCase { - private $class; - private $obj; +class FormatImplementationTest extends TestCase +{ + private $class; + private $obj; - /** - * @dataProvider dataFormatsProvider - */ - public function testClassName($path) { - $this->setFormat($path); - $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character'); - $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces'); - $this->assertStringEndsWith('Format', $this->class, 'class name must end with "Format"'); - } + /** + * @dataProvider dataFormatsProvider + */ + public function testClassName($path) + { + $this->setFormat($path); + $this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character'); + $this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces'); + $this->assertStringEndsWith('Format', $this->class, 'class name must end with "Format"'); + } - /** - * @dataProvider dataFormatsProvider - */ - public function testClassType($path) { - $this->setFormat($path); - $this->assertInstanceOf(FormatInterface::class, $this->obj); - } + /** + * @dataProvider dataFormatsProvider + */ + public function testClassType($path) + { + $this->setFormat($path); + $this->assertInstanceOf(FormatInterface::class, $this->obj); + } - public function dataFormatsProvider() { - $formats = array(); - foreach (glob(PATH_LIB_FORMATS . '*.php') as $path) { - $formats[basename($path, '.php')] = array($path); - } - return $formats; - } + public function dataFormatsProvider() + { + $formats = []; + foreach (glob(PATH_LIB_FORMATS . '*.php') as $path) { + $formats[basename($path, '.php')] = [$path]; + } + return $formats; + } - private function setFormat($path) { - $this->class = basename($path, '.php'); - $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist'); - $this->obj = new $this->class(); - } + private function setFormat($path) + { + $this->class = basename($path, '.php'); + $this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist'); + $this->obj = new $this->class(); + } } diff --git a/tests/Formats/JsonFormatTest.php b/tests/Formats/JsonFormatTest.php index 3b9f8d47..c21d3f34 100644 --- a/tests/Formats/JsonFormatTest.php +++ b/tests/Formats/JsonFormatTest.php @@ -1,4 +1,5 @@ <?php + /** * JsonFormat - JSON Feed Version 1 * https://jsonfeed.org/version/1 @@ -10,18 +11,20 @@ require_once __DIR__ . '/BaseFormatTest.php'; use PHPUnit\Framework\TestCase; -class JsonFormatTest extends BaseFormatTest { - private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedJsonFormat/'; +class JsonFormatTest extends BaseFormatTest +{ + private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedJsonFormat/'; - /** - * @dataProvider sampleProvider - * @runInSeparateProcess - */ - public function testOutput(string $name, string $path) { - $data = $this->formatData('Json', $this->loadSample($path)); - $this->assertNotNull(json_decode($data), 'invalid JSON output: ' . json_last_error_msg()); + /** + * @dataProvider sampleProvider + * @runInSeparateProcess + */ + public function testOutput(string $name, string $path) + { + $data = $this->formatData('Json', $this->loadSample($path)); + $this->assertNotNull(json_decode($data), 'invalid JSON output: ' . json_last_error_msg()); - $expected = self::PATH_EXPECTED . $name . '.json'; - $this->assertJsonStringEqualsJsonFile($expected, $data); - } + $expected = self::PATH_EXPECTED . $name . '.json'; + $this->assertJsonStringEqualsJsonFile($expected, $data); + } } diff --git a/tests/Formats/MrssFormatTest.php b/tests/Formats/MrssFormatTest.php index 6def6afb..af74923e 100644 --- a/tests/Formats/MrssFormatTest.php +++ b/tests/Formats/MrssFormatTest.php @@ -1,4 +1,5 @@ <?php + /** * MrssFormat - RSS 2.0 + Media RSS * http://www.rssboard.org/rss-specification @@ -11,18 +12,20 @@ require_once __DIR__ . '/BaseFormatTest.php'; use PHPUnit\Framework\TestCase; -class MrssFormatTest extends BaseFormatTest { - private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedMrssFormat/'; +class MrssFormatTest extends BaseFormatTest +{ + private const PATH_EXPECTED = self::PATH_SAMPLES . 'expectedMrssFormat/'; - /** - * @dataProvider sampleProvider - * @runInSeparateProcess - */ - public function testOutput(string $name, string $path) { - $data = $this->formatData('Mrss', $this->loadSample($path)); - $this->assertNotFalse(simplexml_load_string($data)); + /** + * @dataProvider sampleProvider + * @runInSeparateProcess + */ + public function testOutput(string $name, string $path) + { + $data = $this->formatData('Mrss', $this->loadSample($path)); + $this->assertNotFalse(simplexml_load_string($data)); - $expected = self::PATH_EXPECTED . $name . '.xml'; - $this->assertXmlStringEqualsXmlFile($expected, $data); - } + $expected = self::PATH_EXPECTED . $name . '.xml'; + $this->assertXmlStringEqualsXmlFile($expected, $data); + } } |