summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes 'josch' Schauer <josch@debian.org>2018-07-20 13:00:02 +0200
committerJohannes 'josch' Schauer <josch@debian.org>2018-07-20 13:00:02 +0200
commitb93b13c065aab268d0fd12ce423bd45ab203642c (patch)
tree091e6ab2bb819c299922afb9a6177286817f0039
parentcad54b35d336e58278a02e72d2b10b8c14acf120 (diff)
Import upstream version 2018-07-17
-rw-r--r--.dockerignore8
-rw-r--r--.gitignore6
-rw-r--r--Dockerfile5
-rw-r--r--README.md2
-rw-r--r--bridges/AmazonPriceTrackerBridge.php149
-rw-r--r--bridges/Arte7Bridge.php8
-rw-r--r--bridges/BlaguesDeMerdeBridge.php2
-rw-r--r--bridges/CADBridge.php6
-rw-r--r--bridges/ChristianDailyReporterBridge.php25
-rw-r--r--bridges/ContainerLinuxReleasesBridge.php93
-rw-r--r--bridges/CopieDoubleBridge.php2
-rw-r--r--bridges/CourrierInternationalBridge.php2
-rw-r--r--bridges/CpasbienBridge.php2
-rw-r--r--bridges/DanbooruBridge.php2
-rw-r--r--bridges/DansTonChatBridge.php9
-rw-r--r--bridges/DealabsBridge.php339
-rw-r--r--bridges/DemoBridge.php10
-rw-r--r--bridges/DemonoidBridge.php252
-rw-r--r--bridges/DiscogsBridge.php112
-rw-r--r--bridges/ETTVBridge.php142
-rw-r--r--bridges/EZTVBridge.php10
-rw-r--r--bridges/ElloBridge.php146
-rw-r--r--bridges/FB2Bridge.php60
-rw-r--r--bridges/FDroidBridge.php54
-rw-r--r--bridges/FacebookBridge.php366
-rw-r--r--bridges/FootitoBridge.php18
-rw-r--r--bridges/FourchanBridge.php2
-rw-r--r--bridges/GithubIssueBridge.php2
-rw-r--r--bridges/GoComicsBridge.php36
-rw-r--r--bridges/GoogleSearchBridge.php2
-rw-r--r--bridges/GrandComicsDatabaseBridge.php61
-rw-r--r--bridges/HotUKDealsBridge.php1397
-rw-r--r--bridges/IPBBridge.php21
-rw-r--r--bridges/InstagramBridge.php156
-rw-r--r--bridges/IsoHuntBridge.php465
-rw-r--r--bridges/JustETFBridge.php353
-rw-r--r--bridges/KernelBugTrackerBridge.php2
-rw-r--r--bridges/KununuBridge.php10
-rw-r--r--bridges/LeBonCoinBridge.php294
-rw-r--r--bridges/LesJoiesDuCodeBridge.php6
-rw-r--r--bridges/MangareaderBridge.php26
-rw-r--r--bridges/MydealsBridge.php144
-rw-r--r--bridges/NasaApodBridge.php4
-rw-r--r--bridges/NotAlwaysBridge.php57
-rw-r--r--bridges/PinterestBridge.php6
-rw-r--r--bridges/PixivBridge.php73
-rw-r--r--bridges/RadioMelodieBridge.php30
-rw-r--r--bridges/RainbowSixSiegeBridge.php6
-rw-r--r--bridges/ReadComicsBridge.php2
-rw-r--r--bridges/ReporterreBridge.php2
-rw-r--r--bridges/Rue89Bridge.php6
-rw-r--r--bridges/SexactuBridge.php2
-rw-r--r--bridges/ShanaprojectBridge.php2
-rw-r--r--bridges/Shimmie2Bridge.php2
-rw-r--r--bridges/SoundcloudBridge.php2
-rw-r--r--bridges/SteamBridge.php115
-rw-r--r--bridges/SupInfoBridge.php57
-rw-r--r--bridges/SuperSmashBlogBridge.php45
-rw-r--r--bridges/T411Bridge.php96
-rw-r--r--bridges/VkBridge.php269
-rw-r--r--bridges/WhydBridge.php4
-rw-r--r--bridges/WorldOfTanksBridge.php64
-rw-r--r--bridges/YGGTorrentBridge.php143
-rw-r--r--bridges/YoutubeBridge.php38
-rw-r--r--bridges/ZenodoBridge.php55
-rw-r--r--caches/FileCache.php6
-rw-r--r--config.default.ini.php44
-rw-r--r--formats/AtomFormat.php18
-rw-r--r--formats/HtmlFormat.php15
-rw-r--r--formats/MrssFormat.php16
-rw-r--r--index.php81
-rw-r--r--lib/Authentication.php31
-rw-r--r--lib/BridgeAbstract.php16
-rw-r--r--lib/BridgeInterface.php16
-rw-r--r--lib/Configuration.php120
-rw-r--r--lib/Exceptions.php9
-rw-r--r--lib/FeedExpander.php6
-rw-r--r--lib/RssBridge.php2
-rw-r--r--lib/contents.php86
-rw-r--r--lib/html.php37
-rw-r--r--phpcs.xml4
-rw-r--r--static/HtmlFormat.css8
-rw-r--r--static/style.css5
83 files changed, 4996 insertions, 1411 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..15154f0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+.git
+cache/*
+DEBUG
+Dockerfile
+whitelist.txt
+phpcs.xml
+CHANGELOG.md
+CONTRIBUTING.md \ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d28d273..a848b5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -227,8 +227,12 @@ pip-log.txt
/cache
/whitelist.txt
DEBUG
+config.ini.php
######################
## VisualStudioCode ##
######################
-.vscode/* \ No newline at end of file
+.vscode/*
+
+#Builder
+.buildconfig
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..35caac8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,5 @@
+FROM ulsmith/alpine-apache-php7
+
+COPY ./ /app/public/
+
+RUN chown -R apache:root /app/public \ No newline at end of file
diff --git a/README.md b/README.md
index 45bbc6d..3ca495c 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ Requirements
* PHP 5.6, e.g. `AddHandler application/x-httpd-php56 .php` in `.htaccess`
* `openssl` extension enabled in PHP config (`php.ini`)
- * `allow_url_fopen=1` in `php.ini`
+ * `curl` extension enabled in PHP config (`php.ini`)
Enabling/Disabling bridges
===
diff --git a/bridges/AmazonPriceTrackerBridge.php b/bridges/AmazonPriceTrackerBridge.php
new file mode 100644
index 0000000..dd352af
--- /dev/null
+++ b/bridges/AmazonPriceTrackerBridge.php
@@ -0,0 +1,149 @@
+<?php
+
+class AmazonPriceTrackerBridge extends BridgeAbstract {
+ const MAINTAINER = 'captn3m0';
+ 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',
+ 'required' => true,
+ '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',
+ 'United Kingdom' => 'co.uk',
+ 'United States' => 'com',
+ ),
+ 'defaultValue' => 'com',
+ ),
+ ));
+
+ 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();
+ }
+ }
+
+ /**
+ * Returns a generated image tag for the product
+ */
+ private function getImage($html) {
+ $imageSrc = $html->find('#main-image-container img', 0);
+
+ if ($imageSrc) {
+ $imageSrc = $imageSrc ? $imageSrc->getAttribute('data-old-hires') : '';
+ return <<<EOT
+<img width="300" style="max-width:300;max-height:300" src="$imageSrc" 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.');
+ }
+
+ /**
+ * Scrape method for Amazon product page
+ * @return [type] [description]
+ */
+ public function collectData() {
+ $html = $this->getHtml();
+ $this->title = $this->getTitle($html);
+ $imageTag = $this->getImage($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" ... />
+ $currency = $asinData->getAttribute('data-asin-currency-code');
+ $shipping = $asinData->getAttribute('data-asin-shipping');
+ $price = $asinData->getAttribute('data-asin-price');
+
+ $item = array(
+ 'title' => $this->title,
+ 'uri' => $this->getURI(),
+ 'content' => "$imageTag<br/>Price: $price $currency",
+ );
+
+ if ($shipping !== '0') {
+ $item['content'] .= "<br>Shipping: $shipping $currency</br>";
+ }
+
+ $this->items[] = $item;
+ }
+}
diff --git a/bridges/Arte7Bridge.php b/bridges/Arte7Bridge.php
index 1162d17..16952dc 100644
--- a/bridges/Arte7Bridge.php
+++ b/bridges/Arte7Bridge.php
@@ -64,13 +64,11 @@ class Arte7Bridge extends BridgeAbstract {
. $lang
. ($category != null ? '&category.code=' . $category : '');
- $context = array(
- 'http' => array(
- 'header' => 'Authorization: Bearer '. self::API_TOKEN
- )
+ $header = array(
+ 'Authorization: Bearer ' . self::API_TOKEN
);
- $input = getContents($url, false, stream_context_create($context)) or die('Could not request ARTE.');
+ $input = getContents($url, $header) or die('Could not request ARTE.');
$input_json = json_decode($input, true);
foreach($input_json['videos'] as $element) {
diff --git a/bridges/BlaguesDeMerdeBridge.php b/bridges/BlaguesDeMerdeBridge.php
index 25c018a..3ae59a1 100644
--- a/bridges/BlaguesDeMerdeBridge.php
+++ b/bridges/BlaguesDeMerdeBridge.php
@@ -19,7 +19,7 @@ class BlaguesDeMerdeBridge extends BridgeAbstract {
$item['content'] = trim($element->find('div.joke_text_contener', 0)->innertext);
$uri = $temp[2]->href;
$item['uri'] = $uri;
- $item['title'] = substr($uri, (strrpos($uri, "/") + 1));
+ $item['title'] = substr($uri, (strrpos($uri, '/') + 1));
$date = $element->find('li.bdm_date', 0)->innertext;
$time = mktime(0, 0, 0, substr($date, 3, 2), substr($date, 0, 2), substr($date, 6, 4));
$item['timestamp'] = $time;
diff --git a/bridges/CADBridge.php b/bridges/CADBridge.php
index 09e3e65..e88cbbb 100644
--- a/bridges/CADBridge.php
+++ b/bridges/CADBridge.php
@@ -23,14 +23,14 @@ class CADBridge extends FeedExpander {
if($html3 == false)
return 'Daily comic not released yet';
- $htmlpart = explode("/", $url);
+ $htmlpart = explode('/', $url);
switch ($htmlpart[3]) {
case 'cad':
- preg_match_all("/http:\/\/cdn2\.cad-comic\.com\/comics\/cad-\S*png/", $html3, $url2);
+ preg_match_all('/http:\/\/cdn2\.cad-comic\.com\/comics\/cad-\S*png/', $html3, $url2);
break;
case 'sillies':
- preg_match_all("/http:\/\/cdn2\.cad-comic\.com\/comics\/sillies-\S*gif/", $html3, $url2);
+ preg_match_all('/http:\/\/cdn2\.cad-comic\.com\/comics\/sillies-\S*gif/', $html3, $url2);
break;
default:
return 'Daily comic not released yet';
diff --git a/bridges/ChristianDailyReporterBridge.php b/bridges/ChristianDailyReporterBridge.php
new file mode 100644
index 0000000..b8cbf3c
--- /dev/null
+++ b/bridges/ChristianDailyReporterBridge.php
@@ -0,0 +1,25 @@
+<?php
+class ChristianDailyReporterBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'rogerdc';
+ const NAME = 'Christian Daily Reporter Unofficial RSS';
+ const URI = 'https://www.christiandailyreporter.com/';
+ const DESCRIPTION = 'The Unofficial Christian Daily Reporter RSS';
+ // const CACHE_TIMEOUT = 86400; // 1 day
+
+
+ public function collectData() {
+ $uri = 'https://www.christiandailyreporter.com/';
+
+ $html = getSimpleHTMLDOM($uri)
+ or returnServerError('Could not request Christian Daily Reporter.');
+ foreach($html->find('div.top p a,div.column p a') as $element) {
+ $item = array();
+ // Title
+ $item['title'] = $element->innertext;
+ // URL
+ $item['uri'] = $element->href;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/ContainerLinuxReleasesBridge.php b/bridges/ContainerLinuxReleasesBridge.php
new file mode 100644
index 0000000..06c8ac4
--- /dev/null
+++ b/bridges/ContainerLinuxReleasesBridge.php
@@ -0,0 +1,93 @@
+<?php
+class ContainerLinuxReleasesBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'captn3m0';
+ const NAME = 'Core OS Container Linux Releases Bridge';
+ const URI = 'https://coreos.com/releases/';
+ const DESCRIPTION = 'Returns the releases notes for Container Linux';
+
+ const STABLE = 'stable';
+ const BETA = 'beta';
+ const ALPHA = 'alpha';
+
+ const PARAMETERS = [
+ [
+ 'channel' => [
+ 'name' => 'Release Channel',
+ 'type' => 'list',
+ 'required' => true,
+ 'defaultValue' => self::STABLE,
+ 'values' => [
+ 'Stable' => self::STABLE,
+ 'Beta' => self::BETA,
+ 'Alpha' => self::ALPHA,
+ ],
+ ]
+ ]
+ ];
+
+ public function getReleaseFeed($jsonUrl) {
+ $json = getContents($jsonUrl)
+ or returnServerError('Could not request Core OS Website.');
+ return json_decode($json, true);
+ }
+
+ public function collectData() {
+ $data = $this->getReleaseFeed($this->getJsonUri());
+
+ foreach ($data as $releaseVersion => $release) {
+ $item = [];
+
+ $item['uri'] = "https://coreos.com/releases/#$releaseVersion";
+ $item['title'] = $releaseVersion;
+
+ $content = $release['release_notes'];
+ $content .= <<<EOT
+
+Major Software:
+* Kernel: {$release['major_software']['kernel'][0]}
+* Docker: {$release['major_software']['docker'][0]}
+* etcd: {$release['major_software']['etcd'][0]}
+EOT;
+ $item['timestamp'] = strtotime($release['release_date']);
+
+ // Based on https://gist.github.com/jbroadway/2836900
+ // Links
+ $regex = '/\[([^\[]+)\]\(([^\)]+)\)/';
+ $replacement = '<a href=\'\2\'>\1</a>';
+ $item['content'] = preg_replace($regex, $replacement, $content);
+
+ // Headings
+ $regex = '/^(.*)\:\s?$/m';
+ $replacement = '<h3>\1</h3>';
+ $item['content'] = preg_replace($regex, $replacement, $item['content']);
+
+ // Lists
+ $regex = '/\n\s*[\*|\-](.*)/';
+ $item['content'] = preg_replace_callback ($regex, function($regs) {
+ $item = $regs[1];
+ return sprintf ('<ul><li>%s</li></ul>', trim ($item));
+ }, $item['content']);
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function getJsonUri() {
+ $channel = $this->getInput('channel');
+
+ return "https://coreos.com/releases/releases-$channel.json";
+ }
+
+ public function getURI() {
+ return self::URI;
+ }
+
+ public function getName(){
+ if(!is_null($this->getInput('channel'))) {
+ return 'Container Linux Releases: ' . $this->getInput('channel') . ' Channel';
+ }
+
+ return parent::getName();
+ }
+}
diff --git a/bridges/CopieDoubleBridge.php b/bridges/CopieDoubleBridge.php
index 767cdce..3545c6f 100644
--- a/bridges/CopieDoubleBridge.php
+++ b/bridges/CopieDoubleBridge.php
@@ -25,7 +25,7 @@ class CopieDoubleBridge extends BridgeAbstract {
} 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('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 1573863..1e7c93e 100644
--- a/bridges/CourrierInternationalBridge.php
+++ b/bridges/CourrierInternationalBridge.php
@@ -11,7 +11,7 @@ class CourrierInternationalBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Error.');
- $element = $html->find("article");
+ $element = $html->find('article');
$article_count = 1;
foreach($element as $article) {
diff --git a/bridges/CpasbienBridge.php b/bridges/CpasbienBridge.php
index 19efd84..f9b4b50 100644
--- a/bridges/CpasbienBridge.php
+++ b/bridges/CpasbienBridge.php
@@ -16,7 +16,7 @@ class CpasbienBridge extends BridgeAbstract {
));
public function collectData(){
- $request = str_replace(" ", "-", trim($this->getInput('q')));
+ $request = str_replace(' ', '-', trim($this->getInput('q')));
$html = getSimpleHTMLDOM(self::URI . '/recherche/' . urlencode($request) . '.html')
or returnServerError('No results for this query.');
diff --git a/bridges/DanbooruBridge.php b/bridges/DanbooruBridge.php
index 36b8c08..82f2167 100644
--- a/bridges/DanbooruBridge.php
+++ b/bridges/DanbooruBridge.php
@@ -41,7 +41,7 @@ class DanbooruBridge extends BridgeAbstract {
$item = array();
$item['uri'] = $element->find('a', 0)->href;
- $item['postid'] = (int)preg_replace("/[^0-9]/", '', $element->getAttribute(static::IDATTRIBUTE));
+ $item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src;
$item['tags'] = $this->getTags($element);
diff --git a/bridges/DansTonChatBridge.php b/bridges/DansTonChatBridge.php
index 545f162..0983bff 100644
--- a/bridges/DansTonChatBridge.php
+++ b/bridges/DansTonChatBridge.php
@@ -15,8 +15,13 @@ class DansTonChatBridge extends BridgeAbstract {
foreach($html->find('div.item') as $element) {
$item = array();
$item['uri'] = $element->find('a', 0)->href;
- $item['title'] = 'DansTonChat ' . $element->find('a', 1)->plaintext;
- $item['content'] = $element->find('a', 0)->innertext;
+ $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/DealabsBridge.php b/bridges/DealabsBridge.php
index d2cab2f..fb88f37 100644
--- a/bridges/DealabsBridge.php
+++ b/bridges/DealabsBridge.php
@@ -1,8 +1,9 @@
<?php
-class DealabsBridge extends BridgeAbstract {
- const NAME = 'Dealabs search bridge';
+class DealabsBridge extends PepperBridgeAbstract {
+
+ const NAME = 'Dealabs Bridge';
const URI = 'https://www.dealabs.com/';
- const DESCRIPTION = 'Return the Dealabs search result using keywords';
+ const DESCRIPTION = 'Affiche les Deals de Dealabs';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Recherche par Mot(s) clé(s)' => array (
@@ -39,7 +40,7 @@ class DealabsBridge extends BridgeAbstract {
),
'Deals par groupe' => array(
- 'groupe' => array(
+ 'group' => array(
'name' => 'Groupe',
'type' => 'list',
'required' => 'true',
@@ -61,10 +62,10 @@ class DealabsBridge extends BridgeAbstract {
'Services divers' => 'services-divers',
'Sports & plein air' => 'sports-plein-air',
'Téléphonie' => 'telephonie',
- 'Voyages & sorties' => 'voyages-sorties-restaurants'
+ 'Voyages & sorties' => 'voyages-sorties-restaurants',
)
),
- 'ordre' => array(
+ 'order' => array(
'name' => 'Trier par',
'type' => 'list',
'required' => 'true',
@@ -78,37 +79,99 @@ class DealabsBridge extends BridgeAbstract {
)
);
+ public $lang = array(
+ 'bridge-uri' => SELF::URI,
+ 'bridge-name' => SELF::NAME,
+ 'context-keyword' => 'Recherche par Mot(s) clé(s)',
+ 'context-group' => 'Deals par groupe',
+ 'uri-group' => '/groupe/',
+ 'request-error' => 'Could not request Dealabs',
+ 'no-results' => 'Il n&#039;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',
+ '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'
+ ),
+ );
+
+
+
+}
+
+class PepperBridgeAbstract extends BridgeAbstract {
+
const CACHE_TIMEOUT = 3600;
public function collectData(){
switch($this->queriedContext) {
- case 'Recherche par Mot(s) clé(s)':
- return $this->collectDataMotsCles();
+ case $this->i8n('context-keyword'):
+ return $this->collectDataKeywords();
break;
- case 'Deals par groupe':
- return $this->collectDataGroupe();
+ case $this->i8n('context-group'):
+ return $this->collectDataGroup();
break;
}
}
/**
- * Get the Deal data from the choosen groupe in the choose order
+ * Get the Deal data from the choosen group in the choosed order
*/
- public function collectDataGroupe()
+ public function collectDataGroup()
{
- $groupe = $this->getInput('groupe');
- $ordre = $this->getInput('ordre');
+ $group = $this->getInput('group');
+ $order = $this->getInput('order');
- $url = self::URI
- . '/groupe/' . $groupe . $ordre;
+ $url = $this->i8n('bridge-uri')
+ . $this->i8n('uri-group') . $group . $order;
$this->collectDeals($url);
}
/**
* Get the Deal data from the choosen keywords and parameters
*/
- public function collectDataMotsCles()
+ public function collectDataKeywords()
{
$q = $this->getInput('q');
$hide_expired = $this->getInput('hide_expired');
@@ -117,7 +180,7 @@ class DealabsBridge extends BridgeAbstract {
$priceTo = $this->getInput('priceFrom');
/* Even if the original website uses POST with the search page, GET works too */
- $url = self::URI
+ $url = $this->i8n('bridge-uri')
. '/search/advanced?q='
. urlencode($q)
. '&hide_expired='. $hide_expired
@@ -138,8 +201,8 @@ class DealabsBridge extends BridgeAbstract {
*/
public function collectDeals($url){
$html = getSimpleHTMLDOM($url)
- or returnServerError('Could not request Dealabs.');
- $list = $html->find('article');
+ or returnServerError($this->i8n('request-error'));
+ $list = $html->find('article[id]');
// Deal Image Link CSS Selector
$selectorImageLink = implode(
@@ -148,7 +211,6 @@ class DealabsBridge extends BridgeAbstract {
'cept-thread-image-link',
'imgFrame',
'imgFrame--noBorder',
- 'box--all-i',
'thread-listImgCell',
)
);
@@ -160,9 +222,6 @@ class DealabsBridge extends BridgeAbstract {
'cept-tt',
'thread-link',
'linkPlain',
- 'space--r-1',
- 'size--all-s',
- 'size--fromW3-m',
)
);
@@ -184,7 +243,7 @@ class DealabsBridge extends BridgeAbstract {
'cept-description-container',
'overflow--wrap-break',
'size--all-s',
- 'size--fromW3-m',
+ 'size--fromW3-m'
)
);
@@ -194,7 +253,6 @@ class DealabsBridge extends BridgeAbstract {
array(
'size--all-s',
'flex',
- 'flex--wrap',
'flex--justify-e',
'flex--grow-1',
)
@@ -202,40 +260,44 @@ class DealabsBridge extends BridgeAbstract {
// 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 && $noresult->plaintext == 'Il n&#039;y a rien à afficher pour le moment :(') {
+ if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
$this->items = array();
} else {
- foreach($list as $deal) {
+ foreach ($list as $deal) {
$item = array();
$item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href;
- $item['title'] = $deal->find('a[class='. $selectorLink .']', 0
- )->plaintext;
+ $item['title'] = $deal->find('a[class*='. $selectorLink .']', 0
+ )->plaintext;
$item['author'] = $deal->find('span.thread-username', 0)->plaintext;
$item['content'] = '<table><tr><td><a href="'
. $deal->find(
'a[class*='. $selectorImageLink .']', 0)->href
- . '"><img src="'
- . $this->getImage($deal)
- . '"/></td><td><h2><a href="'
- . $deal->find('a[class='. $selectorLink .']', 0)->href
- . '">'
- . $deal->find('a[class='. $selectorLink .']', 0)->innertext
- . '</a></h2>'
- . $this->getPrix($deal)
- . $this->getReduction($deal)
- . $this->getExpedition($deal)
- . $this->getLivraison($deal)
- . $this->getOrigine($deal)
- . $deal->find('div[class='. $selectorDescription .']', 0)->innertext
- . '</td><td>'
- . $deal->find('div[class='. $selectorHot .']', 0)->children(0)->outertext
- . '</td></table>';
- $dealDateDiv = $deal->find('div[class='. $selectorDate .']', 0)
+ . '"><img src="'
+ . $this->getImage($deal)
+ . '"/></td><td><h2><a href="'
+ . $deal->find('a[class*='. $selectorLink .']', 0)->href
+ . '">'
+ . $deal->find('a[class*='. $selectorLink .']', 0)->innertext
+ . '</a></h2>'
+ . $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)->children(0)->outertext
+ . '</td></table>';
+ $dealDateDiv = $deal->find('div[class*='. $selectorDate .']', 0)
->find('span[class=hide--toW3]');
$itemDate = end($dealDateDiv)->plaintext;
- if(substr( $itemDate, 0, 6 ) === 'il y a') {
+ // 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 {
+ } else {
$item['timestamp'] = $this->parseDate($itemDate);
}
$this->items[] = $item;
@@ -244,14 +306,28 @@ class DealabsBridge extends BridgeAbstract {
}
/**
+ * 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 getPrix($deal)
+ private function getPrice($deal)
{
- if($deal->find(
+ if ($deal->find(
'span[class*=thread-price]', 0) != null) {
- return '<div>Prix : '
+ return '<div>'.$this->i8n('price') .' : '
. $deal->find(
'span[class*=thread-price]', 0
)->plaintext
@@ -266,17 +342,17 @@ class DealabsBridge extends BridgeAbstract {
* Get the Shipping costs from a Deal if it exists
* @return string String of the deal shipping Cost
*/
- private function getLivraison($deal)
+ private function getShippingCost($deal)
{
- if($deal->find('span[class*=cept-shipping-price]', 0) != null) {
- if($deal->find('span[class*=cept-shipping-price]', 0)->children(0) != null) {
- return '<div>Livraison : '
- . $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
- . '</div>';
+ if ($deal->find('span[class*=cept-shipping-price]', 0) != null) {
+ if ($deal->find('span[class*=cept-shipping-price]', 0)->children(0) != null) {
+ return '<div>'. $this->i8n('shipping') .' : '
+ . $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
+ . '</div>';
} else {
- return '<div>Livraison : '
- . $deal->find('span[class*=cept-shipping-price]', 0)->innertext
- . '</div>';
+ return '<div>'. $this->i8n('shipping') .' : '
+ . $deal->find('span[class*=cept-shipping-price]', 0)->innertext
+ . '</div>';
}
} else {
return '';
@@ -287,10 +363,10 @@ class DealabsBridge extends BridgeAbstract {
* Get the source of a Deal if it exists
* @return string String of the deal source
*/
- private function getOrigine($deal)
+ private function GetSource($deal)
{
- if($deal->find('a[class=text--color-greyShade]', 0) != null) {
- return '<div>Origine : '
+ 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 {
@@ -302,15 +378,21 @@ class DealabsBridge extends BridgeAbstract {
* Get the original Price and discout from a Deal if it exists
* @return string String of the deal original price and discount
*/
- private function getReduction($deal)
+ private function getDiscount($deal)
{
- if($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
- return '<div>Réduction : <span style="text-decoration: line-through;">'
+ 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
+ )->plaintext
. '</span>&nbsp;'
- . $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0)->plaintext
+ . $discount
. '</div>';
} else {
return '';
@@ -323,7 +405,6 @@ class DealabsBridge extends BridgeAbstract {
*/
private function getImage($deal)
{
-
$selectorLazy = implode(
' ', /* Notice this is a space! */
array(
@@ -337,7 +418,7 @@ class DealabsBridge extends BridgeAbstract {
)
);
- $selectorPlain = implode(
+ $selectorPlain = implode(
' ', /* Notice this is a space! */
array(
'thread-image',
@@ -347,21 +428,21 @@ class DealabsBridge extends BridgeAbstract {
'cept-thread-img'
)
);
- if($deal->find('img[class='. $selectorLazy .']', 0) != null) {
+ 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'};
+ ->getAttribute('data-lazy-img')))->{'src'};
} else {
- return $deal->find('img[class='. $selectorPlain .']', 0 )->src;
+ return $deal->find('img[class*='. $selectorPlain .']', 0 )->src;
}
}
/**
- * Get the originating country from a Deal if it existsa
+ * Get the originating country from a Deal if it exists
* @return string String of the deal originating country
*/
- private function getExpedition($deal)
+ private function getShipsFrom($deal)
{
$selector = implode(
' ', /* Notice this is a space! */
@@ -372,7 +453,7 @@ class DealabsBridge extends BridgeAbstract {
'text--color-greyShade'
)
);
- if($deal->find('span[class='. $selector .']', 0) != null) {
+ if ($deal->find('span[class='. $selector .']', 0) != null) {
return '<div>'
. $deal->find('span[class='. $selector .']', 0)->children(2)->plaintext
. '</div>';
@@ -382,25 +463,12 @@ class DealabsBridge extends BridgeAbstract {
}
/**
- * Transforms a French date into a timestam
+ * Transforms a local date into a timestamp
* @return int timestamp of the input date
*/
private function parseDate($string)
{
- $month_fr = array(
- 'janvier',
- 'février',
- 'mars',
- 'avril',
- 'mai',
- 'juin',
- 'juillet',
- 'août',
- 'septembre',
- 'octobre',
- 'novembre',
- 'décembre'
- );
+ $month_local = $this->i8n('local-months');
$month_en = array(
'January',
'February',
@@ -415,11 +483,18 @@ class DealabsBridge extends BridgeAbstract {
'November',
'December'
);
- $date_str = trim(str_replace($month_fr, $month_en, $string));
- if(!preg_match('/[0-9]{4}/', $string)) {
+ // 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);
@@ -427,21 +502,41 @@ class DealabsBridge extends BridgeAbstract {
}
/**
- * Transforms a relate French date into a timestam
+ * 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();
- $search = array(
- 'il y a ',
- 'min',
- 'h',
- 'jour',
- 'jours',
- 'mois',
- 'ans',
- 'et '
- );
+
+ // 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',
@@ -456,18 +551,38 @@ class DealabsBridge extends BridgeAbstract {
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 'Recherche par Mot(s) clé(s)':
- return self::NAME . ' - Recherche : '. $this->getInput('q');
+ case $this->i8n('context-keyword'):
+ return $this->i8n('bridge-name') . ' - '. $this->i8n('title-keyword') .' : '. $this->getInput('q');
break;
- case 'Deals par groupe':
- $values = self::PARAMETERS['Deals par groupe']['groupe']['values'];
- $groupe = array_search($this->getInput('groupe'), $values);
- return self::NAME . ' - Groupe : '. $groupe;
+ 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;
default: // Return default value
- return self::NAME;
+ return static::NAME;
+ }
+ }
+
+
+
+ /**
+ * 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
+ */
+ public function i8n($key)
+ {
+ if (array_key_exists($key, $this->lang)) {
+ return $this->lang[$key];
+ } else {
+ return null;
}
}
diff --git a/bridges/DemoBridge.php b/bridges/DemoBridge.php
index ea2088e..f48b451 100644
--- a/bridges/DemoBridge.php
+++ b/bridges/DemoBridge.php
@@ -35,11 +35,11 @@ class DemoBridge extends BridgeAbstract {
public function collectData(){
$item = array();
- $item['author'] = "Me!";
- $item['title'] = "Test";
- $item['content'] = "Awesome content !";
- $item['id'] = "Lalala";
- $item['uri'] = "http://example.com/test";
+ $item['author'] = 'Me!';
+ $item['title'] = 'Test';
+ $item['content'] = 'Awesome content !';
+ $item['id'] = 'Lalala';
+ $item['uri'] = 'http://example.com/test';
$this->items[] = $item;
}
diff --git a/bridges/DemonoidBridge.php b/bridges/DemonoidBridge.php
index f99b80f..bfbb785 100644
--- a/bridges/DemonoidBridge.php
+++ b/bridges/DemonoidBridge.php
@@ -4,143 +4,163 @@ class DemonoidBridge extends BridgeAbstract {
const MAINTAINER = 'metaMMA';
const NAME = 'Demonoid';
const URI = 'https://www.demonoid.pw/';
- const DESCRIPTION = 'Returns results for the keywords (in all categories or
- a specific category). You can put several keywords separated by a semicolon
- (e.g. "one show;another show"). Searches can by done in a specific category;
- category number must be specified. (All=0, Movies=1, Music=2, TV=3, Games=4,
- Applications=5, Pictures=8, Anime=9, Comics=10, Books=11 Music Videos=8,
- Audio Books=17). User feed takes the Uploader ID number (not uploader name)
- as keyword. Uploader ID is found by clicking on uploader, clicking on
- "View this user\'s torrents", and copying the number after "uid=". An entire
- category feed is accomplished by leaving "keywords" box blank and using the
- corresponding category number.';
+ const DESCRIPTION = 'Returns results from search';
- const PARAMETERS = array( array(
+ const PARAMETERS = array(array(
'q' => array(
- 'name' => 'keywords/user ID/category, separated by semicolons',
- 'exampleValue' => 'first list;second list;…',
- 'required' => true
- ),
- 'crit' => array(
+ 'name' => 'keywords',
+ 'exampleValue' => 'keyword1 keyword2…',
+ 'required' => true,
+ ),
+ 'category' => array(
+ 'name' => 'Category',
'type' => 'list',
- 'name' => 'Feed type',
'values' => array(
- 'search' => 'search',
- 'category' => 'cat',
- 'user' => 'usr'
+ 'All' => 0,
+ 'Movies' => 1,
+ 'Music' => 2,
+ 'TV' => 3,
+ 'Games' => 4,
+ 'Applications' => 5,
+ 'Pictures' => 8,
+ 'Anime' => 9,
+ 'Comics' => 10,
+ 'Books' => 11,
+ 'Audiobooks' => 17
+ )
)
- ),
- 'catCheck' => array(
- 'type' => 'checkbox',
- 'name' => 'Specify category for keyword search ?',
- ),
- 'cat' => array(
- 'name' => 'Category number',
- ),
- ));
+ ), array(
+ 'catOnly' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'All' => 0,
+ 'Movies' => 1,
+ 'Music' => 2,
+ 'TV' => 3,
+ 'Games' => 4,
+ 'Applications' => 5,
+ 'Pictures' => 8,
+ 'Anime' => 9,
+ 'Comics' => 10,
+ 'Books' => 11,
+ 'Audiobooks' => 17
+ )
+ )
+ ), array(
+ 'userid' => array(
+ 'name' => 'user id',
+ 'exampleValue' => '00000',
+ 'required' => true,
+ 'type' => 'number'
+ ),
+ 'category' => array(
+ 'name' => 'Category',
+ 'type' => 'list',
+ 'values' => array(
+ 'All' => 0,
+ 'Movies' => 1,
+ 'Music' => 2,
+ 'TV' => 3,
+ 'Games' => 4,
+ 'Applications' => 5,
+ 'Pictures' => 8,
+ 'Anime' => 9,
+ 'Comics' => 10,
+ 'Books' => 11,
+ 'Audiobooks' => 17
+ )
+ )
+ )
+ );
public function collectData() {
- $catBool = $this->getInput('catCheck');
- if($catBool) {
- $catNum = $this->getInput('cat');
- }
- $critList = $this->getInput('crit');
+ if(!empty($this->getInput('q'))) {
- $keywordsList = explode(';', $this->getInput('q'));
- foreach($keywordsList as $keywords) {
- switch($critList) {
- case 'search':
- if($catBool == false) {
- $html = file_get_contents(
- self::URI .
- 'files/?category=0&subcategory=All&quality=All&seeded=2&external=2&query=' .
- urlencode($keywords) . #not rawurlencode so space -> '+'
- '&uid=0&sort='
- ) or returnServerError('Could not request Demonoid.');
- } else {
- $html = file_get_contents(
- self::URI .
- 'files/?category=' .
- rawurlencode($catNum) .
- '&subcategory=All&quality=All&seeded=2&external=2&query=' .
- urlencode($keywords) . #not rawurlencode so space -> '+'
- '&uid=0&sort='
- ) or returnServerError('Could not request Demonoid.');
- }
- break;
- case 'usr':
- $html = file_get_contents(
- self::URI .
- 'files/?uid=' .
- rawurlencode($keywords) .
- '&seeded=2'
- ) or returnServerError('Could not request Demonoid.');
- break;
- case 'cat':
- $html = file_get_contents(
- self::URI .
- 'files/?uid=0&category=' .
- rawurlencode($keywords) .
- '&subcategory=0&language=0&seeded=2&quality=0&query=&sort='
- ) or returnServerError('Could not request Demonoid.');
- break;
- }
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'files/?category=' .
+ rawurlencode($this->getInput('category')) .
+ '&subcategory=All&quality=All&seeded=2&external=2&query=' .
+ urlencode($this->getInput('q')) .
+ '&uid=0&sort='
+ ) or returnServerError('Could not request Demonoid.');
- if(preg_match('~No torrents found~', $html)) {
- returnServerError('No result for query ' . $keywords);
- }
+ } elseif(!empty($this->getInput('catOnly'))) {
+
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'files/?uid=0&category=' .
+ rawurlencode($this->getInput('catOnly')) .
+ '&subcategory=0&language=0&seeded=2&quality=0&query=&sort='
+ ) or returnServerError('Could not request Demonoid.');
- $bigTable = explode('<!-- start torrent list -->', $html)[1];
- $last50 = explode('<!-- end torrent list -->', $bigTable)[0];
- $dateChunk = explode('added_today', $last50);
- $item = array ();
+ } elseif(!empty($this->getInput('userid'))) {
- for($block = 1;$block < count($dateChunk);$block++) {
- preg_match('~(?<=>Add).*?(?=<)~', $dateChunk[$block], $dateStr);
+ $html = getSimpleHTMLDOM(
+ self::URI .
+ 'files/?uid=' .
+ rawurlencode($this->getInput('userid')) .
+ '&seeded=2'
+ ) or returnServerError('Could not request Demonoid.');
+
+ } else {
+ returnServerError('Invalid parameters !');
+ }
+
+ if(preg_match('~No torrents found~', $html)) {
+ return;
+ }
+
+ $table = $html->find('td[class=ctable_content_no_pad]', 0);
+ $cursorCount = 4;
+ $elementCount = 0;
+ while($elementCount != 40) {
+ $elementCount++;
+ $currentElement = $table->find('tr', $cursorCount);
+ if(preg_match('~items total~', $currentElement)) {
+ break;
+ }
+ $item = array();
+ //Do we have a date ?
+ if(preg_match('~Added.*?(.*)~', $currentElement->plaintext, $dateStr)) {
if(preg_match('~today~', $dateStr[0])) {
date_default_timezone_set('UTC');
$timestamp = mktime(0, 0, 0, gmdate('n'), gmdate('j'), gmdate('Y'));
- } else {
- preg_match('~(?<=ed on ).*\d+~', $dateStr[0], $fullDateStr);
+ } else {
+ preg_match('~(?<=ed on ).*\d+~', $currentElement->plaintext, $fullDateStr);
date_default_timezone_set('UTC');
$dateObj = strptime($fullDateStr[0], '%A, %b %d, %Y');
$timestamp = mktime(0, 0, 0, $dateObj['tm_mon'] + 1, $dateObj['tm_mday'], 1900 + $dateObj['tm_year']);
}
+ $cursorCount++;
+ }
- $itemsChunk = explode('<!-- tstart -->', $dateChunk[$block]);
+ $content = $table->find('tr', $cursorCount)->find('a', 1);
+ $cursorCount++;
+ $torrentInfo = $table->find('tr', $cursorCount);
+ $item['timestamp'] = $timestamp;
+ $item['title'] = $content->plaintext;
+ $item['id'] = self::URI . $content->href;
+ $item['uri'] = self::URI . $content->href;
+ $item['author'] = $torrentInfo->find('a[class=user]', 0)->plaintext;
+ $item['seeders'] = $torrentInfo->find('font[class=green]', 0)->plaintext;
+ $item['leechers'] = $torrentInfo->find('font[class=red]', 0)->plaintext;
+ $item['size'] = $torrentInfo->find('td', 3)->plaintext;
+ $item['content'] = 'Uploaded by ' . $item['author']
+ . ' , Size ' . $item['size']
+ . '<br>seeders: '
+ . $item['seeders']
+ . ' | leechers: '
+ . $item['leechers']
+ . '<br><a href="'
+ . $item['id']
+ . '">info page</a>';
- for($items = 1;$items < count($itemsChunk);$items++) {
- $item = array();
- $cols = explode('<td', $itemsChunk[$items]);
- preg_match('~(?<=href=\"/).*?(?=\")~', $cols[1], $matches);
- $item['id'] = self::URI . $matches[0];
- preg_match('~(?<=href=\").*?(?=\")~', $cols[4], $matches);
- $item['uri'] = $matches[0];
- $item['timestamp'] = $timestamp;
- preg_match('~(?<=href=\"/users/).*?(?=\")~', $cols[3], $matches);
- $item['author'] = $matches[0];
- preg_match('~(?<=/\">).*?(?=</a>)~', $cols[1], $matches);
- $item['title'] = $matches[0];
- preg_match('~(?<=green\">)\d+(?=</font>)~', $cols[8], $matches);
- $item['seeders'] = $matches[0];
- preg_match('~(?<=red\">)\d+(?=</font>)~', $cols[9], $matches);
- $item['leechers'] = $matches[0];
- preg_match('~(?<=>).*?(?=</td>)~', $cols[5], $matches);
- $item['size'] = $matches[0];
- $item['content'] = 'Uploaded by ' . $item['author']
- . ' , Size ' . $item['size']
- . '<br>seeders: '
- . $item['seeders']
- . ' | leechers: '
- . $item['leechers']
- . '<br><a href="'
- . $item['id']
- . '">info page</a>';
- if(isset($item['title']))
- $this->items[] = $item;
- }
- }
+ $this->items[] = $item;
+
+ $cursorCount++;
}
}
}
diff --git a/bridges/DiscogsBridge.php b/bridges/DiscogsBridge.php
new file mode 100644
index 0000000..9fe4f51
--- /dev/null
+++ b/bridges/DiscogsBridge.php
@@ -0,0 +1,112 @@
+<?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',
+ )
+ ),
+ 'Label Releases' => array(
+ 'labelid' => array(
+ 'name' => 'Label ID',
+ 'type' => 'number',
+ )
+ ),
+ 'User Wantlist' => array(
+ 'username_wantlist' => array(
+ 'name' => 'Username',
+ 'type' => 'text',
+ )
+ ),
+ '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')
+ or returnServerError('Unable to query discogs !');
+ } elseif(!empty($this->getInput('labelid'))) {
+ $data = getContents('https://api.discogs.com/labels/'
+ . $this->getInput('labelid')
+ . '/releases?sort=year&sort_order=desc')
+ or returnServerError('Unable to query discogs !');
+ }
+
+ $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;
+ $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')
+ or returnServerError('Unable to query discogs !');
+ $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')
+ or returnServerError('Unable to query discogs !');
+ $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;
+ }
+}
diff --git a/bridges/ETTVBridge.php b/bridges/ETTVBridge.php
new file mode 100644
index 0000000..ab90bf7
--- /dev/null
+++ b/bridges/ETTVBridge.php
@@ -0,0 +1,142 @@
+<?php
+class ETTVBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'GregThib';
+ const NAME = 'ETTV';
+ const URI = 'https://www.ettv.tv/';
+ const DESCRIPTION = 'Returns list of 20 latest torrents for a specific search.';
+ const CACHE_TIMEOUT = 14400; // 4 hours
+
+ const PARAMETERS = array( array(
+ 'query' => array(
+ 'name' => 'Keywords',
+ 'required' => true
+ ),
+ 'cat' => array(
+ 'type' => 'list',
+ 'name' => 'Category',
+ 'values' => array(
+ '(ALL TYPES)' => '0',
+ 'Anime: Movies' => '73',
+ 'Anime: Dubbed/Subbed' => '74',
+ 'Anime: Others' => '75',
+ 'Books: Ebooks' => '53',
+ 'Books: Magazines' => '54',
+ 'Books: Comics' => '55',
+ 'Books: Audio' => '56',
+ 'Books: Others' => '68',
+ 'Games: Windows' => '57',
+ 'Games: Android' => '58',
+ 'Games: Others' => '71',
+ 'Movies: HD 1080p' => '1',
+ 'Movies: HD 720p' => '2',
+ 'Movies: UltraHD/4K' => '3',
+ 'Movies: XviD' => '42',
+ 'Movies: X264/H264' => '47',
+ 'Movies: 3D' => '49',
+ 'Movies: Dubs/Dual Audio' => '51',
+ 'Movies: CAM/TS' => '65',
+ 'Movies: BluRay Disc/Remux' => '66',
+ 'Movies: DVDR' => '67',
+ 'Movies: HEVC/x265' => '76',
+ 'Music: MP3' => '59',
+ 'Music: FLAC' => '60',
+ 'Music: Music Videos' => '61',
+ 'Music: Others' => '69',
+ 'Software: Windows' => '62',
+ 'Software: Android' => '63',
+ 'Software: Mac' => '64',
+ 'Software: Others' => '70',
+ 'TV: HD/X264/H264' => '41',
+ 'TV: SD/X264/H264' => '5',
+ 'TV: TV Packs' => '7',
+ 'TV: SD/XVID' => '50',
+ 'TV: Sport' => '72',
+ 'TV: HEVC/x265' => '77',
+ 'Unsorted: Unsorted' => '78'
+ ),
+ 'defaultValue' => '(ALL TYPES)'
+ ),
+ 'status' => array(
+ 'type' => 'list',
+ 'name' => 'Status',
+ 'values' => array(
+ 'Active Transfers' => '0',
+ 'Included Dead' => '1',
+ 'Only Dead' => '2'
+ ),
+ 'defaultValue' => 'Included Dead'
+ ),
+ 'lang' => array(
+ 'type' => 'list',
+ 'name' => 'Lang',
+ 'values' => array(
+ '(ALL)' => '0',
+ 'Arabic' => '17',
+ 'Chinese ' => '10',
+ 'Danish' => '13',
+ 'Dutch' => '11',
+ 'English' => '1',
+ 'Finnish' => '18',
+ 'French' => '2',
+ 'German' => '3',
+ 'Greek' => '15',
+ 'Hindi' => '8',
+ 'Italian' => '4',
+ 'Japanese' => '5',
+ 'Korean' => '9',
+ 'Polish' => '14',
+ 'Russian' => '7',
+ 'Spanish' => '6',
+ 'Turkish' => '16'
+ ),
+ 'defaultValue' => '(ALL)'
+ )
+ ));
+
+ public function collectData(){
+ // No control on inputs, because all have defaultValue set
+ $query_str = 'torrents-search.php';
+ $query_str .= '?search=' . urlencode('+'.str_replace(' ', ' +', $this->getInput('query')));
+ $query_str .= '&cat=' . $this->getInput('cat');
+ $query_str .= 'incldead&=' . $this->getInput('status');
+ $query_str .= '&lang=' . $this->getInput('lang');
+ $query_str .= '&sort=id&order=desc';
+
+ // Get results page
+ $html = getSimpleHTMLDOM(self::URI . $query_str)
+ or returnServerError('Could not request ' . $this->getName());
+
+ // Loop on each entry
+ foreach($html->find('table.table tr') as $element) {
+ if($element->parent->tag == 'thead') continue;
+ $entry = $element->find('td', 1)->find('a', 0);
+
+ // retrieve result page to get more details
+ $link = rtrim(self::URI, '/') . $entry->href;
+ $page = getSimpleHTMLDOM($link)
+ or returnServerError('Could not request page ' . $link);
+
+ // get details & download links
+ $details = $page->find('fieldset.download table', 0); // WHAT?? It should be the second one…
+ $dllinks = $page->find('div#downloadbox table', 0);
+
+ // fill item
+ $item = array();
+ $item['author'] = $details->children(6)->children(1)->plaintext;
+ $item['title'] = $entry->title;
+ $item['uri'] = $dllinks->children(0)->children(0)->children(0)->href;
+ $item['timestamp'] = strtotime($details->children(7)->children(1)->plaintext);
+ $item['content'] = '';
+ $item['content'] .= '<br/><b>Name: </b>' . $details->children(0)->children(1)->innertext;
+ $item['content'] .= '<br/><b>Lang: </b>' . $details->children(3)->children(1)->innertext;
+ $item['content'] .= '<br/><b>Size: </b>' . $details->children(4)->children(1)->innertext;
+ $item['content'] .= '<br/><b>Hash: </b>' . $details->children(5)->children(1)->innertext;
+ foreach($dllinks->children(0)->children(1)->find('a') as $dl) {
+ $item['content'] .= '<br/>' . $dl->outertext;
+ }
+ $item['content'] .= '<br/><br/>' . $details->children(1)->children(0)->innertext;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/EZTVBridge.php b/bridges/EZTVBridge.php
index 4fb9e57..c016ff3 100644
--- a/bridges/EZTVBridge.php
+++ b/bridges/EZTVBridge.php
@@ -1,7 +1,7 @@
<?php
class EZTVBridge extends BridgeAbstract {
- const MAINTAINER = "alexAubin";
+ const MAINTAINER = 'alexAubin';
const NAME = 'EZTV';
const URI = 'https://eztv.ch/';
const DESCRIPTION = 'Returns list of *recent* torrents for a specific show
@@ -23,15 +23,15 @@ on EZTV. Get showID from URLs in https://eztv.ch/shows/showID/show-full-name.';
$relativeDays = 0;
$relativeHours = 0;
- foreach(explode(" ", $relativeReleaseTime) as $relativeTimeElement) {
- if(substr($relativeTimeElement, -1) == "d") $relativeDays = substr($relativeTimeElement, 0, -1);
- if(substr($relativeTimeElement, -1) == "h") $relativeHours = substr($relativeTimeElement, 0, -1);
+ foreach(explode(' ', $relativeReleaseTime) as $relativeTimeElement) {
+ if(substr($relativeTimeElement, -1) == 'd') $relativeDays = substr($relativeTimeElement, 0, -1);
+ if(substr($relativeTimeElement, -1) == 'h') $relativeHours = substr($relativeTimeElement, 0, -1);
}
return mktime(date('h') - $relativeHours, 0, 0, date('m'), date('d') - $relativeDays, date('Y'));
}
// Loop on show ids
- $showList = explode(",", $this->getInput('i'));
+ $showList = explode(',', $this->getInput('i'));
foreach($showList as $showID) {
// Get show page
diff --git a/bridges/ElloBridge.php b/bridges/ElloBridge.php
new file mode 100644
index 0000000..c0e266e
--- /dev/null
+++ b/bridges/ElloBridge.php
@@ -0,0 +1,146 @@
+<?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,
+ 'title' => 'Username'
+ )
+ ),
+ 'Search' => array(
+ 's' => array(
+ 'name' => 'Search',
+ 'required' => true,
+ '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'] = $this->findText($post->summary);
+ $item['content'] = $this->getPostContent($post->body);
+ $item['enclosures'] = $this->getEnclosures($post, $postData);
+ $content = $post->body;
+
+ $this->items[] = $item;
+ $count += 1;
+
+ }
+
+ }
+
+ public function findText($path) {
+
+ foreach($path as $summaryElement) {
+
+ if($summaryElement->kind == 'text') {
+ return $summaryElement->data;
+ }
+
+ }
+
+ return '';
+
+ }
+
+ public 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;
+
+ }
+
+ public 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;
+
+ }
+
+ public function getUsername($post, $postData) {
+
+ foreach($postData->linked->users as $user) {
+ if($user->id == $post->links->author->id) {
+ return $user->username;
+ }
+ }
+
+ }
+
+ public function getAPIKey() {
+ $cache = Cache::create('FileCache');
+ $cache->setPath(CACHE_DIR);
+ $cache->setParameters(['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/FB2Bridge.php b/bridges/FB2Bridge.php
index 7d78b87..1aeb30d 100644
--- a/bridges/FB2Bridge.php
+++ b/bridges/FB2Bridge.php
@@ -95,7 +95,7 @@ EOD;
. $pageID
. '&cursor={"card_id"%3A"videos"%2C"has_next_page"%3Atrue}&surface=mobile_page_home&unit_count=8';
- $fileContent = file_get_contents($requestString);
+ $fileContent = getContents($requestString);
$articleIndex = 0;
$maxArticle = 3;
@@ -103,19 +103,19 @@ EOD;
$html = $this->buildContent($fileContent);
$author = $this->getInput('u');
- foreach($html->find("article") as $content) {
+ foreach($html->find('article') as $content) {
$item = array();
- $item['uri'] = "http://touch.facebook.com"
- . $content->find("div[class='_52jc _5qc4 _24u0 _36xo']", 0)->find("a", 0)->getAttribute("href");
+ $item['uri'] = 'http://touch.facebook.com'
+ . $content->find("div[class='_52jc _5qc4 _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href');
- if($content->find("header", 0) !== null) {
- $content->find("header", 0)->innertext = "";
+ if($content->find('header', 0) !== null) {
+ $content->find('header', 0)->innertext = '';
}
- if($content->find("footer", 0) !== null) {
- $content->find("footer", 0)->innertext = "";
+ if($content->find('footer', 0) !== null) {
+ $content->find('footer', 0)->innertext = '';
}
//Remove html nodes, keep only img, links, basic formatting
@@ -168,7 +168,7 @@ EOD;
$regex = implode(
'',
array(
- "/timeline_unit",
+ '/timeline_unit',
"\\\\\\\\u00253A1",
"\\\\\\\\u00253A([0-9]*)",
"\\\\\\\\u00253A([0-9]*)",
@@ -182,29 +182,29 @@ EOD;
return implode(
'',
array(
- "https://touch.facebook.com/pages_reaction_units/more/?page_id=",
+ 'https://touch.facebook.com/pages_reaction_units/more/?page_id=',
$pageID,
- "&cursor=%7B%22timeline_cursor%22%3A%22timeline_unit%3A1%3A",
+ '&cursor=%7B%22timeline_cursor%22%3A%22timeline_unit%3A1%3A',
$result[1],
- "%3A",
+ '%3A',
$result[2],
- "%3A",
+ '%3A',
$result[3],
- "%3A",
+ '%3A',
$result[4],
- "%22%2C%22timeline_section_cursor%22%3A%7B%7D%2C%22",
- "has_next_page%22%3Atrue%7D&surface=mobile_page_home&unit_count=3"
+ '%22%2C%22timeline_section_cursor%22%3A%7B%7D%2C%22',
+ 'has_next_page%22%3Atrue%7D&surface=mobile_page_home&unit_count=3'
)
);
}
//Builds the HTML from the encoded JS that Facebook provides.
private function buildContent($pageContent){
-
- $regex = "/\\\"html\\\":\\\"(.*?)\\\",\\\"replace/";
+ // The html ends with:
+ // /div>","replaceifexists
+ $regex = '/\\"html\\":(\".+\/div>"),"replace/';
preg_match($regex, $pageContent, $result);
-
- return str_get_html(html_entity_decode(json_decode('"' . $result[1] . '"')));
+ return str_get_html(html_entity_decode(json_decode($result[1])));
}
@@ -214,7 +214,7 @@ EOD;
$ctx = stream_context_create(array(
'http' => array(
- 'user_agent' => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0",
+ 'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
)
)
@@ -222,12 +222,12 @@ EOD;
$a = file_get_contents($pageURL, 0, $ctx);
//First request to get the cookie
- $cookies = "";
+ $cookies = '';
foreach($http_response_header as $hdr) {
- if(strpos($hdr, "Set-Cookie") !== false) {
- $cLine = explode(":", $hdr)[1];
- $cLine = explode(";", $cLine)[0];
- $cookies .= ";" . $cLine;
+ if(strpos($hdr, 'Set-Cookie') !== false) {
+ $cLine = explode(':', $hdr)[1];
+ $cLine = explode(';', $cLine)[0];
+ $cookies .= ';' . $cLine;
}
}
@@ -239,7 +239,7 @@ EOD;
$context = stream_context_create(array(
'http' => array(
- 'user_agent' => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0",
+ 'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
'header' => 'Cookie: ' . $cookies
)
)
@@ -247,12 +247,12 @@ EOD;
$pageContent = file_get_contents($page, 0, $context);
- if(strpos($pageContent, "signup-button") != false) {
+ if(strpos($pageContent, 'signup-button') != false) {
return -1;
}
//Get the page ID if we don't have a captcha
- $regex = "/page_id=([0-9]*)&/";
+ $regex = '/page_id=([0-9]*)&/';
preg_match($regex, $pageContent, $matches);
if(count($matches) > 0) {
@@ -260,7 +260,7 @@ EOD;
}
//Get the page ID if we do have a captcha
- $regex = "/\"pageID\":\"([0-9]*)\"/";
+ $regex = '/"pageID":"([0-9]*)"/';
preg_match($regex, $pageContent, $matches);
return $matches[1];
diff --git a/bridges/FDroidBridge.php b/bridges/FDroidBridge.php
new file mode 100644
index 0000000..a1a37ef
--- /dev/null
+++ b/bridges/FDroidBridge.php
@@ -0,0 +1,54 @@
+<?php
+class FDroidBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'Mitsukarenai';
+ const NAME = 'F-Droid Bridge';
+ const URI = 'https://f-droid.org/';
+ const CACHE_TIMEOUT = 60 * 60 * 2; // 2 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',
+ 'required' => true,
+ 'values' => array(
+ 'Latest added apps' => 'added',
+ 'Latest updated apps' => 'updated'
+ )
+ )
+ ));
+
+ public function collectData(){
+ $url = self::URI;
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError('Could not request F-Droid.');
+
+ // targetting the corresponding widget based on user selection
+ // "updated" is the 4th widget on the page, "added" is the 5th
+
+ switch($this->getInput('u')) {
+ case 'updated':
+ $html_widget = $html->find('div.sidebar-widget', 4);
+ break;
+ default:
+ $html_widget = $html->find('div.sidebar-widget', 5);
+ break;
+ }
+
+ // 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['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;
+ }
+ }
+}
diff --git a/bridges/FacebookBridge.php b/bridges/FacebookBridge.php
index 90f3d74..c8ea0d7 100644
--- a/bridges/FacebookBridge.php
+++ b/bridges/FacebookBridge.php
@@ -1,34 +1,257 @@
<?php
class FacebookBridge extends BridgeAbstract {
- const MAINTAINER = 'teromene';
+ const MAINTAINER = 'teromene, logmanoriginal';
const NAME = 'Facebook';
const URI = 'https://www.facebook.com/';
const CACHE_TIMEOUT = 300; // 5min
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
- ),
- 'media_type' => array(
- 'name' => 'Media type',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'All' => 'all',
- 'Video' => 'video',
- 'No Video' => 'novideo'
+ 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'
),
- 'defaultValue' => 'all'
+ 'skip_reviews' => array(
+ 'name' => 'Skip reviews',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'defaultValue' => false,
+ 'title' => 'Feed includes reviews when checked'
+ )
+ ),
+ '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'
+ )
)
- ));
+ );
private $authorName = '';
+ private $groupName = '';
+
+ public function getURI() {
+ $uri = self::URI;
+
+ switch($this->queriedContext) {
+
+ case 'Group':
+ $uri .= 'groups/' . $this->sanitizeGroup(filter_var($this->getInput('g'), FILTER_SANITIZE_URL));
+ break;
+
+ }
+
+ 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 . '"!');
+
+ }
+
+ }
+
+ #region Group
+
+ private function collectGroupData() {
+
+ $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
+
+ $html = getSimpleHTMLDOM($this->getURI(), $header)
+ or returnServerError('Failed loading facebook page: ' . $this->getURI());
+
+ 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.userContentWrapper')
+ or returnServerError('Failed finding posts!');
+
+ foreach($posts as $post) {
+
+ $item = array();
+
+ $item['uri'] = $this->extractGroupURI($post);
+ $item['title'] = $this->extractGroupTitle($post);
+ $item['author'] = $this->extractGroupAuthor($post);
+ $item['content'] = $this->extractGroupContent($post);
+ $item['timestamp'] = $this->extractGroupTimestamp($post);
+ $item['enclosures'] = $this->extractGroupEnclosures($post);
+
+ $this->items[] = $item;
+
+ }
+
+ }
+
+ private function sanitizeGroup($group) {
+
+ if(filter_var(
+ $group,
+ FILTER_VALIDATE_URL,
+ FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_PATH_REQUIRED)) {
+ // User provided a URL
+
+ $urlparts = parse_url($group);
+
+ if($urlparts['host'] !== parse_url(self::URI)['host']
+ && 'www.' . $urlparts['host'] !== parse_url(self::URI)['host']) {
+
+ returnClientError('The host you provided is invalid! Received "'
+ . $urlparts['host']
+ . '", expected "'
+ . parse_url(self::URI)['host']
+ . '"!');
+
+ }
+
+ return explode('/', $urlparts['path'])[2];
+
+ } elseif(strpos($group, '/') !== false) {
+ returnClientError('The group you provided is invalid: ' . $group);
+ } else {
+ return $group;
+ }
+
+ }
+
+ private function isPublicGroup($html) {
+
+ // Facebook redirects to the groups about page for non-public groups
+ $about = $html->find('#pagelet_group_about', 0);
- public function collectData(){
+ return !($about);
+
+ }
+
+ private function extractGroupName($html) {
+
+ $ogtitle = $html->find('meta[property="og:title"]', 0)
+ or returnServerError('Unable to find group title!');
+
+ return htmlspecialchars_decode($ogtitle->content, ENT_QUOTES);
+
+ }
+
+ private function extractGroupURI($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) {
+ return $anchor->href;
+ }
+
+ }
+
+ return null;
+
+ }
+
+ private function extractGroupContent($post) {
+
+ $content = $post->find('div.userContent', 0)
+ or returnServerError('Unable to find user content!');
+
+ return $content->innertext . $content->next_sibling()->innertext;
+
+ }
+
+ private function extractGroupTimestamp($post) {
+
+ $element = $post->find('abbr[data-utime]', 0)
+ or returnServerError('Unable to find timestamp!');
+
+ return $element->getAttribute('data-utime');
+
+ }
+
+ private function extractGroupAuthor($post) {
+
+ $element = $post->find('img', 0)
+ or returnServerError('Unable to find author information!');
+
+ return $element->{'aria-label'};
+
+ }
+
+ private function extractGroupEnclosures($post) {
+
+ $elements = $post->find('div.userContent', 0)->next_sibling()->find('img');
+
+ $enclosures = array();
+
+ foreach($elements as $enclosure) {
+ $enclosures[] = $enclosure->src;
+ }
+
+ return empty($enclosures) ? null : $enclosures;
+
+ }
+
+ private function extractGroupTitle($post) {
+
+ $element = $post->find('h5', 0)
+ or returnServerError('Unable to find title!');
+
+ if(strpos($element->plaintext, 'shared') === false) {
+
+ $content = strip_tags($this->extractGroupContent($post));
+
+ return $this->extractGroupAuthor($post)
+ . ' posted: '
+ . substr(
+ $content,
+ 0,
+ strpos(wordwrap($content, 64), "\n")
+ )
+ . '...';
+
+ }
+
+ return $element->plaintext;
+
+ }
+
+ #endregion
+
+ private function collectUserData(){
//Extract a string using start and end delimiters
function extractFromDelimiters($string, $start, $end){
@@ -95,18 +318,16 @@ class FacebookBridge extends BridgeAbstract {
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']);
- $http_options = array(
- 'http' => array(
- 'method' => 'POST',
- 'user_agent' => ini_get('user_agent'),
- 'header' => array("Content-type:
- application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscript=1\r\n"),
- 'content' => http_build_query($captcha_fields)
- ),
+ $captcha_fields['captcha_response'] = preg_replace('/[^a-zA-Z0-9]+/', '', $_POST['captcha_response']);
+
+ $header = array("Content-type:
+application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscript=1\r\n");
+ $opts = array(
+ CURLOPT_POST => 1,
+ CURLOPT_POSTFIELDS => http_build_query($captcha_fields)
);
- $context = stream_context_create($http_options);
- $html = getContents($captcha_action, false, $context);
+
+ $html = getContents($captcha_action, $header, $opts);
if($html === false) {
returnServerError('Failed to submit captcha response back to Facebook');
@@ -120,24 +341,46 @@ class FacebookBridge extends BridgeAbstract {
//Retrieve page contents
if(is_null($html)) {
- $http_options = array(
- 'http' => array(
- 'method' => 'GET',
- 'user_agent' => ini_get('user_agent'),
- 'header' => 'Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n"
- )
- );
- $context = stream_context_create($http_options);
- if(!strpos($this->getInput('u'), "/")) {
- $html = getSimpleHTMLDOM(self::URI . urlencode($this->getInput('u')) . '?_fb_noscript=1',
- false,
- $context)
- or returnServerError('No results for this query.');
+ $header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
+
+ // Check if the user provided a fully qualified URL
+ if (filter_var($this->getInput('u'), FILTER_VALIDATE_URL)) {
+
+ $urlparts = parse_url($this->getInput('u'));
+
+ if($urlparts['host'] !== parse_url(self::URI)['host']) {
+ returnClientError('The host you provided is invalid! Received "'
+ . $urlparts['host']
+ . '", expected "'
+ . parse_url(self::URI)['host']
+ . '"!');
+ }
+
+ if(!array_key_exists('path', $urlparts)
+ || $urlparts['path'] === '/') {
+ returnClientError('The URL you provided doesn\'t contain the user name!');
+ }
+
+ $user = explode('/', $urlparts['path'])[1];
+
+ $html = getSimpleHTMLDOM(self::URI . urlencode($user) . '?_fb_noscript=1', $header)
+ or returnServerError('No results for this query.');
+
} else {
- $html = getSimpleHTMLDOM(self::URI . 'pages/' . $this->getInput('u') . '?_fb_noscript=1',
- false,
- $context)
- or returnServerError('No results for this query.');
+
+ // First character cannot be a forward slash
+ if(strpos($this->getInput('u'), '/') === 0) {
+ returnClientError('Remove leading slash "/" from the username!');
+ }
+
+ if(!strpos($this->getInput('u'), '/')) {
+ $html = getSimpleHTMLDOM(self::URI . urlencode($this->getInput('u')) . '?_fb_noscript=1', $header)
+ or returnServerError('No results for this query.');
+ } else {
+ $html = getSimpleHTMLDOM(self::URI . 'pages/' . $this->getInput('u') . '?_fb_noscript=1', $header)
+ or returnServerError('No results for this query.');
+ }
+
}
}
@@ -171,6 +414,12 @@ EOD;
}
//No captcha? We can carry on retrieving page contents :)
+ //First, we check wether 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)
@@ -196,6 +445,12 @@ EOD;
$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')) {
@@ -266,7 +521,7 @@ EOD;
);
//Retrieve date of the post
- $date = $post->find("abbr")[0];
+ $date = $post->find('abbr')[0];
if(isset($date) && $date->hasAttribute('data-utime')) {
$date = $date->getAttribute('data-utime');
} else {
@@ -297,9 +552,22 @@ EOD;
}
public function getName(){
- if(!empty($this->authorName)) {
- return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName
- . ' - Facebook Bridge';
+
+ switch($this->queriedContext) {
+
+ case 'User':
+ if(!empty($this->authorName)) {
+ return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName
+ . ' - Facebook Bridge';
+ }
+ break;
+
+ case 'Group':
+ if(!empty($this->groupName)) {
+ return $this->groupName . ' - Facebook Bridge';
+ }
+ break;
+
}
return parent::getName();
diff --git a/bridges/FootitoBridge.php b/bridges/FootitoBridge.php
index ac06cd5..22aead4 100644
--- a/bridges/FootitoBridge.php
+++ b/bridges/FootitoBridge.php
@@ -15,47 +15,47 @@ class FootitoBridge extends BridgeAbstract {
$content = trim($element->innertext);
$content = str_replace(
- "<img",
+ '<img',
"<img style='float : left;'",
$content );
$content = str_replace(
- "class=\"logo\"",
+ 'class="logo"',
"style='float : left;'",
$content );
$content = str_replace(
- "class=\"contenu\"",
+ 'class="contenu"',
"style='margin-left : 60px;'",
$content );
$content = str_replace(
- "class=\"responsive-comment\"",
+ 'class="responsive-comment"',
"style='border-top : 1px #DDD solid; background-color : white; padding : 10px;'",
$content );
$content = str_replace(
- "class=\"jaime\"",
+ 'class="jaime"',
"style='display : none;'",
$content );
$content = str_replace(
- "class=\"auteur-event responsive\"",
+ 'class="auteur-event responsive"',
"style='display : none;'",
$content );
$content = str_replace(
- "class=\"report-abuse-button\"",
+ 'class="report-abuse-button"',
"style='display : none;'",
$content );
$content = str_replace(
- "class=\"reaction clearfix\"",
+ 'class="reaction clearfix"',
"style='margin : 10px 0px; padding : 5px; border-bottom : 1px #DDD solid;'",
$content );
$content = str_replace(
- "class=\"infos\"",
+ 'class="infos"',
"style='font-size : 0.7em;'",
$content );
diff --git a/bridges/FourchanBridge.php b/bridges/FourchanBridge.php
index 6aaa13e..ecd9d90 100644
--- a/bridges/FourchanBridge.php
+++ b/bridges/FourchanBridge.php
@@ -30,7 +30,7 @@ class FourchanBridge extends BridgeAbstract {
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
- or returnServerError("Could not request 4chan, thread not found");
+ or returnServerError('Could not request 4chan, thread not found');
foreach($html->find('div.postContainer') as $element) {
$item = array();
diff --git a/bridges/GithubIssueBridge.php b/bridges/GithubIssueBridge.php
index 4f121d8..0ed775d 100644
--- a/bridges/GithubIssueBridge.php
+++ b/bridges/GithubIssueBridge.php
@@ -106,7 +106,7 @@ class GithubIssueBridge extends BridgeAbstract {
$content = $comment->parent()->innertext;
} else {
$title .= ' / ' . trim($comment->firstChild()->plaintext);
- $content = "<pre>" . $comment->find('.comment-body', 0)->innertext . "</pre>";
+ $content = '<pre>' . $comment->find('.comment-body', 0)->innertext . '</pre>';
}
$item = array();
diff --git a/bridges/GoComicsBridge.php b/bridges/GoComicsBridge.php
index 268bd90..3223d19 100644
--- a/bridges/GoComicsBridge.php
+++ b/bridges/GoComicsBridge.php
@@ -3,7 +3,7 @@ class GoComicsBridge extends BridgeAbstract {
const MAINTAINER = 'sky';
const NAME = 'GoComics Unofficial RSS';
- const URI = 'http://www.gocomics.com/';
+ const URI = 'https://www.gocomics.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'The Unofficial GoComics RSS';
const PARAMETERS = array( array(
@@ -18,25 +18,27 @@ class GoComicsBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request GoComics: ' . $this->getURI());
- foreach($html->find('div.comic__container') as $element) {
+ //Get info from first page
+ $author = preg_replace('/By /', '', $html->find('.media-subheading', 0)->plaintext);
- $img = $element->find('.item-comic-image img', 0);
- $link = $element->find('a.js-item-comic-link', 0);
- $comic = $img->src;
- $title = $link->title;
- $url = $html->find('input.js-copy-link', 0)->value;
- $date = substr($title, -10);
- if (empty($title))
- $title = 'GoComics ' . $this->getInput('comicname') . ' on ' . $date;
- $date = strtotime($date);
+ $link = self::URI . $html->find('.gc-deck--cta-0', 0)->find('a', 0)->href;
+ for($i = 0; $i < 5; $i++) {
$item = array();
- $item['id'] = $url;
- $item['uri'] = $url;
- $item['title'] = $title;
- $item['author'] = preg_replace('/by /', '', $element->find('a.link-blended small', 0)->plaintext);
- $item['timestamp'] = $date;
- $item['content'] = '<img src="' . $comic . '" alt="' . $title . '" />';
+
+ $page = getSimpleHTMLDOM($link)
+ or returnServerError('Could not request GoComics: ' . $link);
+ $imagelink = $page->find('.img-fluid', 1)->src;
+ $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;
}
}
diff --git a/bridges/GoogleSearchBridge.php b/bridges/GoogleSearchBridge.php
index 2c4a5f1..2eb5841 100644
--- a/bridges/GoogleSearchBridge.php
+++ b/bridges/GoogleSearchBridge.php
@@ -17,7 +17,7 @@ class GoogleSearchBridge extends BridgeAbstract {
const PARAMETERS = array(array(
'q' => array(
- 'name' => "keyword",
+ 'name' => 'keyword',
'required' => true
)
));
diff --git a/bridges/GrandComicsDatabaseBridge.php b/bridges/GrandComicsDatabaseBridge.php
new file mode 100644
index 0000000..2b2fdfe
--- /dev/null
+++ b/bridges/GrandComicsDatabaseBridge.php
@@ -0,0 +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)
+ or returnServerError('Error while downloading the website content');
+
+ $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
+ $item = array();
+ $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/HotUKDealsBridge.php b/bridges/HotUKDealsBridge.php
new file mode 100644
index 0000000..a7eebf6
--- /dev/null
+++ b/bridges/HotUKDealsBridge.php
@@ -0,0 +1,1397 @@
+<?php
+
+require_once(__DIR__ . '/DealabsBridge.php');
+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 = array(
+ 'Search by keyword(s))' => array (
+ 'q' => array(
+ 'name' => 'Keyword(s)',
+ 'type' => 'text',
+ 'required' => true
+ ),
+ 'hide_expired' => array(
+ 'name' => 'Hide expired deals',
+ 'type' => 'checkbox',
+ 'required' => 'true'
+ ),
+ 'hide_local' => array(
+ 'name' => 'Hide local deals',
+ 'type' => 'checkbox',
+ 'title' => 'Hide deals in physical store',
+ 'required' => 'true'
+ ),
+ 'priceFrom' => array(
+ 'name' => 'Minimal Price',
+ 'type' => 'text',
+ 'title' => 'Minmal Price in Pounds',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ 'priceTo' => array(
+ 'name' => 'Maximum Price',
+ 'type' => 'text',
+ 'title' => 'Maximum Price in Pounds',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ ),
+
+ 'Deals per group' => array(
+ 'group' => array(
+ 'name' => 'Group',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Group whose deals must be displayed',
+ 'values' => array(
+ '2DS' => '2ds',
+ '3D Bluray' => '3d-bluray',
+ '3D Glasses' => '3d-glasses',
+ '3D Printer' => '3d-printer',
+ '3DS' => '3ds',
+ '3DS Games' => '3ds-games',
+ '3D TV' => '3d-tv',
+ '4G' => '4g',
+ '4k Bluray' => '4k-bluray',
+ '4k Monitor' => '4k-monitor',
+ '4K TV' => '4k-tv',
+ '7up' => '7up',
+ '144Hz Monitor' => '144hz',
+ 'AA Batteries' => 'aa',
+ 'Acer' => 'acer',
+ 'Actifry' => 'actifry',
+ 'Action Camera' => 'action-camera',
+ 'Add On Item' => 'add-on-item',
+ 'Adidas' => 'adidas',
+ 'Adobe' => 'adobe',
+ 'Aftershave' => 'aftershave',
+ 'Air Bed' => 'air-bed',
+ 'Air Conditioner' => 'air-con',
+ 'Air Fryer' => 'air-fryer',
+ 'Airport Parking' => 'airport-parking',
+ 'AKG' => 'akg',
+ 'Alarm' => 'alarm',
+ 'Alcatel' => 'alcatel',
+ 'Alcohol' => 'alcohol',
+ 'Alienware' => 'alienware',
+ 'Alton Towers' => 'alton-towers',
+ 'Amazon Echo' => 'amazon-echo',
+ 'Amazon Fire Stick' => 'amazon-fire-stick',
+ 'Amazon Fire Tv' => 'fire-tv',
+ 'Amazon Pantry' => 'amazon-pantry',
+ 'Amazon Prime' => 'amazon-prime',
+ 'Amazon Warehouse' => 'amazon-warehouse',
+ 'AMD' => 'amd',
+ 'Amex' => 'amex',
+ 'Amiibo' => 'amiibo',
+ 'Amsterdam' => 'amsterdam',
+ 'Android' => 'android',
+ 'Android Tablet' => 'android-tablet',
+ 'Android TV' => 'android-tv',
+ 'Anker' => 'anker',
+ 'Apple' => 'apple',
+ 'Apple TV' => 'apple-tv',
+ 'Apple Watch' => 'apple-watch',
+ 'Armani' => 'armani',
+ 'Asics' => 'asics',
+ 'ASUS' => 'asus',
+ 'Audi' => 'audi',
+ 'Baby' => 'baby',
+ 'Baby & Kids' => 'kids',
+ 'Baby Monitor' => 'baby-monitor',
+ 'Baby Swing' => 'baby-swing',
+ 'Backpack' => 'backpack',
+ 'Bag' => 'bag',
+ 'Bank' => 'bank',
+ 'Barbour' => 'barbour',
+ 'Barcelona' => 'barcelona',
+ 'Bathroom' => 'bathroom',
+ 'Batman' => 'batman',
+ 'Battery' => 'battery',
+ 'Battlefield' => 'battlefield',
+ 'Battlefield 1' => 'battlefield-1',
+ 'BBQ' => 'bbq',
+ 'Bean To Cup' => 'bean-to-cup',
+ 'Beard Trimmer' => 'beard-trimmer',
+ 'Bed' => 'bed',
+ 'Bedding' => 'bedding',
+ 'Bed Frame' => 'bed-frame',
+ 'Beer' => 'beer',
+ 'Beko' => 'beko',
+ 'Belfast' => 'belfast',
+ 'Bench' => 'bench',
+ 'Berghaus' => 'berghaus',
+ 'Bike' => 'bike',
+ 'Bin' => 'bin',
+ 'Bioshock' => 'bioshock',
+ 'Black Ops 3' => 'black-ops-3',
+ 'Blackpool' => 'blackpool',
+ 'Blender' => 'blender',
+ 'Blinds' => 'blinds',
+ 'Bloodborne' => 'bloodborne',
+ 'Blu-Ray' => 'blu-ray',
+ 'Bluetooth Headphones' => 'bluetooth-headphones',
+ 'Bluetooth Speaker' => 'bluetooth-speaker',
+ 'BMW' => 'bmw',
+ 'Board Game' => 'board-game',
+ 'Boiler' => 'boiler',
+ 'Bosch' => 'bosch',
+ 'Bose' => 'bose',
+ 'Bourbon' => 'bourbon',
+ 'Boxers' => 'boxers',
+ 'Bra' => 'bra',
+ 'Braun' => 'braun',
+ 'Breakdown' => 'breakdown',
+ 'Brewdog' => 'brewdog',
+ 'Brita' => 'brita',
+ 'Broadband' => 'broadband',
+ 'BT' => 'bt',
+ 'Bt Sport' => 'bt-sport',
+ 'Buggy' => 'buggy',
+ 'Call Of Duty' => 'call-of-duty',
+ 'Camera' => 'camera',
+ 'Camera Lens' => 'lens',
+ 'Camping' => 'camping',
+ 'Candle' => 'candle',
+ 'Canon' => 'canon',
+ 'Canvas' => 'canvas',
+ 'Car' => 'car',
+ 'Caravan' => 'caravan',
+ 'Car Battery' => 'car-battery',
+ 'Car Hire' => 'car-hire',
+ 'Car Insurance' => 'car-insurance',
+ 'Car Lease' => 'car-lease',
+ 'Car Mats' => 'car-mats',
+ 'Carpet' => 'carpet',
+ 'Carpet Cleaner' => 'carpet-cleaner',
+ 'Car Seat' => 'car-seat',
+ 'Car Stereo' => 'car-stereo',
+ 'Casio' => 'casio',
+ 'Caterpillar Boots' => 'caterpillar',
+ 'Cat Food' => 'cat-food',
+ 'CCTV' => 'cctv',
+ 'Chainsaw' => 'chainsaw',
+ 'Chair' => 'chair',
+ 'Champagne' => 'champagne',
+ 'Charger' => 'charger',
+ 'Chessington' => 'chessington',
+ 'Chest Freezer' => 'chest-freezer',
+ 'Chocolate' => 'chocolate',
+ 'Chromebook' => 'chromebook',
+ 'Chromecast' => 'chromecast',
+ 'Cider' => 'cider',
+ 'Cinema' => 'cinema',
+ 'Cineworld' => 'cineworld',
+ 'Circular Saw' => 'circular-saw',
+ 'Circulon' => 'circulon',
+ 'Citizen' => 'citizen',
+ 'Clarins' => 'clarins',
+ 'Clarks' => 'clarks',
+ 'Clinique' => 'clinique',
+ 'Clothes' => 'clothes',
+ 'Coat' => 'coat',
+ 'Coffee' => 'coffee',
+ 'Coffee Machine' => 'coffee-machine',
+ 'Coke' => 'coke',
+ 'Compost' => 'compost',
+ 'Computers' => 'computers',
+ 'Converse' => 'converse',
+ 'Cooker' => 'cooker',
+ 'Cordless Phone' => 'cordless-phone',
+ 'Corsair' => 'corsair',
+ 'Cot' => 'cot',
+ 'CPU' => 'cpu',
+ 'Crash Bandicoot' => 'crash-bandicoot',
+ 'Credit Card' => 'credit-card',
+ 'Cricket' => 'cricket',
+ 'Crisps' => 'crisps',
+ 'Crocs' => 'crocs',
+ 'Cruise' => 'cruise',
+ 'Cuprinol' => 'cuprinol',
+ 'Cutlery' => 'cutlery',
+ 'Dab Radio' => 'dab-radio',
+ 'Dark Souls' => 'dark-souls',
+ 'Dark Souls 3' => 'dark-souls-3',
+ 'Dash Cam' => 'dash-cam',
+ 'Days Out' => 'days-out',
+ 'DDR3' => 'ddr3',
+ 'DDR4' => 'ddr4',
+ 'Deezer' => 'deezer',
+ 'Dehumidifier' => 'dehumidifier',
+ 'Dell' => 'dell',
+ 'Delonghi' => 'delonghi',
+ 'Denon' => 'denon',
+ 'Desk' => 'desk',
+ 'Desktop' => 'desktop',
+ 'Destiny' => 'destiny',
+ 'Destiny 2' => 'destiny-2',
+ 'Deus Ex' => 'deus-ex',
+ 'Dewalt' => 'dewalt',
+ 'Digital Camera' => 'digital-camera',
+ 'Dining Table' => 'dining-table',
+ 'Dinner Set' => 'dinner-set',
+ 'Dirt 4' => 'dirt-4',
+ 'Dishonored 2' => 'dishonored-2',
+ 'Dishwasher' => 'dishwasher',
+ 'Disney' => 'disney',
+ 'Disney Infinity' => 'disney-infinity',
+ 'Disneyland' => 'disneyland',
+ 'DIY' => 'diy',
+ 'Doctor Who' => 'doctor-who',
+ 'Dog' => 'dog',
+ 'Dog Bed' => 'dog-bed',
+ 'Dolce Gusto' => 'dolce-gusto',
+ 'DOOM' => 'doom',
+ 'Dremel' => 'dremel',
+ 'Dress' => 'dress',
+ 'Drill' => 'drill',
+ 'Drone' => 'drone',
+ 'Dryer' => 'dryer',
+ 'DSLR Camera' => 'dslr',
+ 'Dubai' => 'dubai',
+ 'Dulux' => 'dulux',
+ 'Durex' => 'durex',
+ 'Duvet' => 'duvet',
+ 'DVD' => 'dvd',
+ 'DVD Player' => 'dvd-player',
+ 'Dying Light' => 'dying-light',
+ 'Dyson' => 'dyson',
+ 'Dyson V6' => 'dyson-v6',
+ 'Dyson V8' => 'dyson-v8',
+ 'E-Cig' => 'e-cig',
+ 'EA' => 'ea',
+ 'EA Access' => 'ea-access',
+ 'Earphones' => 'earphones',
+ 'Earrings' => 'earrings',
+ 'Eastpak' => 'eastpak',
+ 'eBook' => 'ebook',
+ 'Eco-Drive' => 'eco-drive',
+ 'Ecobubble' => 'ecobubble',
+ 'Edifice' => 'edifice',
+ 'Edinburgh' => 'edinburgh',
+ 'EE' => 'ee',
+ 'Egg' => 'egg',
+ 'Egypt' => 'egypt',
+ 'Elder Scrolls' => 'elder-scrolls',
+ 'Electric Bike' => 'electric-bike',
+ 'Electric Cooker' => 'electric-cooker',
+ 'Electric Fires' => 'electric-fire',
+ 'Electric Shower' => 'electric-shower',
+ 'Electric Toothbrush' => 'electric-toothbrush',
+ 'Electronics' => 'electronics',
+ 'Elemis' => 'elemis',
+ 'Elephone' => 'elephone',
+ 'Elgato' => 'elgato',
+ 'Elite Dangerous' => 'elite-dangerous',
+ 'Emirates' => 'emirates',
+ 'Eneloop' => 'eneloop',
+ 'Energy' => 'energy',
+ 'Engine Oil' => 'engine-oil',
+ 'Entertainment' => 'entertainment',
+ 'Epilator' => 'epilator',
+ 'Epson' => 'epson',
+ 'eReader' => 'ereader',
+ 'Espresso' => 'espresso',
+ 'Estee Lauder' => 'estee-lauder',
+ 'Ethernet' => 'ethernet',
+ 'Eurostar' => 'eurostar',
+ 'Eurotunnel' => 'eurotunnel',
+ 'EVGA' => 'evga',
+ 'Extension Lead' => 'extension-lead',
+ 'External Hard Drive' => 'external-hard-drive',
+ 'Fairy' => 'fairy',
+ 'Fallout' => 'fallout',
+ 'Fallout 4' => 'fallout-4',
+ 'Fan' => 'fan',
+ 'Fancy Dress' => 'fancy-dress',
+ 'Far Cry' => 'far-cry',
+ 'Far Cry 4' => 'far-cry-4',
+ 'Far Cry Primal' => 'far-cry-primal',
+ 'Fashion' => 'fashion',
+ 'Fathers Day' => 'fathers-day',
+ 'Felix' => 'felix',
+ 'Fence' => 'fence',
+ 'Fender Guitars' => 'fender',
+ 'Ferrero Rocher' => 'ferrero-rocher',
+ 'Ferry' => 'ferry',
+ 'Festival' => 'festival',
+ 'Fiat' => 'fiat',
+ 'FIFA' => 'fifa',
+ 'FIFA 17' => 'fifa-17',
+ 'FIFA 18' => 'fifa-18',
+ 'Figures' => 'figures',
+ 'Final Fantasy' => 'final-fantasy',
+ 'Finance & Utilities' => 'personal-finance',
+ 'Finish' => 'finish',
+ 'Finlux' => 'finlux',
+ 'Fire Emblem' => 'fire-emblem',
+ 'Fire Pit' => 'fire-pit',
+ 'Fireplace' => 'fireplace',
+ 'Fish' => 'fish',
+ 'Fisher Price' => 'fisher-price',
+ 'Fishing' => 'fishing',
+ 'Fiskars' => 'fiskars',
+ 'Fitbit' => 'fitbit',
+ 'Fitbit Alta' => 'fitbit-alta',
+ 'Fitbit Blaze' => 'fitbit-blaze',
+ 'Fitbit Charge 2' => 'fitbit-charge-2',
+ 'Fitness Tracker' => 'fitness-tracker',
+ 'Flamingo Land' => 'flamingo-land',
+ 'Flask' => 'flask',
+ 'Fleece' => 'fleece',
+ 'Flight' => 'flight',
+ 'Flip Flops' => 'flip-flops',
+ 'Floodlight' => 'floodlight',
+ 'Flooring' => 'flooring',
+ 'Florida' => 'florida',
+ 'Flowers' => 'flowers',
+ 'Flybe' => 'flybe',
+ 'Flymo' => 'flymo',
+ 'Food' => 'food',
+ 'Food Mixer' => 'food-mixer',
+ 'Food Processor' => 'food-processor',
+ 'Football' => 'football',
+ 'Football Boots' => 'football-boots',
+ 'Football Manager' => 'football-manager',
+ 'Football Shirt' => 'football-shirt',
+ 'Ford' => 'ford',
+ 'For Honor' => 'for-honor',
+ 'Formula 1' => 'f1',
+ 'Forza' => 'forza',
+ 'Forza Horizon' => 'forza-horizon',
+ 'Forza Horizon 3' => 'forza-horizon-3',
+ 'Fossil' => 'fossil',
+ 'Fosters' => 'fosters',
+ 'Foundation' => 'foundation',
+ 'France' => 'france',
+ 'Fred Perry' => 'fred-perry',
+ 'Freebies' => 'freebies',
+ 'Freesat' => 'freesat',
+ 'Freeview' => 'freeview',
+ 'Freezer' => 'freezer',
+ 'Fridge' => 'fridge',
+ 'Fridge Freezer' => 'fridge-freezer',
+ 'Frozen' => 'frozen',
+ 'Fruit' => 'fruit',
+ 'Fryer' => 'fryer',
+ 'Frying Pan' => 'frying-pan',
+ 'Fujifilm' => 'fuji',
+ 'Funko Pop' => 'funko-pop',
+ 'Furby' => 'furby',
+ 'Furniture' => 'furniture',
+ 'Fusion' => 'fusion',
+ 'G-Shock' => 'g-shock',
+ 'G-Sync Monitor' => 'g-sync',
+ 'Game Of Thrones' => 'game-of-thrones',
+ 'Gaming' => 'gaming',
+ 'Gaming Chair' => 'gaming-chair',
+ 'Gaming Controller' => 'controller',
+ 'Gaming Headset' => 'gaming-headset',
+ 'Gaming Keyboard' => 'gaming-keyboard',
+ 'Gaming Laptop' => 'gaming-laptop',
+ 'Gaming Monitor' => 'gaming-monitor',
+ 'Gaming PC' => 'gaming-pc',
+ 'Garage' => 'garage',
+ 'Garden' => 'garden',
+ 'Garden Furniture' => 'garden-furniture',
+ 'Garmin' => 'garmin',
+ 'Gas' => 'gas',
+ 'Gas Cooker' => 'gas-cooker',
+ 'Gatwick' => 'gatwick',
+ 'Gazebo' => 'gazebo',
+ 'Gazelle' => 'gazelle',
+ 'GBK' => 'gbk',
+ 'Gears Of War' => 'gears-of-war',
+ 'Gears Of War 4' => 'gears-of-war-4',
+ 'GeForce' => 'geforce',
+ 'George Foreman' => 'george-foreman',
+ 'Geox' => 'geox',
+ 'GHD' => 'ghd',
+ 'Ghostbusters' => 'ghostbusters',
+ 'Ghost Recon' => 'ghost-recon',
+ 'Gibson Guitars' => 'gibson',
+ 'Giffgaff' => 'giffgaff',
+ 'Gift Card' => 'gift-card',
+ 'Gifts' => 'gifts',
+ 'Gift Set' => 'gift-set',
+ 'Gilet' => 'gilet',
+ 'Gillette' => 'gillette',
+ 'Gimbal' => 'gimbal',
+ 'Gin' => 'gin',
+ 'Glasgow' => 'glasgow',
+ 'Glasses' => 'glasses',
+ 'Gloves' => 'gloves',
+ 'Glue Gun' => 'glue-gun',
+ 'Gluten Free' => 'gluten-free',
+ 'Goggles' => 'goggles',
+ 'Go Kart' => 'go-kart',
+ 'Golf' => 'golf',
+ 'Golf Balls' => 'golf-balls',
+ 'Golf Clubs' => 'golf-clubs',
+ 'Goodfellas' => 'goodfellas',
+ 'Google' => 'google',
+ 'Google Home' => 'google-home',
+ 'Google Pixel' => 'google-pixel',
+ 'Go Outdoors' => 'go-outdoors',
+ 'GoPro' => 'gopro',
+ 'Graco' => 'graco',
+ 'Grand National' => 'grand-national',
+ 'Graphics Card' => 'graphics-card',
+ 'Gravity Rush' => 'gravity-rush',
+ 'Graze' => 'graze',
+ 'Greece' => 'greece',
+ 'Greenhouse' => 'greenhouse',
+ 'Greggs' => 'greggs',
+ 'Grey Goose' => 'grey-goose',
+ 'Grill' => 'grill',
+ 'Grinder' => 'grinder',
+ 'Grobag' => 'grobag',
+ 'Groceries' => 'groceries',
+ 'GTA' => 'gta',
+ 'GTA V' => 'gta-v',
+ 'Gtx 970' => 'gtx-970',
+ 'GTX 1060' => 'gtx-1060',
+ 'GTX 1070' => 'gtx-1070',
+ 'GTX 1080' => 'gtx-1080',
+ 'Guardians Of The Galaxy' => 'guardians-of-the-galaxy',
+ 'Gucci' => 'gucci',
+ 'Guinness' => 'guinness',
+ 'Guitar' => 'guitar',
+ 'Guitar Hero' => 'guitar-hero',
+ 'Gullivers' => 'gullivers',
+ 'Gym' => 'gym',
+ 'Gym Membership' => 'gym-membership',
+ 'H1z1' => 'h1z1',
+ 'Habitat' => 'habitat',
+ 'Hair' => 'hair',
+ 'Hair Clipper' => 'hair-clipper',
+ 'Hair Dryer' => 'hair-dryer',
+ 'Hair Dye' => 'hair-dye',
+ 'Halifax' => 'halifax',
+ 'Halo' => 'halo',
+ 'Halo 5' => 'halo-5',
+ 'Hammer' => 'hammer',
+ 'Hammock' => 'hammock',
+ 'Hamper' => 'hamper',
+ 'Handbag' => 'handbag',
+ 'Hand Mixer' => 'hand-mixer',
+ 'Happyland' => 'happyland',
+ 'Hard Drive' => 'hard-drive',
+ 'Haribo' => 'haribo',
+ 'Harman Kardon' => 'harman-kardon',
+ 'Harmony' => 'harmony',
+ 'Harry Potter' => 'harry-potter',
+ 'Hat' => 'hat',
+ 'Hatchimals' => 'hatchimals',
+ 'Hayfever' => 'hayfever',
+ 'Hdr Tv' => 'hdr-tv',
+ 'HD TV' => 'hd-tv',
+ 'Headboard' => 'headboard',
+ 'Headphones' => 'headphones',
+ 'Headset' => 'headset',
+ 'Health & Beauty' => 'beauty',
+ 'Heater' => 'heater',
+ 'Hedge Trimmer' => 'hedge-trimmer',
+ 'Heineken' => 'heineken',
+ 'Heinz' => 'heinz',
+ 'Helmet' => 'helmet',
+ 'Hermes' => 'hermes',
+ 'Highchair' => 'highchair',
+ 'Hiking' => 'hiking',
+ 'Hilton' => 'hilton',
+ 'Hisense' => 'hisense',
+ 'Hitachi' => 'hitachi',
+ 'Hitman' => 'hitman',
+ 'Hive' => 'hive',
+ 'Hob' => 'hob',
+ 'Holiday Inn' => 'holiday-inn',
+ 'Holidays & Leisure' => 'holiday',
+ 'Home & Garden' => 'home',
+ 'Home Cinema' => 'home-cinema',
+ 'Homedics' => 'homedics',
+ 'Homefront' => 'homefront',
+ 'Homeplug' => 'homeplug',
+ 'Home Security' => 'home-security',
+ 'Honey' => 'honey',
+ 'Honeywell' => 'honeywell',
+ 'Hong Kong' => 'hong-kong',
+ 'Honor' => 'honor',
+ 'Honor 6x' => 'honor-6x',
+ 'Hoodie' => 'hoodie',
+ 'Hoover' => 'hoover',
+ 'Horizon Zero Dawn' => 'horizon-zero-dawn',
+ 'Hornby' => 'hornby',
+ 'Hose' => 'hose',
+ 'Hotel' => 'hotel',
+ 'Hotpoint' => 'hotpoint',
+ 'Hot Tub' => 'hot-tub',
+ 'Hot Wheels' => 'hot-wheels',
+ 'Hozelock' => 'hozelock',
+ 'HP' => 'hp',
+ 'HP Envy' => 'hp-envy',
+ 'HP Laptop' => 'hp-laptop',
+ 'H Samuel' => 'h-samuel',
+ 'HTC' => 'htc',
+ 'HTC 10' => 'htc-10',
+ 'HTC Vive' => 'htc-vive',
+ 'Huawei' => 'huawei',
+ 'Huawei P9' => 'huawei-p9',
+ 'Huggies' => 'huggies',
+ 'Hugo Boss' => 'hugo-boss',
+ 'Humax' => 'humax',
+ 'Humidifier' => 'humidifier',
+ 'Hunter' => 'hunter',
+ 'Hydro 5' => 'hydro-5',
+ 'Hyperx' => 'hyperx',
+ 'Hyundai' => 'hyundai',
+ 'Iams Pet Food' => 'iams',
+ 'Ibiza' => 'ibiza',
+ 'Icandy' => 'icandy',
+ 'Ice Cream' => 'ice-cream',
+ 'Ice Cream Maker' => 'ice-cream-maker',
+ 'Imaginext' => 'imaginext',
+ 'Impact Driver' => 'impact-driver',
+ 'Indesit' => 'indesit',
+ 'India' => 'india',
+ 'Inflatable' => 'inflatable',
+ 'Injustice' => 'injustice',
+ 'Ink' => 'ink',
+ 'Inner Tube' => 'inner-tube',
+ 'Instant Ink' => 'instant-ink',
+ 'Insulation' => 'insulation',
+ 'Insurance' => 'insurance',
+ 'Intel' => 'intel',
+ 'Intel Atom' => 'atom',
+ 'Intel i3' => 'i3',
+ 'Intel i5' => 'i5',
+ 'Intel i7' => 'i7',
+ 'Internal Hard Drive' => 'internal-hard-drive',
+ 'Internet' => 'internet',
+ 'In The Night Garden' => 'in-the-night-garden',
+ 'iOS' => 'ios',
+ 'iPad' => 'ipad',
+ 'iPad Air' => 'ipad-air',
+ 'iPad Case' => 'ipad-case',
+ 'iPad Mini' => 'ipad-mini',
+ 'iPad Pro' => 'ipad-pro',
+ '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 Case' => 'iphone-case',
+ 'Iphone SE' => 'iphone-se',
+ 'iPod' => 'ipod',
+ 'iPod Nano' => 'ipod-nano',
+ 'iPod Touch' => 'ipod-touch',
+ 'Ireland' => 'ireland',
+ 'Irn Bru' => 'irn-bru',
+ 'Iron' => 'iron',
+ 'Ironing Board' => 'ironing-board',
+ 'Isle Of Wight' => 'isle-of-wight',
+ 'Isofix' => 'isofix',
+ 'Issey Miyake' => 'issey-miyake',
+ 'Italy' => 'italy',
+ 'iTunes' => 'itunes',
+ 'ITV' => 'itv',
+ 'Jabra' => 'jabra',
+ 'Jack Daniels' => 'jack-daniels',
+ 'Jacket' => 'jacket',
+ 'Jack Wills' => 'jack-wills',
+ 'Jack Wolfskin' => 'jack-wolfskin',
+ 'Jaguar' => 'jaguar',
+ 'Jamaica' => 'jamaica',
+ 'Jameson' => 'jameson',
+ 'Japan' => 'japan',
+ 'Jawbone' => 'jawbone',
+ 'Jaybird' => 'jaybird',
+ 'JBL' => 'jbl',
+ 'Jeans' => 'jeans',
+ 'Jewellery' => 'jewellery',
+ 'Jigsaw' => 'jigsaw',
+ 'Jim Beam' => 'jim-beam',
+ 'Jimmy Choo' => 'jimmy-choo',
+ 'Joop' => 'joop',
+ 'Jordan' => 'jordan',
+ 'Joseph Joseph' => 'joseph-joseph',
+ 'Joules' => 'joules',
+ 'Juice' => 'juice',
+ 'Juicer' => 'juicer',
+ 'Jumper' => 'jumper',
+ 'Jumperoo' => 'jumperoo',
+ 'Jura' => 'jura',
+ 'Just Cause 3' => 'just-cause-3',
+ 'Just Dance' => 'just-dance',
+ 'JVC' => 'jvc',
+ 'Karcher' => 'karcher',
+ 'Kaspersky' => 'kaspersky',
+ 'Kayak' => 'kayak',
+ 'Keg' => 'keg',
+ 'Kenwood' => 'kenwood',
+ 'Kenwood kMix' => 'kmix',
+ 'Keter' => 'keter',
+ 'Kettle' => 'kettle',
+ 'Kettlebell' => 'kettlebell',
+ 'Keyboard' => 'keyboard',
+ 'Kia' => 'kia',
+ 'Kickers' => 'kickers',
+ 'Kids Bike' => 'kids-bike',
+ 'Kinder' => 'kinder',
+ 'Kindle' => 'kindle',
+ 'Kindle Fire' => 'kindle-fire',
+ 'Kindle Paperwhite' => 'kindle-paperwhite',
+ 'Kinect' => 'kinect',
+ 'Kingdom Hearts' => 'kingdom-hearts',
+ 'King Size' => 'king-size',
+ 'Kirby' => 'kirby',
+ 'Kitchen' => 'kitchen',
+ 'KitchenAid' => 'kitchenaid',
+ 'Kitchen Roll' => 'kitchen-roll',
+ 'Kitsound' => 'kitsound',
+ 'Knickers' => 'knickers',
+ 'Knife' => 'knife',
+ 'Kobo' => 'kobo',
+ 'Kodi' => 'kodi',
+ 'Kopparberg' => 'kopparberg',
+ 'Kraken' => 'kraken',
+ 'Krakow' => 'krakow',
+ 'Krispy Kreme' => 'krispy-kreme',
+ 'Kurt Geiger' => 'kurt-geiger',
+ 'Lacoste' => 'lacoste',
+ 'Ladder' => 'ladder',
+ 'Lamb' => 'lamb',
+ 'Laminate' => 'laminate',
+ 'Lamp' => 'lamp',
+ 'Lancome' => 'lancome',
+ 'Laptop' => 'laptop',
+ 'Laser Printer' => 'laser-printer',
+ 'Laura Ashley' => 'laura-ashley',
+ 'Lavazza' => 'lavazza',
+ 'Lavender' => 'lavender',
+ 'Lawnmower' => 'lawnmower',
+ 'Lay-Z-Spa' => 'lay-z-spa',
+ 'Le Creuset' => 'le-creuset',
+ 'LED Bulbs' => 'led-bulbs',
+ 'LED TV' => 'led-tv',
+ 'Leeds' => 'leeds',
+ 'Lego' => 'lego',
+ 'Lego City' => 'lego-city',
+ 'Lego Dimensions' => 'lego-dimensions',
+ 'Lego Friends' => 'lego-friends',
+ 'Legoland' => 'legoland',
+ 'Lego Star Wars' => 'lego-star-wars',
+ 'Lego Technic' => 'lego-technic',
+ 'Leicester' => 'leicester',
+ 'Lenovo' => 'lenovo',
+ 'Lenovo Tablet' => 'lenovo-tablet',
+ 'Lenovo Yoga' => 'lenovo-yoga',
+ 'Levi' => 'levi',
+ 'LG' => 'lg',
+ 'LG G5' => 'lg-g5',
+ 'LG G6' => 'lg-g6',
+ 'LG TV' => 'lg-tv',
+ 'Light' => 'light',
+ 'Lighting' => 'lighting',
+ 'Lightning Cable' => 'lightning-cable',
+ 'Lindor' => 'lindor',
+ 'Lindt' => 'lindt',
+ 'Lingerie' => 'lingerie',
+ 'Linx' => 'linx',
+ 'Little Tikes' => 'little-tikes',
+ 'Liverpool' => 'liverpool',
+ 'Logitech' => 'logitech',
+ 'London' => 'london',
+ 'London Eye' => 'london-eye',
+ 'London Zoo' => 'london-zoo',
+ 'Lottery' => 'lottery',
+ 'Lounger' => 'lounger',
+ 'Lurpak' => 'lurpak',
+ 'Lynx' => 'lynx',
+ 'MacBook' => 'macbook',
+ 'MacBook Air' => 'macbook-air',
+ 'MacBook Pro' => 'macbook-pro',
+ 'Mac Mini' => 'mac-mini',
+ 'Mad Max' => 'mad-max',
+ 'Mafia 3' => 'mafia-3',
+ 'Magazine' => 'magazine',
+ 'Magimix' => 'magimix',
+ 'Majorca' => 'majorca',
+ 'Make Up' => 'make-up',
+ 'Makita' => 'makita',
+ 'Maldives' => 'maldives',
+ 'Manchester' => 'manchester',
+ 'Manfrotto' => 'manfrotto',
+ 'Marc Jacobs' => 'marc-jacobs',
+ 'Mario' => 'mario',
+ 'Mario Kart' => 'mario-kart',
+ 'Mario Kart 8' => 'mario-kart-8',
+ 'Mario Kart 8 Deluxe' => 'mario-kart-8-deluxe',
+ 'Marvel' => 'marvel',
+ 'Mascara' => 'mascara',
+ 'Massage' => 'massage',
+ 'Mass Effect' => 'mass-effect',
+ 'Mass Effect Andromeda' => 'mass-effect-andromeda',
+ 'Maternity' => 'maternity',
+ 'Mattress' => 'mattress',
+ 'Mattress Topper' => 'mattress-topper',
+ 'Mavic' => 'mavic',
+ 'Maxi Cosi' => 'maxi-cosi',
+ 'Meat' => 'meat',
+ 'Mechanical Keyboard' => 'mechanical-keyboard',
+ 'Medion' => 'medion',
+ 'Memory Foam' => 'memory-foam',
+ 'Mens Boots' => 'mens-boots',
+ 'Mens Fashion' => 'mens-clothing',
+ 'Mens Shoes' => 'mens-shoes',
+ 'Mercedes' => 'mercedes',
+ 'Merlin' => 'merlin',
+ 'Merrell' => 'merrell',
+ 'Mexico' => 'mexico',
+ 'Michael Kors' => 'michael-kors',
+ 'Microphone' => 'microphone',
+ 'Micro SD' => 'micro-sd',
+ 'Microserver' => 'microserver',
+ 'Microsoft' => 'microsoft',
+ 'Microsoft Office' => 'microsoft-office',
+ 'Microwave' => 'microwave',
+ 'Miele' => 'miele',
+ 'Milwaukee' => 'milwaukee',
+ 'Minecraft' => 'minecraft',
+ 'Mini' => 'mini',
+ 'Mini Fridge' => 'mini-fridge',
+ 'Mini PC' => 'mini-pc',
+ 'Mirror' => 'mirror',
+ 'Mitre Saw' => 'mitre-saw',
+ 'Mobile Broadband' => 'mobile-broadband',
+ 'Mobile Contract' => 'mobile-contract',
+ 'Mobile Phone' => 'mobile-phone',
+ 'Mobiles' => 'mobiles',
+ 'Molton Brown' => 'molton-brown',
+ 'Monitor' => 'monitor',
+ 'Monopoly' => 'monopoly',
+ 'Monsoon' => 'monsoon',
+ 'Mop' => 'mop',
+ 'Morocco' => 'morocco',
+ 'Mortgage' => 'mortgage',
+ 'Moses Basket' => 'moses-basket',
+ 'Mot' => 'mot',
+ 'Motherboard' => 'motherboard',
+ 'Moto 360' => 'moto-360',
+ 'Moto G' => 'moto-g',
+ 'Moto G4' => 'moto-g4',
+ 'Moto G5' => 'moto-g5',
+ 'Motorcycle' => 'motorcycle',
+ 'Motorola' => 'motorola',
+ 'Moto Z' => 'moto-z',
+ 'Mountain Bike' => 'mountain-bike',
+ 'Mouse' => 'mouse',
+ 'Mouse Mat' => 'mouse-mat',
+ 'Movie' => 'movie',
+ 'MP3 Player' => 'mp3-player',
+ 'MSI' => 'msi',
+ 'Mug' => 'mug',
+ 'Multitool' => 'multitool',
+ 'Music' => 'music',
+ 'My Little Pony' => 'my-little-pony',
+ 'Nandos' => 'nandos',
+ 'NAS' => 'nas',
+ 'National Express' => 'national-express',
+ 'National Trust' => 'national-trust',
+ 'Necklace' => 'necklace',
+ 'Nectar' => 'nectar',
+ 'Neff' => 'neff',
+ 'Nerf' => 'nerf',
+ 'Nescafe' => 'nescafe',
+ 'Nespresso' => 'nespresso',
+ 'Nest' => 'nest',
+ 'Netflix' => 'netflix',
+ 'Netgear' => 'netgear',
+ 'Netgear Arlo' => 'arlo',
+ 'New Balance' => 'new-balance',
+ 'Newcastle' => 'newcastle',
+ 'New Look' => 'new-look',
+ 'New York' => 'new-york',
+ 'Nextbase' => 'nextbase',
+ 'Nexus' => 'nexus',
+ 'NFL' => 'nfl',
+ 'Nier' => 'nier',
+ 'Nike' => 'nike',
+ 'Nike Air Max' => 'nike-air-max',
+ 'Nikon' => 'nikon',
+ 'Nilfisk' => 'nilfisk',
+ 'Ninja' => 'ninja',
+ 'Nintendo' => 'nintendo',
+ 'Nintendo DS' => 'nintendo-ds',
+ 'Nintendo eShop' => 'eshop',
+ 'Nintendo Switch' => 'nintendo-switch',
+ 'Nioh' => 'nioh',
+ 'Nissan' => 'nissan',
+ 'Nivea' => 'nivea',
+ 'Nokia' => 'nokia',
+ 'North Face' => 'north-face',
+ 'Norton' => 'norton',
+ 'Note 4' => 'note-4',
+ 'Now TV' => 'now-tv',
+ 'Nursery' => 'nursery',
+ 'Nus' => 'nus',
+ 'Nutella' => 'nutella',
+ 'Nutribullet' => 'nutribullet',
+ 'Nutri Ninja' => 'nutri-ninja',
+ 'NVIDIA' => 'nvidia',
+ 'O2' => 'o2',
+ 'O2 Refresh' => 'o2-refresh',
+ 'Oak' => 'oak',
+ 'Oakley' => 'oakley',
+ 'Oculus' => 'oculus',
+ 'Odeon' => 'odeon',
+ 'Office' => 'office',
+ 'Office Chair' => 'office-chair',
+ 'OLED TV' => 'oled',
+ 'Olympus' => 'olympus',
+ 'Oneplus' => 'oneplus',
+ 'Onkyo' => 'onkyo',
+ 'Oral-B' => 'oral-b',
+ 'Origin' => 'origin',
+ 'Orlando' => 'orlando',
+ 'Osprey' => 'osprey',
+ 'Ottoman' => 'ottoman',
+ 'Outdoor' => 'outdoor',
+ 'Oven' => 'oven',
+ 'Overwatch' => 'overwatch',
+ 'Paddling Pool' => 'paddling-pool',
+ 'Paint' => 'paint',
+ 'Pampers' => 'pampers',
+ 'Pan' => 'pan',
+ 'Panasonic' => 'panasonic',
+ 'Panasonic Lumix' => 'lumix',
+ 'Pandora' => 'pandora',
+ 'Papa Johns' => 'papa-johns',
+ 'Parasol' => 'parasol',
+ 'Parcel' => 'parcel',
+ 'Paris' => 'paris',
+ 'Parking' => 'parking',
+ 'Paw Patrol' => 'paw-patrol',
+ 'PAYG' => 'payg',
+ 'Paypal' => 'paypal',
+ 'PC' => 'pc',
+ 'PC Case' => 'pc-case',
+ 'PC Game' => 'pc-game',
+ 'Pebble' => 'pebble',
+ 'Peppa Pig' => 'peppa-pig',
+ 'Pepsi' => 'pepsi',
+ 'Perfume' => 'perfume',
+ 'Persona' => 'persona',
+ 'Persona 5' => 'persona-5',
+ 'Petrol' => 'petrol',
+ 'Philips' => 'philips',
+ 'Philips Hue' => 'philips-hue',
+ 'Phones' => 'phone',
+ 'Photo' => 'photo',
+ 'Piano' => 'piano',
+ 'Pillow' => 'pillow',
+ 'Pizza' => 'pizza',
+ 'Plant' => 'plant',
+ 'Playhouse' => 'playhouse',
+ 'Playmobil' => 'playmobil',
+ 'Playstation' => 'playstation',
+ 'Playstation Plus' => 'playstation-plus',
+ 'Playstation VR' => 'playstation-vr',
+ 'Pokemon' => 'pokemon',
+ 'Pool' => 'pool',
+ 'Power Bank' => 'power-bank',
+ 'Powerline' => 'powerline',
+ 'Power Rangers' => 'power-rangers',
+ 'Pram' => 'pram',
+ 'Pressure Cooker' => 'pressure-cooker',
+ 'Pressure Washer' => 'pressure-washer',
+ 'Printer' => 'printer',
+ 'Projector' => 'projector',
+ 'Protein' => 'protein',
+ 'PS3' => 'ps3',
+ 'PS4' => 'ps4',
+ 'PS4 Controller' => 'ps4-controller',
+ 'PS4 Games' => 'ps4-games',
+ 'PS4 Headset' => 'ps4-headset',
+ 'PS4 Pro' => 'ps4-pro',
+ 'PS4 Slim' => 'ps4-slim',
+ 'PSN' => 'psn',
+ 'PSU' => 'psu',
+ 'PS Vita' => 'ps-vita',
+ 'Puma' => 'puma',
+ 'Pushchair' => 'pushchair',
+ 'Qnap' => 'qnap',
+ 'Quorn' => 'quorn',
+ 'Rab' => 'rab',
+ 'Radiator' => 'radiator',
+ 'Radio' => 'radio',
+ 'Radley' => 'radley',
+ 'Railcard' => 'railcard',
+ 'Ralph Lauren' => 'ralph-lauren',
+ 'RAM' => 'ram',
+ 'Raspberry Pi' => 'raspberry-pi',
+ 'Rattan Garden Furniture' => 'rattan',
+ 'Ray Ban' => 'ray-ban',
+ 'Razer' => 'razer',
+ 'Razor' => 'razor',
+ 'Reebok' => 'reebok',
+ 'Resident Evil' => 'resident-evil',
+ 'Resident Evil 7' => 'resident-evil-7',
+ 'Rice' => 'rice',
+ 'Ring' => 'ring',
+ 'Road Bike' => 'road-bike',
+ 'Rocket League' => 'rocket-league',
+ 'Rogue One' => 'rogue-one',
+ 'Roku' => 'roku',
+ 'Rolex' => 'rolex',
+ 'Roof Box' => 'roof-box',
+ 'Roses' => 'roses',
+ 'Rotary' => 'rotary',
+ 'Router' => 'router',
+ 'Rug' => 'rug',
+ 'Rum' => 'rum',
+ 'Running' => 'running',
+ 'RX 480' => 'rx-480',
+ 'Ryanair' => 'ryanair',
+ 'Ryobi' => 'ryobi',
+ 'Sale' => 'sale',
+ 'Salmon' => 'salmon',
+ 'Salomon' => 'salomon',
+ 'Samsonite' => 'samsonite',
+ 'Samsung' => 'samsung',
+ 'Samsung Galaxy' => 'samsung-galaxy',
+ 'Samsung Galaxy S7' => 'samsung-galaxy-s7',
+ 'Samsung Galaxy S7 Edge' => 'samsung-galaxy-s7-edge',
+ 'Samsung Galaxy S8' => 'samsung-galaxy-s8',
+ 'Samsung Galaxy S8 Plus' => 'samsung-s8-plus',
+ 'Samsung Gear' => 'samsung-gear',
+ 'Samsung TV' => 'samsung-tv',
+ 'Sandals' => 'sandals',
+ 'Sander' => 'sander',
+ 'SanDisk' => 'sandisk',
+ 'Sat Nav' => 'sat-nav',
+ 'Saw' => 'saw',
+ 'Scalextric' => 'scalextric',
+ 'Scooter' => 'scooter',
+ 'Screenwash' => 'screenwash',
+ 'Screwdriver' => 'screwdriver',
+ 'SD Card' => 'sd-card',
+ 'SDXC' => 'sdxc',
+ 'Seagate' => 'seagate',
+ 'Seat' => 'seat',
+ 'Security Camera' => 'security-camera',
+ 'Seeds' => 'seeds',
+ 'Seiko' => 'seiko',
+ 'Sennheiser' => 'sennheiser',
+ 'Server' => 'server',
+ 'Sewing Machine' => 'sewing-machine',
+ 'Shadow Of Mordor' => 'shadow-of-mordor',
+ 'Shark' => 'shark',
+ 'Sharpie' => 'sharpie',
+ 'Shaver' => 'shaver',
+ 'Shed' => 'shed',
+ 'Shelves' => 'shelves',
+ 'Shirt' => 'shirt',
+ 'Shoes' => 'shoe',
+ 'Shopkins' => 'shopkins',
+ 'Shorts' => 'shorts',
+ 'Shower' => 'shower',
+ 'Shredder' => 'shredder',
+ 'Sideboard' => 'sideboard',
+ 'Sim' => 'sim',
+ 'Sim Free' => 'sim-free',
+ 'Sim Only' => 'sim-only',
+ 'Sink' => 'sink',
+ 'Skechers' => 'skechers',
+ 'Ski' => 'ski',
+ 'Skoda' => 'skoda',
+ 'Sky' => 'sky',
+ 'Skylanders' => 'skylanders',
+ 'Skyrim' => 'skyrim',
+ 'Sleeping Bag' => 'sleeping-bag',
+ 'Slide' => 'slide',
+ 'Slimming World' => 'slimming-world',
+ 'Slippers' => 'slippers',
+ 'Slow Cooker' => 'slow-cooker',
+ 'SLR Camera' => 'slr',
+ 'Smart' => 'smart',
+ 'Smartphone' => 'smartphone',
+ 'Smartthings' => 'smartthings',
+ 'Smart TV' => 'smart-tv',
+ 'Smartwatch' => 'smartwatch',
+ 'Snapfish' => 'snapfish',
+ 'Socket Set' => 'socket-set',
+ 'Socks' => 'socks',
+ 'Sofa' => 'sofa',
+ 'Software & Apps' => 'software-apps',
+ 'Sonicare' => 'sonicare',
+ 'Sonos' => 'sonos',
+ 'Sony' => 'sony',
+ 'Sony TV' => 'sony-tv',
+ 'Soundbar' => 'soundbar',
+ 'Soup Maker' => 'soup-maker',
+ 'Spa' => 'spa',
+ 'Spain' => 'spain',
+ 'Speakers' => 'speakers',
+ 'Spinner' => 'spinner',
+ 'Spirits' => 'spirits',
+ 'Splatoon' => 'splatoon',
+ 'Sports & Fitness' => 'sports-fitness',
+ 'SSD' => 'ssd',
+ 'Starbucks' => 'starbucks',
+ 'Star Trek' => 'star-trek',
+ 'Star Wars' => 'star-wars',
+ 'Steak' => 'steak',
+ 'Steam' => 'steam',
+ 'Steamer' => 'steamer',
+ 'Steam Iron' => 'steam-iron',
+ 'Steam Link' => 'steam-link',
+ 'Steam Mop' => 'steam-mop',
+ 'Storage' => 'storage',
+ 'Storage Box' => 'storage-box',
+ 'Strimmer' => 'strimmer',
+ 'Student' => 'student',
+ 'Suit' => 'suit',
+ 'Suitcase' => 'suitcase',
+ 'Sun Cream' => 'sun-cream',
+ 'Sunglasses' => 'sunglasses',
+ 'Superdry' => 'superdry',
+ 'Surface' => 'surface',
+ 'Surface Book' => 'surface-book',
+ 'Sweets' => 'sweets',
+ 'Swing' => 'swing',
+ 'Synology' => 'synology',
+ 'T-Shirt' => 't-shirt',
+ 'Table' => 'table',
+ 'Tablet' => 'tablet',
+ 'Table Tennis' => 'table-tennis',
+ 'Tado' => 'tado',
+ 'Tag Heuer' => 'tag-heuer',
+ 'Takeaway' => 'takeaway',
+ 'Talkmobile' => 'talkmobile',
+ 'Tap' => 'tap',
+ 'Tassimo' => 'tassimo',
+ 'Tastecard' => 'tastecard',
+ 'Tea' => 'tea',
+ 'Ted Baker' => 'ted-baker',
+ 'Tefal' => 'tefal',
+ 'Tekken' => 'tekken',
+ 'Tekken 7' => 'tekken-7',
+ 'Telegraph' => 'telegraph',
+ 'Telescope' => 'telescope',
+ 'Tenerife' => 'tenerife',
+ 'Tennis' => 'tennis',
+ 'Tent' => 'tent',
+ 'Tesco Clothing' => 'tesco-clothing',
+ 'Tesla' => 'tesla',
+ 'Thailand' => 'thailand',
+ 'Theatre' => 'theatre',
+ 'The Body Shop' => 'body-shop',
+ 'The Last Guardian' => 'the-last-guardian',
+ 'The Last Of Us' => 'the-last-of-us',
+ 'Theme Park' => 'theme-park',
+ 'Thermometer' => 'thermometer',
+ 'Thermos' => 'thermos',
+ 'Thermostat' => 'thermostat',
+ 'The Sun' => 'the-sun',
+ 'The Witcher 3' => 'the-witcher-3',
+ 'Thinkpad' => 'thinkpad',
+ 'Thomas Sabo' => 'thomas-sabo',
+ 'Thorntons' => 'thorntons',
+ 'Thorpe Park' => 'thorpe-park',
+ 'Throw' => 'throw',
+ 'Thrustmaster' => 'thrustmaster',
+ 'Thule' => 'thule',
+ 'Tights' => 'tights',
+ 'Tile' => 'tile',
+ 'Timberland' => 'timberland',
+ 'Tissot' => 'tissot',
+ 'Titanfall' => 'titanfall',
+ 'Titanfall 2' => 'titanfall-2',
+ 'Toaster' => 'toaster',
+ 'Toddler Bed' => 'toddler-bed',
+ 'Toilet' => 'toilet',
+ 'Toilet Roll' => 'toilet-roll',
+ 'Toilet Seat' => 'toilet-seat',
+ 'Tomb Raider' => 'tomb-raider',
+ 'Tom Clancy' => 'tom-clancy',
+ 'Tom Ford' => 'tom-ford',
+ 'Tommee Tippee' => 'tommee-tippee',
+ 'Toms' => 'toms',
+ 'TomTom' => 'tomtom',
+ 'Tool' => 'tool',
+ 'Toothbrush' => 'toothbrush',
+ 'Toothpaste' => 'toothpaste',
+ 'Toot Toot' => 'toot-toot',
+ 'Torch' => 'torch',
+ 'Torque Wrench' => 'torque-wrench',
+ 'Toshiba' => 'toshiba',
+ 'Towel' => 'towel',
+ 'Toyota' => 'toyota',
+ 'Toys' => 'toy',
+ 'Toy Story' => 'toy-story',
+ 'Tp Link' => 'tp-link',
+ 'Tracksuit' => 'tracksuit',
+ 'Train' => 'train',
+ 'Trainers' => 'trainers',
+ 'Trampoline' => 'trampoline',
+ 'Transcend' => 'transcend',
+ 'Transformers' => 'transformers',
+ 'Travel' => 'travel',
+ 'Travel Insurance' => 'travel-insurance',
+ 'Travelodge' => 'travelodge',
+ 'Travel System' => 'travel-system',
+ 'Treadmill' => 'treadmill',
+ 'Trespass' => 'trespass',
+ 'Trike' => 'trike',
+ 'Tripod' => 'tripod',
+ 'Tripp' => 'tripp',
+ 'Trolley' => 'trolley',
+ 'Trousers' => 'trousers',
+ 'Trunki' => 'trunki',
+ 'Tumble Dryer' => 'tumble-dryer',
+ 'Tuna' => 'tuna',
+ 'Turbo Trainer' => 'turbo-trainer',
+ 'Turkey' => 'turkey',
+ 'Turntable' => 'turntable',
+ 'Turtle Beach' => 'turtle-beach',
+ 'TV' => 'tv',
+ 'TV Stand' => 'tv-stand',
+ 'Tyres' => 'tyres',
+ 'Ubisoft' => 'ubisoft',
+ 'Ue Boom' => 'ue-boom',
+ 'UFC' => 'ufc',
+ 'UGG' => 'ugg',
+ 'Ulefone' => 'ulefone',
+ 'Ultimate Ears UE Boom 2' => 'ue-boom-2',
+ 'Ultimate Outdoors' => 'ultimate-outdoors',
+ 'Ultrabook' => 'ultrabook',
+ 'Ultrawide Monitor' => 'ultrawide',
+ 'Umbrella' => 'umbrella',
+ 'Umi' => 'umi',
+ 'Uncharted' => 'uncharted',
+ 'Uncharted 4' => 'uncharted-4',
+ 'Under Armour' => 'under-armour',
+ 'Underwear' => 'underwear',
+ 'Unicorn' => 'unicorn',
+ 'Unidays' => 'unidays',
+ 'Urban Decay' => 'urban-decay',
+ 'Usa' => 'usa',
+ 'USB Hub' => 'usb-hub',
+ 'USB Memory Stick' => 'flash-drive',
+ 'Usn' => 'usn',
+ 'Vacuum Cleaners' => 'vacuum-cleaners',
+ 'Vango' => 'vango',
+ 'Vanish' => 'vanish',
+ 'Vans' => 'vans',
+ 'Vape' => 'vape',
+ 'Vauxhall' => 'vauxhall',
+ 'Vax' => 'vax',
+ 'Velvet' => 'velvet',
+ 'Venice' => 'venice',
+ 'Versace' => 'versace',
+ 'Vibrator' => 'vibrator',
+ 'Victorinox' => 'victorinox',
+ 'Vileda' => 'vileda',
+ 'Vinyl' => 'vinyl',
+ 'Virgin' => 'virgin',
+ 'Vitamix' => 'vitamix',
+ 'Vodafone' => 'vodafone',
+ 'Vodka' => 'vodka',
+ 'Volvo' => 'volvo',
+ 'VPN' => 'vpn',
+ 'VR' => 'vr',
+ 'VTech' => 'vtech',
+ 'Vue' => 'vue',
+ 'VW' => 'vw',
+ 'Wacom' => 'wacom',
+ 'Waffle Maker' => 'waffle-maker',
+ 'Wahl' => 'wahl',
+ 'Walkers' => 'walkers',
+ 'Walking Boots' => 'walking-boots',
+ 'Walking Dead' => 'walking-dead',
+ 'Wallet' => 'wallet',
+ 'Wallpaper' => 'wallpaper',
+ 'Walsall' => 'walsall',
+ 'Wardrobe' => 'wardrobe',
+ 'Warhammer' => 'warhammer',
+ 'Washer Dryer' => 'washer-dryer',
+ 'Washing Machine' => 'washing-machine',
+ 'Watch' => 'watch',
+ 'Watch Dogs' => 'watch-dogs',
+ 'Watch Dogs 2' => 'watch-dogs-2',
+ 'Water Bottle' => 'water-bottle',
+ 'Water Butt' => 'water-butt',
+ 'Water Filter' => 'water-filter',
+ 'Wayfair' => 'wayfair',
+ 'Webcam' => 'webcam',
+ 'Weber' => 'weber',
+ 'Wedding' => 'wedding',
+ 'Weed' => 'weed',
+ 'Weekend Break' => 'weekend-break',
+ 'Weetabix' => 'weetabix',
+ 'Weight Watchers' => 'weight-watchers',
+ 'Wellies' => 'wellies',
+ 'Wenger' => 'wenger',
+ 'Western Digital' => 'western-digital',
+ 'Wetsuit' => 'wetsuit',
+ 'Wheelbarrow' => 'wheelbarrow',
+ 'Whey' => 'whey',
+ 'Whiskas' => 'whiskas',
+ 'Whisky' => 'whisky',
+ 'Wifi Camera' => 'wifi-camera',
+ 'Wifi Extender' => 'wifi-extender',
+ 'Wii' => 'wii',
+ 'Wii U' => 'wii-u',
+ 'Wii U Pro Controller' => 'wii-u-pro-controller',
+ 'Wileyfox' => 'wileyfox',
+ 'Wilkinson Sword' => 'wilkinson-sword',
+ 'Wimbledon' => 'wimbledon',
+ 'Windows' => 'windows',
+ 'Windows 10' => 'windows-10',
+ 'Wine' => 'wine',
+ 'Wipes' => 'wipes',
+ 'Wireless Headphones' => 'wireless-headphones',
+ 'Wireless Keyboard' => 'wireless-keyboard',
+ 'Witcher' => 'witcher',
+ 'Wok' => 'wok',
+ 'Wolfenstein' => 'wolfenstein',
+ 'Women Fashion' => 'womens-clothes',
+ 'Workbench' => 'workbench',
+ 'World Of Warcraft' => 'world-of-warcraft',
+ 'Worx' => 'worx',
+ 'Wuaki' => 'wuaki',
+ 'WWE' => 'wwe',
+ 'Xbox' => 'xbox',
+ 'Xbox 360' => 'xbox-360',
+ 'Xbox 360 Game' => 'xbox-360-game',
+ 'Xbox Controller' => 'xbox-controller',
+ 'Xbox Gift Card' => 'xbox-gift-card',
+ 'Xbox Headset' => 'xbox-headset',
+ 'Xbox Live' => 'xbox-live',
+ 'Xbox One' => 'xbox-one',
+ '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 Wireless Adapter' => 'xbox-wireless-adapter',
+ 'Xcom' => 'xcom',
+ 'XCOM 2' => 'xcom-2',
+ 'XFX' => 'xfx',
+ 'Xiaomi' => 'xiaomi',
+ 'Xiaomi Redmi' => 'redmi',
+ 'Xperia' => 'xperia',
+ 'Xperia Z3' => 'xperia-z3',
+ 'Xperia Z5' => 'xperia-z5',
+ 'XPS' => 'xps',
+ 'Yakuza' => 'yakuza',
+ 'Yale' => 'yale',
+ 'Yamaha' => 'yamaha',
+ 'Yankee Candle' => 'yankee-candle',
+ 'Yoga' => 'yoga',
+ 'York' => 'york',
+ 'Yorkshire' => 'yorkshire',
+ 'Yoshi' => 'yoshi',
+ 'Youview' => 'youview',
+ 'Yves Saint Laurent' => 'yves-saint-laurent',
+ 'Zante' => 'zante',
+ 'Zanussi' => 'zanussi',
+ 'Zelda' => 'zelda',
+ 'Zelda Breath Of The Wild' => 'zelda-breath-of-the-wild',
+ 'Zenbook' => 'zenbook',
+ 'Zippo' => 'zippo',
+ 'Zizzi' => 'zizzi',
+ 'Zoo' => 'zoo',
+ 'Zoostorm' => 'zoostorm',
+ 'ZOTAC' => 'zotac',
+ 'ZTE' => 'zte',
+ 'ZyXEL' => 'zyxel',
+ )
+ ),
+ 'order' => array(
+ 'name' => 'Order by',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Sort order of deals',
+ 'values' => array(
+ 'From the most to the least hot deal' => '-hot',
+ 'From the most recent deal to the oldest' => '',
+ 'From the most commented deal to the least commented deal' => '-discussed'
+ )
+ )
+ )
+ );
+
+ public $lang = array(
+ 'bridge-uri' => SELF::URI,
+ 'bridge-name' => SELF::NAME,
+ 'context-keyword' => 'Search by keyword(s))',
+ 'context-group' => 'Deals per group',
+ 'uri-group' => '/tag/',
+ 'request-error' => 'Could not request HotUKDeals',
+ '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',
+ 'local-months' => array(
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Occ',
+ 'Nov',
+ 'Dec',
+ 'st',
+ 'nd',
+ 'rd',
+ 'th'
+ ),
+ 'local-time-relative' => array(
+ 'Found ',
+ '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'
+ )
+ );
+
+}
diff --git a/bridges/IPBBridge.php b/bridges/IPBBridge.php
index f3fa14f..5b9d0e0 100644
--- a/bridges/IPBBridge.php
+++ b/bridges/IPBBridge.php
@@ -18,8 +18,8 @@ class IPBBridge extends FeedExpander {
'name' => 'Limit',
'type' => 'number',
'required' => false,
- 'title' => 'Specify how many pages should be fetched (-1: all)',
- 'defaultValue' => 1
+ 'title' => 'Specifies the number of items to return on each request (-1: all)',
+ 'defaultValue' => 10
)
)
);
@@ -161,15 +161,18 @@ class IPBBridge extends FeedExpander {
$next = null; // Holds the URI of the next page
- do {
- // Skip loading HTML on first iteration
- if(!is_null($next)) {
- $html = getSimpleHTMLDOMCached($next);
+ while(true) {
+ $next = $this->$callback($html, is_null($next));
+
+ if(is_null($next) || ($limit > 0 && count($this->items) >= $limit)) {
+ break;
}
- $next = $this->$callback($html, is_null($next));
- $limit--;
- } while(!is_null($next) && $limit <> 0);
+ $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){
diff --git a/bridges/InstagramBridge.php b/bridges/InstagramBridge.php
index 7ae6a45..2539da2 100644
--- a/bridges/InstagramBridge.php
+++ b/bridges/InstagramBridge.php
@@ -6,75 +6,129 @@ class InstagramBridge extends BridgeAbstract {
const URI = 'https://instagram.com/';
const DESCRIPTION = 'Returns the newest images';
- const PARAMETERS = array( array(
- 'u' => array(
- 'name' => 'username',
- 'required' => true
+ const PARAMETERS = array(
+ array(
+ 'u' => array(
+ 'name' => 'username',
+ 'required' => true
+ )
),
- 'media_type' => array(
- 'name' => 'Media type',
- 'type' => 'list',
- 'required' => false,
- 'values' => array(
- 'Both' => 'all',
- 'Video' => 'video',
- 'Picture' => 'picture'
- ),
- 'defaultValue' => 'all'
+ array(
+ 'h' => array(
+ 'name' => 'hashtag',
+ 'required' => true
+ )
+ ),
+ 'global' => array(
+ 'media_type' => array(
+ 'name' => 'Media type',
+ 'type' => 'list',
+ 'required' => false,
+ 'values' => array(
+ 'All' => 'all',
+ 'Story' => 'story',
+ 'Video' => 'video',
+ 'Picture' => 'picture',
+ ),
+ 'defaultValue' => 'all'
+ )
)
- ));
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI())
- or returnServerError('Could not request Instagram.');
+ );
- $innertext = null;
-
- foreach($html->find('script') as $script) {
- if('' === $script->innertext) {
- continue;
- }
-
- $pos = strpos(trim($script->innertext), 'window._sharedData');
- if(0 !== $pos) {
- continue;
- }
+ public function collectData(){
- $innertext = $script->innertext;
- break;
+ if(!is_null($this->getInput('h')) && $this->getInput('media_type') == 'story') {
+ returnClientError('Stories are not supported for hashtags!');
}
- $json = trim(substr($innertext, $pos + 18), ' =;');
- $data = json_decode($json);
+ $data = $this->getInstagramJSON($this->getURI());
- $userMedia = $data->entry_data->ProfilePage[0]->user->media->nodes;
+ if(!is_null($this->getInput('u'))) {
+ $userMedia = $data->entry_data->ProfilePage[0]->graphql->user->edge_owner_to_timeline_media->edges;
+ } else {
+ $userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges;
+ }
foreach($userMedia as $media) {
- // Check media type
- switch($this->getInput('media_type')) {
- case 'all': break;
- case 'video':
- if($media->is_video === false) continue 2;
- break;
- case 'picture':
- if($media->is_video === true) continue 2;
- break;
- default: break;
+ $media = $media->node;
+
+ if(!is_null($this->getInput('u'))) {
+ switch($this->getInput('media_type')) {
+ case 'all': break;
+ case 'video':
+ if($media->__typename != 'GraphVideo') continue 2;
+ break;
+ case 'picture':
+ if($media->__typename != 'GraphImage') continue 2;
+ break;
+ case 'story':
+ if($media->__typename != 'GraphSidecar') continue 2;
+ break;
+ default: break;
+ }
+ } else {
+ if($this->getInput('media_type') == 'video' && !$media->is_video) continue;
}
$item = array();
- $item['uri'] = self::URI . 'p/' . $media->code . '/';
- $item['content'] = '<img src="' . htmlentities($media->display_src) . '" />';
- if (isset($media->caption)) {
- $item['title'] = $media->caption;
+ $item['uri'] = self::URI . 'p/' . $media->shortcode . '/';
+
+ if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
+ $item['title'] = $media->edge_media_to_caption->edges[0]->node->text;
} else {
- $item['title'] = basename($media->display_src);
+ $item['title'] = basename($media->display_url);
}
- $item['timestamp'] = $media->date;
+
+ if(!is_null($this->getInput('u')) && $media->__typename == 'GraphSidecar') {
+ $data = $this->getInstagramStory($item['uri']);
+ $item['content'] = $data[0];
+ $item['enclosures'] = $data[1];
+ } else {
+ $item['content'] = '<img src="' . htmlentities($media->display_url) . '" alt="'. $item['title'] . '" />';
+ $item['enclosures'] = array($media->display_url);
+ }
+
+ $item['timestamp'] = $media->taken_at_timestamp;
+
$this->items[] = $item;
}
}
+ protected function getInstagramStory($uri) {
+
+ $data = $this->getInstagramJSON($uri);
+ $mediaInfo = $data->entry_data->PostPage[0]->graphql->shortcode_media;
+
+ //Process the first element, that isn't in the node graph
+ $caption = $mediaInfo->edge_media_to_caption->edges[0]->node->text;
+
+ $enclosures = [$mediaInfo->display_url];
+ $content = '<img src="' . htmlentities($mediaInfo->display_url) . '" alt="'. $caption . '" />';
+
+ foreach($mediaInfo->edge_sidecar_to_children->edges as $media) {
+
+ $content .= '<img src="' . htmlentities($media->node->display_url) . '" alt="'. $caption . '" />';
+ $enclosures[] = $media->node->display_url;
+
+ }
+
+ return [$content, $enclosures];
+
+ }
+
+ protected function getInstagramJSON($uri) {
+
+ $html = getContents($uri)
+ or returnServerError('Could not request Instagram.');
+ $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';
@@ -86,6 +140,8 @@ class InstagramBridge extends BridgeAbstract {
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'));
}
return parent::getURI();
diff --git a/bridges/IsoHuntBridge.php b/bridges/IsoHuntBridge.php
deleted file mode 100644
index 57038fc..0000000
--- a/bridges/IsoHuntBridge.php
+++ /dev/null
@@ -1,465 +0,0 @@
-<?php
-class IsoHuntBridge extends BridgeAbstract {
- const MAINTAINER = 'logmanoriginal';
- const NAME = 'isoHunt Bridge';
- const URI = 'https://isohunt.to/';
- const CACHE_TIMEOUT = 300; //5min
- const DESCRIPTION = 'Returns the latest results by category or search result';
-
- const PARAMETERS = array(
- /*
- * Get feeds for one of the "latest" categories
- * Notice: The categories "News" and "Top Searches" are received from the main page
- * Elements are sorted by name ascending!
- */
- 'By "Latest" category' => array(
- 'latest_category' => array(
- 'name' => 'Latest category',
- 'type' => 'list',
- 'required' => true,
- 'title' => 'Select your category',
- 'defaultValue' => 'news',
- 'values' => array(
- 'Hot Torrents' => 'hot_torrents',
- 'News' => 'news',
- 'Releases' => 'releases',
- 'Torrents' => 'torrents'
- )
- )
- ),
-
- /*
- * Get feeds for one of the "torrent" categories
- * Make sure to add new categories also to get_torrent_category_index($)!
- * Elements are sorted by name ascending!
- */
- 'By "Torrent" category' => array(
- 'torrent_category' => array(
- 'name' => 'Torrent category',
- 'type' => 'list',
- 'required' => true,
- 'title' => 'Select your category',
- 'defaultValue' => 'anime',
- 'values' => array(
- 'Adult' => 'adult',
- 'Anime' => 'anime',
- 'Books' => 'books',
- 'Games' => 'games',
- 'Movies' => 'movies',
- 'Music' => 'music',
- 'Other' => 'other',
- 'Series & TV' => 'series_tv',
- 'Software' => 'software'
- )
- ),
- 'torrent_popularity' => array(
- 'name' => 'Sort by popularity',
- 'type' => 'checkbox',
- 'title' => 'Activate to receive results by popularity'
- )
- ),
-
- /*
- * Get feeds for a specific search request
- */
- 'Search torrent by name' => array(
- 'search_name' => array(
- 'name' => 'Name',
- 'required' => true,
- 'title' => 'Insert your search query',
- 'exampleValue' => 'Bridge'
- ),
- 'search_category' => array(
- 'name' => 'Category',
- 'type' => 'list',
- 'title' => 'Select your category',
- 'defaultValue' => 'all',
- 'values' => array(
- 'Adult' => 'adult',
- 'All' => 'all',
- 'Anime' => 'anime',
- 'Books' => 'books',
- 'Games' => 'games',
- 'Movies' => 'movies',
- 'Music' => 'music',
- 'Other' => 'other',
- 'Series & TV' => 'series_tv',
- 'Software' => 'software'
- )
- )
- )
- );
-
- public function getURI(){
- $uri = self::URI;
- switch($this->queriedContext) {
- case 'By "Latest" category':
- switch($this->getInput('latest_category')) {
- case 'hot_torrents':
- $uri .= 'statistic/hot/torrents';
- break;
- case 'news':
- break;
- case 'releases':
- $uri .= 'releases.php';
- break;
- case 'torrents':
- $uri .= 'latest.php';
- break;
- }
- break;
- case 'By "Torrent" category':
- $uri .= $this->buildCategoryUri(
- $this->getInput('torrent_category'),
- $this->getInput('torrent_popularity')
- );
- break;
- case 'Search torrent by name':
- $category = $this->getInput('search_category');
- $uri .= $this->buildCategoryUri($category);
- if($category !== 'movies')
- $uri .= '&ihq=' . urlencode($this->getInput('search_name'));
- break;
-
- default: parent::getURI();
- }
-
- return $uri;
- }
-
- public function getName(){
- switch($this->queriedContext) {
- case 'By "Latest" category':
- $categoryName = array_search(
- $this->getInput('latest_category'),
- self::PARAMETERS['By "Latest" category']['latest_category']['values']
- );
- $name = 'Latest ' . $categoryName . ' - ' . self::NAME;
- break;
- case 'By "Torrent" category':
- $categoryName = array_search(
- $this->getInput('torrent_category'),
- self::PARAMETERS['By "Torrent" category']['torrent_category']['values']
- );
- $name = 'Category: ' . $categoryName . ' - ' . self::NAME;
- break;
- case 'Search torrent by name':
- $categoryName = array_search(
- $this->getInput('search_category'),
- self::PARAMETERS['Search torrent by name']['search_category']['values']
- );
- $name = 'Search: "'
- . $this->getInput('search_name')
- . '" in category: '
- . $categoryName . ' - '
- . self::NAME;
- break;
- default: return parent::getName();
- }
-
- return $name;
- }
-
- public function collectData(){
- $html = $this->loadHtml($this->getURI());
-
- switch($this->queriedContext) {
- case 'By "Latest" category':
- switch($this->getInput('latest_category')) {
- case 'hot_torrents':
- $this->getLatestHotTorrents($html);
- break;
- case 'news':
- $this->getLatestNews($html);
- break;
- case 'releases':
- case 'torrents':
- $this->getLatestTorrents($html);
- break;
- }
- break;
- case 'By "Torrent" category':
- if($this->getInput('torrent_category') === 'movies') {
- // This one is special (content wise)
- $this->getMovieTorrents($html);
- } else {
- $this->getLatestTorrents($html);
- }
- break;
- case 'Search torrent by name':
- if($this->getInput('search_category') === 'movies') {
- // This one is special (content wise)
- $this->getMovieTorrents($html);
- } else {
- $this->getLatestTorrents($html);
- }
- break;
- }
- }
-
- #region Helper functions for "Movie Torrents"
-
- private function getMovieTorrents($html){
- $container = $html->find('div#w0', 0);
- if(!$container)
- returnServerError('Unable to find torrent container!');
-
- $torrents = $container->find('article');
- if(!$torrents)
- returnServerError('Unable to find torrents!');
-
- foreach($torrents as $torrent) {
-
- $anchor = $torrent->find('a', 0);
- if(!$anchor)
- returnServerError('Unable to find anchor!');
-
- $date = $torrent->find('small', 0);
- if(!$date)
- returnServerError('Unable to find date!');
-
- $item = array();
-
- $item['uri'] = $this->fixRelativeUri($anchor->href);
- $item['title'] = $anchor->title;
- // $item['author'] =
- $item['timestamp'] = strtotime($date->plaintext);
- $item['content'] = $this->fixRelativeUri($torrent->innertext);
-
- $this->items[] = $item;
- }
- }
-
- #endregion
-
- #region Helper functions for "Latest Hot Torrents"
-
- private function getLatestHotTorrents($html){
- $container = $html->find('div#serps', 0);
- if(!$container)
- returnServerError('Unable to find torrent container!');
-
- $torrents = $container->find('tr');
- if(!$torrents)
- returnServerError('Unable to find torrents!');
-
- // Remove first element (header row)
- $torrents = array_slice($torrents, 1);
-
- foreach($torrents as $torrent) {
-
- $cell = $torrent->find('td', 0);
- if(!$cell)
- returnServerError('Unable to find cell!');
-
- $element = $cell->find('a', 0);
- if(!$element)
- returnServerError('Unable to find element!');
-
- $item = array();
-
- $item['uri'] = $element->href;
- $item['title'] = $element->plaintext;
- // $item['author'] =
- // $item['timestamp'] =
- // $item['content'] =
-
- $this->items[] = $item;
- }
- }
-
- #endregion
-
- #region Helper functions for "Latest News"
-
- private function getLatestNews($html){
- $container = $html->find('div#postcontainer', 0);
- if(!$container)
- returnServerError('Unable to find post container!');
-
- $posts = $container->find('div.index-post');
- if(!$posts)
- returnServerError('Unable to find posts!');
-
- foreach($posts as $post) {
- $item = array();
-
- $item['uri'] = $this->latestNewsExtractUri($post);
- $item['title'] = $this->latestNewsExtractTitle($post);
- $item['author'] = $this->latestNewsExtractAuthor($post);
- $item['timestamp'] = $this->latestNewsExtractTimestamp($post);
- $item['content'] = $this->latestNewsExtractContent($post);
-
- $this->items[] = $item;
- }
- }
-
- private function latestNewsExtractAuthor($post){
- $author = $post->find('small', 0);
- if(!$author)
- returnServerError('Unable to find author!');
-
- // The author is hidden within a string like: 'Posted by {author} on {date}'
- preg_match('/Posted\sby\s(.*)\son/i', $author->innertext, $matches);
-
- return $matches[1];
- }
-
- private function latestNewsExtractTimestamp($post){
- $date = $post->find('small', 0);
- if(!$date)
- returnServerError('Unable to find date!');
-
- // The date is hidden within a string like: 'Posted by {author} on {date}'
- preg_match('/Posted\sby\s.*\son\s(.*)/i', $date->innertext, $matches);
-
- $timestamp = strtotime($matches[1]);
-
- // Make sure date is not in the future (dates are given like 'Nov. 20' without year)
- if($timestamp > time()) {
- $timestamp = strtotime('-1 year', $timestamp);
- }
-
- return $timestamp;
- }
-
- private function latestNewsExtractTitle($post){
- $title = $post->find('a', 0);
- if(!$title)
- returnServerError('Unable to find title!');
-
- return $title->plaintext;
- }
-
- private function latestNewsExtractUri($post){
- $uri = $post->find('a', 0);
- if(!$uri)
- returnServerError('Unable to find uri!');
-
- return $uri->href;
- }
-
- private function latestNewsExtractContent($post){
- $content = $post->find('div', 0);
- if(!$content)
- returnServerError('Unable to find content!');
-
- // Remove <h2>...</h2> (title)
- foreach($content->find('h2') as $element) {
- $element->outertext = '';
- }
-
- // Remove <small>...</small> (author)
- foreach($content->find('small') as $element) {
- $element->outertext = '';
- }
-
- return $content->innertext;
- }
-
- #endregion
-
- #region Helper functions for "Latest Torrents", "Latest Releases" and "Torrent Category"
-
- private function getLatestTorrents($html){
- $container = $html->find('div#serps', 0);
- if(!$container)
- returnServerError('Unable to find torrent container!');
-
- $torrents = $container->find('tr[data-key]');
- if(!$torrents)
- returnServerError('Unable to find torrents!');
-
- foreach($torrents as $torrent) {
- $item = array();
-
- $item['uri'] = $this->latestTorrentsExtractUri($torrent);
- $item['title'] = $this->latestTorrentsExtractTitle($torrent);
- $item['author'] = $this->latestTorrentsExtractAuthor($torrent);
- $item['timestamp'] = $this->latestTorrentsExtractTimestamp($torrent);
- $item['content'] = ''; // There is no valuable content
-
- $this->items[] = $item;
- }
- }
-
- private function latestTorrentsExtractTitle($torrent){
- $cell = $torrent->find('td.title-row', 0);
- if(!$cell)
- returnServerError('Unable to find title cell!');
-
- $title = $cell->find('span', 0);
- if(!$title)
- returnServerError('Unable to find title!');
-
- return $title->plaintext;
- }
-
- private function latestTorrentsExtractUri($torrent){
- $cell = $torrent->find('td.title-row', 0);
- if(!$cell)
- returnServerError('Unable to find title cell!');
-
- $uri = $cell->find('a', 0);
- if(!$uri)
- returnServerError('Unable to find uri!');
-
- return $this->fixRelativeUri($uri->href);
- }
-
- private function latestTorrentsExtractAuthor($torrent){
- $cell = $torrent->find('td.user-row', 0);
- if(!$cell)
- return; // No author
-
- $user = $cell->find('a', 0);
- if(!$user)
- returnServerError('Unable to find user!');
-
- return $user->plaintext;
- }
-
- private function latestTorrentsExtractTimestamp($torrent){
- $cell = $torrent->find('td.date-row', 0);
- if(!$cell)
- returnServerError('Unable to find date cell!');
-
- return strtotime('-' . $cell->plaintext, time());
- }
-
- #endregion
-
- #region Generic helper functions
-
- private function loadHtml($uri){
- $html = getSimpleHTMLDOM($uri);
- if(!$html)
- returnServerError('Unable to load ' . $uri . '!');
-
- return $html;
- }
-
- private function fixRelativeUri($uri){
- return preg_replace('/\//i', self::URI, $uri, 1);
- }
-
- private function buildCategoryUri($category, $order_popularity = false){
- switch($category) {
- case 'anime': $index = 1; break;
- case 'software' : $index = 2; break;
- case 'games' : $index = 3; break;
- case 'adult' : $index = 4; break;
- case 'movies' : $index = 5; break;
- case 'music' : $index = 6; break;
- case 'other' : $index = 7; break;
- case 'series_tv' : $index = 8; break;
- case 'books': $index = 9; break;
- case 'all':
- default: $index = 0; break;
- }
-
- return 'torrents/?iht=' . $index . '&ihs=' . ($order_popularity ? 1 : 0) . '&age=0';
- }
-
- #endregion
-}
diff --git a/bridges/JustETFBridge.php b/bridges/JustETFBridge.php
new file mode 100644
index 0000000..f0223e9
--- /dev/null
+++ b/bridges/JustETFBridge.php
@@ -0,0 +1,353 @@
+<?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,
+ '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',
+ 'required' => true,
+ 'type' => 'list',
+ 'values' => array(
+ 'Englisch' => 'en',
+ 'Deutsch' => 'de',
+ 'Italiano' => 'it'
+ ),
+ 'defaultValue' => 'Englisch'
+ )
+ )
+ );
+
+ public function collectData() {
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Failed loading contents from ' . $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);
+
+ // debugMessage(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 = array();
+
+ 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 = array();
+
+ // Common data
+
+ $item['uri'] = $this->extractNewsUri($article);
+ $item['timestamp'] = $this->extractNewsDate($article);
+ $item['title'] = $this->extractNewsTitle($article);
+
+ if($this->getInput('full')) {
+
+ $uri = $this->extractNewsUri($article);
+
+ $html = getSimpleHTMLDOMCached($uri)
+ or returnServerError('Failed loading full article from ' . $uri);
+
+ $fullArticle = $html->find('div.article', 0)
+ or returnServerError('No content found! Layout might have changed!');
+
+ 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);
+
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ private function extractNewsUri($article) {
+ $element = $article->find('a', 0)
+ or returnServerError('Anchor not found!');
+
+ return $element->href;
+ }
+
+ private function extractNewsDate($article) {
+ $element = $article->find('div.subheadline', 0)
+ or returnServerError('Date not found!');
+
+ // debugMessage($element->plaintext);
+
+ $date = trim(explode('|', $element->plaintext)[0]);
+
+ return $this->fixDate($date);
+ }
+
+ private function extractNewsDescription($article) {
+ $element = $article->find('span.newsText', 0)
+ or returnServerError('Description not found!');
+
+ $element->find('a', 0)->onclick = '';
+
+ // debugMessage($element->innertext);
+
+ return $element->innertext;
+ }
+
+ private function extractNewsTitle($article) {
+ $element = $article->find('h3', 0)
+ or returnServerError('Title not found!');
+
+ return $element->plaintext;
+ }
+
+ private function extractFullArticleContent($article) {
+ $element = $article->find('div.article_body', 0)
+ or returnServerError('Article body not found!');
+
+ // Remove teaser image
+ $element->find('img.teaser-img', 0)->outertext = '';
+
+ // Remove self advertisements
+ foreach($element->find('.call-action') as $adv) {
+ $adv->outertext = '';
+ }
+
+ // Remove tips
+ foreach($element->find('.panel-edu') as $tip) {
+ $tip->outertext = '';
+ }
+
+ // 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;
+ }
+
+ private function extractFullArticleAuthor($article) {
+ $element = $article->find('span[itemprop=name]', 0)
+ or returnServerError('Author not found!');
+
+ return $element->plaintext;
+ }
+
+ #endregion
+
+ #region Profile
+
+ private function collectProfile($html) {
+ $item = array();
+
+ $item['uri'] = $this->getURI();
+ $item['timestamp'] = $this->extractProfileDate($html);
+ $item['title'] = $this->extractProfiletitle($html);
+ $item['author'] = $this->extractProfileAuthor($html);
+ $item['content'] = $this->extractProfileContent($html);
+
+ $this->items[] = $item;
+ }
+
+ private function extractProfileDate($html) {
+ $element = $html->find('div.infobox div.vallabel', 0)
+ or returnServerError('Date not found!');
+
+ // debugMessage($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
+}
diff --git a/bridges/KernelBugTrackerBridge.php b/bridges/KernelBugTrackerBridge.php
index 567ee50..f3135af 100644
--- a/bridges/KernelBugTrackerBridge.php
+++ b/bridges/KernelBugTrackerBridge.php
@@ -45,9 +45,7 @@ class KernelBugTrackerBridge extends BridgeAbstract {
// We use the print preview page for simplicity
$html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple',
86400,
- false,
null,
- 0,
null,
true,
true,
diff --git a/bridges/KununuBridge.php b/bridges/KununuBridge.php
index e99e135..85f7c7b 100644
--- a/bridges/KununuBridge.php
+++ b/bridges/KununuBridge.php
@@ -58,7 +58,7 @@ class KununuBridge extends BridgeAbstract {
break;
}
- return self::URI . $site . '/' . $company . '/' . $section;
+ return self::URI . $site . '/' . $company . '/' . $section . '?sort=update_time_desc';
}
return parent::getURI();
@@ -135,8 +135,8 @@ class KununuBridge extends BridgeAbstract {
* Encodes unmlauts in the given text
*/
private function encodeUmlauts($text){
- $umlauts = Array("/ä/","/ö/","/ü/","/Ä/","/Ö/","/Ü/","/ß/");
- $replace = Array("ae","oe","ue","Ae","Oe","Ue","ss");
+ $umlauts = Array('/ä/','/ö/','/ü/','/Ä/','/Ö/','/Ü/','/ß/');
+ $replace = Array('ae','oe','ue','Ae','Oe','Ue','ss');
return preg_replace($umlauts, $replace, $text);
}
@@ -190,11 +190,11 @@ class KununuBridge extends BridgeAbstract {
* Returns the URI from a given article
*/
private function extractArticleUri($article){
- $anchor = $article->find('ku-company-review-more', 0);
+ $anchor = $article->find('h1.review-title a', 0);
if(is_null($anchor))
returnServerError('Cannot find article URI!');
- return self::URI . $anchor->{'review-url'};
+ return self::URI . $anchor->href;
}
/**
diff --git a/bridges/LeBonCoinBridge.php b/bridges/LeBonCoinBridge.php
index d4da546..927b43e 100644
--- a/bridges/LeBonCoinBridge.php
+++ b/bridges/LeBonCoinBridge.php
@@ -1,11 +1,10 @@
<?php
class LeBonCoinBridge extends BridgeAbstract {
- const MAINTAINER = '16mhz';
+ const MAINTAINER = 'jacknumber';
const NAME = 'LeBonCoin';
const URI = 'https://www.leboncoin.fr/';
- const DESCRIPTION = 'Returns most recent results from LeBonCoin for a
-region, and optionally a category and a keyword .';
+ const DESCRIPTION = 'Returns most recent results from LeBonCoin';
const PARAMETERS = array(
array(
@@ -14,125 +13,137 @@ region, and optionally a category and a keyword .';
'name' => 'Région',
'type' => 'list',
'values' => array(
- 'Toute la France' => 'ile_de_france/occasions',
- 'Alsace' => 'alsace',
- 'Aquitaine' => 'aquitaine',
- 'Auvergne' => 'auvergne',
- 'Basse Normandie' => 'basse_normandie',
- 'Bourgogne' => 'bourgogne',
- 'Bretagne' => 'bretagne',
- 'Centre' => 'centre',
- 'Champagne Ardenne' => 'champagne_ardenne',
- 'Corse' => 'corse',
- 'Franche Comté' => 'franche_comte',
- 'Haute Normandie' => 'haute_normandie',
- 'Ile de France' => 'ile_de_france',
- 'Languedoc Roussillon' => 'languedoc_roussillon',
- 'Limousin' => 'limousin',
- 'Lorraine' => 'lorraine',
- 'Midi Pyrénées' => 'midi_pyrenees',
- 'Nord Pas De Calais' => 'nord_pas_de_calais',
- 'Pays de la Loire' => 'pays_de_la_loire',
- 'Picardie' => 'picardie',
- 'Poitou Charentes' => 'poitou_charentes',
- 'Provence Alpes Côte d\'Azur' => 'provence_alpes_cote_d_azur',
- 'Rhône-Alpes' => 'rhone_alpes',
- 'Guadeloupe' => 'guadeloupe',
- 'Martinique' => 'martinique',
- 'Guyane' => 'guyane',
- 'Réunion' => 'reunion'
+ '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'
)
),
'c' => array(
'name' => 'Catégorie',
'type' => 'list',
'values' => array(
- 'TOUS' => '',
- 'EMPLOI' => '_emploi_',
+ 'Toutes catégories' => '',
+ 'EMPLOI' => array(
+ 'Emploi et recrutement' => '71',
+ 'Offres d\'emploi et jobs' => '33'
+ ),
'VEHICULES' => array(
- 'Tous' => '_vehicules_',
- 'Voitures' => 'voitures',
- 'Motos' => 'motos',
- 'Caravaning' => 'caravaning',
- 'Utilitaires' => 'utilitaires',
- 'Équipement Auto' => 'equipement_auto',
- 'Équipement Moto' => 'equipement_moto',
- 'Équipement Caravaning' => 'equipement_caravaning',
- 'Nautisme' => 'nautisme',
- 'Équipement Nautisme' => 'equipement_nautisme'
+ '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' => '_immobilier_',
- 'Ventes immobilières' => 'ventes_immobilieres',
- 'Locations' => 'locations',
- 'Colocations' => 'colocations',
- 'Bureaux & Commerces' => 'bureaux_commerces'
+ 'Tous' => '8',
+ 'Ventes immobilières' => '9',
+ 'Locations' => '10',
+ 'Colocations' => '11',
+ 'Bureaux & Commerces' => '13'
),
'VACANCES' => array(
- 'Tous' => '_vacances_',
- 'Location gîtes' => 'locations_gites',
- 'Chambres d\'hôtes' => 'chambres_d_hotes',
- 'Campings' => 'campings',
- 'Hôtels' => 'hotels',
- 'Hébergements insolites' => 'hebergements_insolites'
+ 'Tous' => '66',
+ 'Locations & Gîtes' => '12',
+ 'Chambres d\'hôtes' => '67',
+ 'Campings' => '68',
+ 'Hôtels' => '69',
+ 'Hébergements insolites' => '70'
),
'MULTIMEDIA' => array(
- 'Tous' => '_multimedia_',
- 'Informatique' => 'informatique',
- 'Consoles & Jeux vidéo' => 'consoles_jeux_video',
- 'Image & Son' => 'image_son',
- 'Téléphonie' => 'telephonie'
+ 'Tous' => '14',
+ 'Informatique' => '15',
+ 'Consoles & Jeux vidéo' => '43',
+ 'Image & Son' => '16',
+ 'Téléphonie' => '17'
),
'LOISIRS' => array(
- 'Tous' => '_loisirs_',
- 'DVD / Films' => 'dvd_films',
- 'CD / Musique' => 'cd_musique',
- 'Livres' => 'livres',
- 'Animaux' => 'animaux',
- 'Vélos' => 'velos',
- 'Sports & Hobbies' => 'sports_hobbies',
- 'Instruments de musique' => 'instruments_de_musique',
- 'Collection' => 'collection',
- 'Jeux & Jouets' => 'jeux_jouets',
- 'Vins & Gastronomie' => 'vins_gastronomie'
+ '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' => '_materiel_professionnel_',
- 'Matériel Agricole' => 'mateiel_agricole',
- 'Transport - Manutention' => 'transport_manutention',
- 'BTP - Chantier - Gros-Å“uvre' => 'btp_chantier_gros_oeuvre',
- 'Outillage - Matériaux 2nd-œuvre' => 'outillage_materiaux_2nd_oeuvre',
- 'Équipements Industriels' => 'equipement_industriels',
- 'Restauration - Hôtellerie' => 'restauration_hotellerie',
- 'Fournitures de Bureau' => 'fournitures_de_bureau',
- 'Commerces & Marchés' => 'commerces_marches',
- 'Matériel médical' => 'materiel_medical'
+ 'MATERIEL 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' => '_services_',
- 'Prestations de services' => 'prestations_de_services',
- 'Billetterie' => 'billetterie',
- 'Évènements' => 'evenements',
- 'Cours particuliers' => 'cours_particuliers',
- 'Covoiturage' => 'covoiturage'
+ 'Tous' => '31',
+ 'Prestations de services' => '34',
+ 'Billetterie' => '35',
+ 'Evénements' => '49',
+ 'Cours particuliers' => '36',
+ 'Covoiturage' => '65'
),
'MAISON' => array(
- 'Tous' => '_maison_',
- 'Ameublement' => 'ameublement',
- 'Électroménager' => 'electromenager',
- 'Arts de la table' => 'arts_de_la_table',
- 'Décoration' => 'decoration',
- 'Linge de maison' => 'linge_de_maison',
- 'Bricolage' => 'bricolage',
- 'Jardinage' => 'jardinage',
- 'Vêtements' => 'vetements',
- 'Chaussures' => 'chaussures',
- 'Accessoires & Bagagerie' => 'accessoires_bagagerie',
- 'Montres & Bijoux' => 'montres_bijoux',
- 'Équipement bébé' => 'equipement_bebe',
- 'Vêtements bébé' => 'vetements_bebe'
+ 'Tous' => '18',
+ 'Ameublement' => '19',
+ 'Electromé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',
+ 'Equipement bébé' => '23',
+ 'Vêtements bébé' => '54',
),
- 'AUTRES' => 'autres'
+ 'AUTRES' => '37'
+ )
+ ),
+ 'o' => array(
+ 'name' => 'Vendeur',
+ 'type' => 'list',
+ 'values' => array(
+ 'Tous' => '',
+ 'Particuliers' => 'private',
+ 'Professionnels' => 'pro',
)
)
)
@@ -140,50 +151,71 @@ region, and optionally a category and a keyword .';
public function collectData(){
- $category = $this->getInput('c');
- if(empty($category)) {
- $category = 'annonces';
+ $params = array(
+ 'text' => $this->getInput('k'),
+ 'region' => $this->getInput('r'),
+ 'category' => $this->getInput('c'),
+ 'owner_type' => $this->getInput('o'),
+ );
+
+ $url = self::URI . 'recherche/?' . http_build_query($params);
+ $html = getContents($url)
+ or returnServerError('Could not request LeBonCoin. Tried: ' . $url);
+
+ if(!preg_match('/^<script>window.FLUX_STATE[^\r\n]*/m', $html, $matches)) {
+ returnServerError('Could not parse JSON in page content.');
}
- $html = getSimpleHTMLDOM(self::URI
- . $category
- . '/offres/'
- . $this->getInput('r')
- . '/?f=a&th=1&q='
- . urlencode($this->getInput('k')))
- or returnServerError('Could not request LeBonCoin.');
+ $clean_match = str_replace(
+ array('</script>', '<script>window.FLUX_STATE = '),
+ array('', ''),
+ $matches[0]
+ );
+ $json = json_decode($clean_match);
- $list = $html->find('.tabsContent', 0);
- if($list === null) {
+ if($json->adSearch->data->total === 0) {
return;
}
- $tags = $list->find('li');
+ foreach($json->adSearch->data->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;
- foreach($tags as $element) {
+ if(isset($element->location->city)) {
- $element = $element->find('a', 0);
+ $item['city'] = $element->location->city;
+ $item['content'] .= ' -- ' . $element->location->city;
- $item = array();
- $item['uri'] = $element->href;
- $title = html_entity_decode($element->getAttribute('title'));
- $content_image = $element->find('div.item_image', 0)->find('.lazyload', 0);
+ }
+
+ 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($content_image !== null) {
- $content = '<img src="' . $content_image->getAttribute('data-imgsrc') . '" alt="thumbnail">';
- } else {
- $content = "";
}
- $date = $element->find('aside.item_absolute', 0)->find('p.item_sup', 0);
- $detailsList = $element->find('section.item_infos', 0);
+ if(isset($element->images->urls)) {
+
+ $item['thumbnail'] = $element->images->thumb_url;
+ $item['enclosures'] = array();
- for($i = 0; $i <= 1; $i++) $content .= $detailsList->find('p.item_supp', $i)->plaintext;
- $price = $detailsList->find('h3.item_price', 0);
- $content .= $price === null ? '' : $price->plaintext;
+ foreach($element->images->urls as $image) {
+ $item['enclosures'][] = $image;
+ }
+
+ }
- $item['title'] = $title;
- $item['content'] = $content . $date;
$this->items[] = $item;
}
}
diff --git a/bridges/LesJoiesDuCodeBridge.php b/bridges/LesJoiesDuCodeBridge.php
index 34145a1..5f61f95 100644
--- a/bridges/LesJoiesDuCodeBridge.php
+++ b/bridges/LesJoiesDuCodeBridge.php
@@ -22,16 +22,16 @@ class LesJoiesDuCodeBridge extends BridgeAbstract {
// retrieve .gif instead of static .jpg
$images = $temp->find('p img');
foreach($images as $image) {
- $img_src = str_replace(".jpg", ".gif", $image->src);
+ $img_src = str_replace('.jpg', '.gif', $image->src);
$image->src = $img_src;
}
$content = $temp->innertext;
$auteur = $temp->find('i', 0);
- $pos = strpos($auteur->innertext, "by");
+ $pos = strpos($auteur->innertext, 'by');
if($pos > 0) {
- $auteur = trim(str_replace("*/", "", substr($auteur->innertext, ($pos + 2))));
+ $auteur = trim(str_replace('*/', '', substr($auteur->innertext, ($pos + 2))));
$item['author'] = $auteur;
}
diff --git a/bridges/MangareaderBridge.php b/bridges/MangareaderBridge.php
index cd7dddc..9153706 100644
--- a/bridges/MangareaderBridge.php
+++ b/bridges/MangareaderBridge.php
@@ -100,7 +100,7 @@ class MangareaderBridge extends BridgeAbstract {
case 'Get popular mangas':
// Find manga name within "Popular mangas for ..."
$pagetitle = $xpath->query(".//*[@id='bodyalt']/h1")->item(0)->nodeValue;
- $this->request = substr($pagetitle, 0, strrpos($pagetitle, " -"));
+ $this->request = substr($pagetitle, 0, strrpos($pagetitle, ' -'));
$this->getPopularMangas($xpath);
break;
case 'Get manga updates':
@@ -120,7 +120,7 @@ class MangareaderBridge extends BridgeAbstract {
// Return some dummy-data if no content available
if(empty($this->items)) {
$item = array();
- $item['content'] = "<p>No updates available</p>";
+ $item['content'] = '<p>No updates available</p>';
$this->items[] = $item;
}
@@ -143,18 +143,18 @@ class MangareaderBridge extends BridgeAbstract {
$item['title'] = htmlspecialchars($manga->nodeValue);
// Add each chapter to the feed
- $item['content'] = "";
+ $item['content'] = '';
foreach ($chapters as $chapter) {
- if($item['content'] <> "") {
- $item['content'] .= "<br>";
+ if($item['content'] <> '') {
+ $item['content'] .= '<br>';
}
$item['content'] .= "<a href='"
. self::URI
. htmlspecialchars($chapter->getAttribute('href'))
. "'>"
. htmlspecialchars($chapter->nodeValue)
- . "</a>";
+ . '</a>';
}
$this->items[] = $item;
@@ -211,13 +211,13 @@ EOD;
foreach ($chapters as $chapter) {
$item = array();
- $item['title'] = htmlspecialchars($xpath->query("td[1]", $chapter)
+ $item['title'] = htmlspecialchars($xpath->query('td[1]', $chapter)
->item(0)
->nodeValue);
- $item['uri'] = self::URI . $xpath->query("td[1]/a", $chapter)
+ $item['uri'] = self::URI . $xpath->query('td[1]/a', $chapter)
->item(0)
->getAttribute('href');
- $item['timestamp'] = strtotime($xpath->query("td[2]", $chapter)
+ $item['timestamp'] = strtotime($xpath->query('td[2]', $chapter)
->item(0)
->nodeValue);
array_unshift($this->items, $item);
@@ -227,12 +227,12 @@ EOD;
public function getURI(){
switch($this->queriedContext) {
case 'Get latest updates':
- $path = "latest";
+ $path = 'latest';
break;
case 'Get popular mangas':
- $path = "popular";
- if($this->getInput('category') !== "all") {
- $path .= "/" . $this->getInput('category');
+ $path = 'popular';
+ if($this->getInput('category') !== 'all') {
+ $path .= '/' . $this->getInput('category');
}
break;
case 'Get manga updates':
diff --git a/bridges/MydealsBridge.php b/bridges/MydealsBridge.php
new file mode 100644
index 0000000..163cf62
--- /dev/null
+++ b/bridges/MydealsBridge.php
@@ -0,0 +1,144 @@
+<?php
+
+require_once(__DIR__ . '/DealabsBridge.php');
+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 = array(
+ 'Suche nach Stichworten' => array (
+ 'q' => array(
+ 'name' => 'Stichworten',
+ 'type' => 'text',
+ 'required' => true
+ ),
+ 'hide_expired' => array(
+ 'name' => 'Abgelaufenes ausblenden',
+ 'type' => 'checkbox',
+ 'required' => 'true'
+ ),
+ 'hide_local' => array(
+ 'name' => 'Lokales ausblenden',
+ 'type' => 'checkbox',
+ 'title' => 'Deals im physischen Geschäft ausblenden',
+ 'required' => 'true'
+ ),
+ 'priceFrom' => array(
+ 'name' => 'Minimaler Preis',
+ 'type' => 'text',
+ 'title' => 'Minmaler Preis in Euros',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ 'priceTo' => array(
+ 'name' => 'Maximaler Preis',
+ 'type' => 'text',
+ 'title' => 'maximaler Preis in Euro',
+ 'required' => 'false',
+ 'defaultValue' => ''
+ ),
+ ),
+
+ 'Deals pro Gruppen' => array(
+ 'group' => array(
+ 'name' => 'Gruppen',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Gruppe, deren Deals angezeigt werden müssen',
+ 'values' => array(
+ 'Elektronik' => 'elektronik',
+ 'Handy & Smartphone' => 'smartphone',
+ 'Gaming' => 'gaming',
+ 'Software' => 'apps-software',
+ 'Fashion Frauen' => 'fashion-frauen',
+ 'Fashion Männer' => 'fashion-accessoires',
+ 'Beauty & Gesundheit' => 'beauty',
+ 'Family & Kids' => 'family-kids',
+ 'Essen & Trinken' => 'food',
+ 'Freizeit & Reisen' => 'reisen',
+ 'Haushalt & Garten' => 'home-living',
+ 'Entertainment' => 'entertainment',
+ 'Verträge & Finanzen' => 'vertraege-finanzen',
+ 'Coupons' => 'coupons',
+
+ )
+ ),
+ 'order' => array(
+ 'name' => 'sortieren nach',
+ 'type' => 'list',
+ 'required' => 'true',
+ 'title' => 'Sortierung der deals',
+ 'values' => array(
+ 'Vom heißesten zum kältesten Deal' => '',
+ 'Vom jüngsten Deal zum ältesten' => '-new',
+ 'Vom am meisten kommentierten Deal zum am wenigsten kommentierten Deal' => '-discussed'
+ )
+ )
+ )
+ );
+
+ public $lang = array(
+ 'bridge-uri' => SELF::URI,
+ 'bridge-name' => SELF::NAME,
+ 'context-keyword' => 'Suche nach Stichworten',
+ 'context-group' => 'Deals pro Gruppen',
+ 'uri-group' => '/gruppe/',
+ 'request-error' => 'Could not request mydeals',
+ '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',
+ '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 '
+ )
+ );
+
+}
diff --git a/bridges/NasaApodBridge.php b/bridges/NasaApodBridge.php
index 74fd219..8e293e0 100644
--- a/bridges/NasaApodBridge.php
+++ b/bridges/NasaApodBridge.php
@@ -12,7 +12,7 @@ class NasaApodBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI . 'archivepix.html')
or returnServerError('Error while downloading the website content');
- $list = explode("<br>", $html->find('b', 0)->innertext);
+ $list = explode('<br>', $html->find('b', 0)->innertext);
for($i = 0; $i < 3; $i++) {
$line = $list[$i];
@@ -32,7 +32,7 @@ class NasaApodBridge extends BridgeAbstract {
$explanation = $picture_html->find('p', 2)->innertext;
//Extract date from the picture page
- $date = explode(" ", $picture_html->find('p', 1)->innertext);
+ $date = explode(' ', $picture_html->find('p', 1)->innertext);
$item['timestamp'] = strtotime($date[4] . $date[3] . $date[2]);
//Other informations
diff --git a/bridges/NotAlwaysBridge.php b/bridges/NotAlwaysBridge.php
new file mode 100644
index 0000000..f5efff4
--- /dev/null
+++ b/bridges/NotAlwaysBridge.php
@@ -0,0 +1,57 @@
+<?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' => 'all',
+ 'Right' => 'right',
+ 'Working' => 'working',
+ 'Romantic' => 'romantic',
+ 'Related' => 'related',
+ 'Learning' => 'learning',
+ 'Friendly' => 'friendly',
+ 'Hopeless' => 'hopeless',
+ 'Unfiltered' => 'unfiltered'
+ ),
+ 'required' => true
+ )
+ ));
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM($this->getURI())
+ or returnServerError('Could not request NotAlways.');
+ 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();
+ }
+}
diff --git a/bridges/PinterestBridge.php b/bridges/PinterestBridge.php
index 7eeafc1..d2dd890 100644
--- a/bridges/PinterestBridge.php
+++ b/bridges/PinterestBridge.php
@@ -44,7 +44,7 @@ class PinterestBridge extends FeedExpander {
$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"]);
+ $item['content'] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item['content']);
$newitems[] = $item;
}
$this->items = $newitems;
@@ -64,10 +64,10 @@ class PinterestBridge extends FeedExpander {
// provide even less info. Thus we attempt multiple options.
$item['title'] = trim($result['title']);
- if($item['title'] === "")
+ if($item['title'] === '')
$item['title'] = trim($result['rich_summary']['display_name']);
- if($item['title'] === "")
+ if($item['title'] === '')
$item['title'] = trim($result['grid_description']);
$item['timestamp'] = strtotime($result['created_at']);
diff --git a/bridges/PixivBridge.php b/bridges/PixivBridge.php
new file mode 100644
index 0000000..3a4cc93
--- /dev/null
+++ b/bridges/PixivBridge.php
@@ -0,0 +1,73 @@
+<?php
+class PixivBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'Pixiv Bridge';
+ const URI = 'https://www.pixiv.net/';
+ const DESCRIPTION = 'Returns the tag search from pixiv.net';
+
+
+ const PARAMETERS = array( array(
+ 'tag' => array(
+ 'name' => 'Tag to search',
+ 'exampleValue' => 'example',
+ 'required' => true
+ ),
+ ));
+
+
+ public function collectData(){
+
+ $html = getContents(static::URI.'search.php?word=' . urlencode($this->getInput('tag')))
+ or returnClientError('Unable to query pixiv.net');
+ $regex = '/<input type="hidden"id="js-mount-point-search-result-list"data-items="([^"]*)/';
+ $timeRegex = '/img\/([0-9]{4})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\//';
+
+ preg_match_all($regex, $html, $matches, PREG_SET_ORDER, 0);
+ if(!$matches) return;
+
+ $content = json_decode(html_entity_decode($matches[0][1]), true);
+ $count = 0;
+ foreach($content as $result) {
+ if($count == 10) break;
+ $count++;
+
+ $item = array();
+ $item['id'] = $result['illustId'];
+ $item['uri'] = 'https://www.pixiv.net/member_illust.php?mode=medium&illust_id=' . $result['illustId'];
+ $item['title'] = $result['illustTitle'];
+ $item['author'] = $result['userName'];
+
+ preg_match_all($timeRegex, $result['url'], $dt, PREG_SET_ORDER, 0);
+ $elementDate = DateTime::createFromFormat('YmdHis',
+ $dt[0][1] . $dt[0][2] . $dt[0][3] . $dt[0][4] . $dt[0][5] . $dt[0][6]);
+ $item['timestamp'] = $elementDate->getTimestamp();
+
+ $item['content'] = "<img src='" . $this->cacheImage($result['url'], $item['id']) . "' />";
+ $this->items[] = $item;
+ }
+ }
+
+ public function cacheImage($url, $illustId) {
+
+ $url = str_replace('_master1200', '', $url);
+ $url = str_replace('c/240x240/img-master/', 'img-original/', $url);
+ $path = CACHE_DIR . '/pixiv_img';
+
+ if(!is_dir($path))
+ mkdir($path, 0755, true);
+
+ if(!is_file($path . '/' . $illustId . '.jpeg')) {
+ $headers = array('Referer: https://www.pixiv.net/member_illust.php?mode=medium&illust_id=' . $illustId);
+ $illust = getContents($url, $headers);
+ if(strpos($illust, '404 Not Found') !== false) {
+ $illust = getContents(str_replace('jpg', 'png', $url), $headers);
+ }
+ file_put_contents($path . '/' . $illustId . '.jpeg', $illust);
+ }
+
+ return 'cache/pixiv_img/' . $illustId . '.jpeg';
+
+ }
+
+}
diff --git a/bridges/RadioMelodieBridge.php b/bridges/RadioMelodieBridge.php
new file mode 100644
index 0000000..9b3772b
--- /dev/null
+++ b/bridges/RadioMelodieBridge.php
@@ -0,0 +1,30 @@
+<?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 collectData(){
+ $html = getSimpleHTMLDOM(self::URI . 'actu')
+ or returnServerError('Could not request Radio Melodie.');
+ $list = $html->find('div[class=actuitem]');
+ foreach($list as $element) {
+ $item = array();
+
+ // Get picture URL
+ $pictureHTML = $element->find('div[class=picture]');
+ preg_match(
+ '/background-image:url\((.*)\);/',
+ $pictureHTML[0]->getAttribute('style'),
+ $pictures);
+ $pictureURL = $pictures[1];
+
+ $item['enclosures'] = array($pictureURL);
+ $item['uri'] = self::URI . $element->parent()->href;
+ $item['title'] = $element->find('h3', 0)->plaintext;
+ $item['content'] = $element->find('p', 0)->plaintext . '<br/><img src="'.$pictureURL.'"/>';
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/RainbowSixSiegeBridge.php b/bridges/RainbowSixSiegeBridge.php
index 302bb89..d362bbd 100644
--- a/bridges/RainbowSixSiegeBridge.php
+++ b/bridges/RainbowSixSiegeBridge.php
@@ -8,9 +8,9 @@ class RainbowSixSiegeBridge extends BridgeAbstract {
const DESCRIPTION = 'Latest articles from the Rainbow Six Siege blog';
public function collectData(){
- $dlUrl = "https://prod-tridionservice.ubisoft.com/live/v1/News/Latest?templateId=tcm%3A152-7677";
- $dlUrl .= "8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A152-194572-64";
- $dlUrl .= "&keywordList=175426&siteId=undefined&useSeoFriendlyUrl=true";
+ $dlUrl = 'https://prod-tridionservice.ubisoft.com/live/v1/News/Latest?templateId=tcm%3A152-7677';
+ $dlUrl .= '8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A152-194572-64';
+ $dlUrl .= '&keywordList=175426&siteId=undefined&useSeoFriendlyUrl=true';
$jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
$json = json_decode($jsonString, true);
diff --git a/bridges/ReadComicsBridge.php b/bridges/ReadComicsBridge.php
index 33c8ed9..739e6cc 100644
--- a/bridges/ReadComicsBridge.php
+++ b/bridges/ReadComicsBridge.php
@@ -25,7 +25,7 @@ class ReadComicsBridge extends BridgeAbstract {
return $timestamp;
}
- $keywordsList = explode(";", $this->getInput('q'));
+ $keywordsList = explode(';', $this->getInput('q'));
foreach($keywordsList as $keywords) {
$html = $this->getSimpleHTMLDOM(self::URI . 'comic/' . rawurlencode($keywords))
or $this->returnServerError('Could not request readcomics.tv.');
diff --git a/bridges/ReporterreBridge.php b/bridges/ReporterreBridge.php
index db1104c..438c55b 100644
--- a/bridges/ReporterreBridge.php
+++ b/bridges/ReporterreBridge.php
@@ -19,7 +19,7 @@ class ReporterreBridge extends BridgeAbstract {
// Replace all relative urls with absolute ones
$text = preg_replace(
'/(href|src)(\=[\"\'])(?!http)([^"\']+)/ims',
- "$1$2" . self::URI . "$3",
+ '$1$2' . self::URI . '$3',
$text
);
diff --git a/bridges/Rue89Bridge.php b/bridges/Rue89Bridge.php
index 6599122..72f01eb 100644
--- a/bridges/Rue89Bridge.php
+++ b/bridges/Rue89Bridge.php
@@ -9,9 +9,9 @@ class Rue89Bridge extends FeedExpander {
protected function parseItem($item){
$item = parent::parseItem($item);
- $url = "http://api.rue89.nouvelobs.com/export/mobile2/node/"
- . str_replace(" ", "", substr($item['uri'], -8))
- . "/full";
+ $url = 'http://api.rue89.nouvelobs.com/export/mobile2/node/'
+ . str_replace(' ', '', substr($item['uri'], -8))
+ . '/full';
$datas = json_decode(getContents($url), true);
$item['content'] = $datas['node']['body'];
diff --git a/bridges/SexactuBridge.php b/bridges/SexactuBridge.php
index 5bc552a..b0a7174 100644
--- a/bridges/SexactuBridge.php
+++ b/bridges/SexactuBridge.php
@@ -32,7 +32,7 @@ class SexactuBridge extends BridgeAbstract {
$item = array();
$item['author'] = self::AUTHOR;
$item['title'] = $title->plaintext;
- $urlAttribute = "data-href";
+ $urlAttribute = 'data-href';
$uri = $title->$urlAttribute;
if($uri === false)
continue;
diff --git a/bridges/ShanaprojectBridge.php b/bridges/ShanaprojectBridge.php
index e86f772..6eadcb1 100644
--- a/bridges/ShanaprojectBridge.php
+++ b/bridges/ShanaprojectBridge.php
@@ -73,7 +73,7 @@ class ShanaprojectBridge extends BridgeAbstract {
// 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))
+ if(preg_match('/url\(\/\/([^\)]+)\)/i', $anime->parent->style, $matches))
return $matches[1];
returnServerError('Could not extract background image!');
diff --git a/bridges/Shimmie2Bridge.php b/bridges/Shimmie2Bridge.php
index efbcd9b..bdbc504 100644
--- a/bridges/Shimmie2Bridge.php
+++ b/bridges/Shimmie2Bridge.php
@@ -21,7 +21,7 @@ class Shimmie2Bridge extends DanbooruBridge {
protected function getItemFromElement($element){
$item = array();
$item['uri'] = $this->getURI() . $element->href;
- $item['id'] = (int)preg_replace("/[^0-9]/", '', $element->getAttribute(static::IDATTRIBUTE));
+ $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $this->getURI() . $element->find('img', 0)->src;
$item['tags'] = $element->getAttribute('data-tags');
diff --git a/bridges/SoundcloudBridge.php b/bridges/SoundcloudBridge.php
index 92d77da..bfd97cb 100644
--- a/bridges/SoundcloudBridge.php
+++ b/bridges/SoundcloudBridge.php
@@ -14,7 +14,7 @@ class SoundCloudBridge extends BridgeAbstract {
)
));
- const CLIENT_ID = '0aca19eae3843844e4053c6d8fdb7875';
+ const CLIENT_ID = '4jkoEFmZEDaqjwJ9Eih4ATNhcH3vMVfp';
public function collectData(){
diff --git a/bridges/SteamBridge.php b/bridges/SteamBridge.php
index 8d6e4f1..8ff456d 100644
--- a/bridges/SteamBridge.php
+++ b/bridges/SteamBridge.php
@@ -4,7 +4,7 @@ class SteamBridge extends BridgeAbstract {
const NAME = 'Steam Bridge';
const URI = 'https://store.steampowered.com/';
const CACHE_TIMEOUT = 3600; // 1h
- const DESCRIPTION = 'Returns games list';
+ const DESCRIPTION = 'Returns apps list';
const MAINTAINER = 'jacknumber';
const PARAMETERS = array(
'Wishlist' => array(
@@ -47,16 +47,6 @@ class SteamBridge extends BridgeAbstract {
'AED' => 'ae',
),
),
- 'sort' => array(
- 'name' => 'Sort by',
- 'type' => 'list',
- 'values' => array(
- 'Rank' => 'rank',
- 'Date Added' => 'added',
- 'Name' => 'name',
- 'Price' => 'price',
- )
- ),
'only_discount' => array(
'name' => 'Only discount',
'type' => 'checkbox',
@@ -68,49 +58,44 @@ class SteamBridge extends BridgeAbstract {
$username = $this->getInput('username');
$params = array(
- 'cc' => $this->getInput('currency'),
- 'sort' => $this->getInput('sort')
+ 'cc' => $this->getInput('currency')
);
- $url = self::URI . 'wishlist/id/' . $username . '/?' . http_build_query($params);
+ $url = self::URI . 'wishlist/id/' . $username . '?' . http_build_query($params);
- $jsonDataRegex = '/var g_rg(?:WishlistData|AppInfo) = ([^;]*)/';
- $content = getContents($url)
- or returnServerError("Could not request Steam Wishlist. Tried:\n - $url");
-
- preg_match_all($jsonDataRegex, $content, $matches, PREG_SET_ORDER, 0);
+ $targetVariable = 'g_rgAppInfo';
+ $sort = array();
- $appList = json_decode($matches[0][1], true);
- $fullAppList = json_decode($matches[1][1], true);
- //var_dump($matches[1][1]);
- //var_dump($fullAppList);
- $sortedElementList = array_fill(0, count($appList), 0);
- foreach($appList as $app) {
+ $html = '';
+ $html = getSimpleHTMLDOM($url)
+ or returnServerError("Could not request Steam Wishlist. Tried:\n - $url");
- $sortedElementList[$app["priority"] - 1] = $app["appid"];
+ $jsContent = $html->find('.responsive_page_template_content script', 0)->innertext;
+ if(preg_match('/var ' . $targetVariable . ' = (.*?);/s', $jsContent, $matches)) {
+ $appsData = json_decode($matches[1]);
+ } else {
+ returnServerError("Could not parse JS variable ($targetVariable) in page content.");
}
- foreach($sortedElementList as $appId) {
+ foreach($appsData as $id => $element) {
- $app = $fullAppList[$appId];
- $gameTitle = $app["name"];
- $gameUri = "http://store.steampowered.com/app/" . $appId . "/";
- $gameImg = $app["capsule"];
+ $appType = $element->type;
+ $appIsBuyable = 0;
+ $appHasDiscount = 0;
+ $appIsFree = 0;
- $item = array();
- $item['uri'] = $gameUri;
- $item['title'] = $gameTitle;
-
- if(count($app["subs"]) > 0) {
- if($app["subs"][0]["discount_pct"] != 0) {
+ if($element->subs) {
+ $appIsBuyable = 1;
- $item['promoValue'] = $app["subs"][0]["discount_pct"];
- $item['oldPrice'] = $app["subs"][0]["price"] / 100 / ((100 - $gamePromoValue / 100));
- $item['newPrice'] = $app["subs"][0]["price"] / 100;
- $item['price'] = $item['newPrice'];
+ if($element->subs[0]->discount_pct) {
- $item['hasPromo'] = true;
+ $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;
+ $appNewPrice = $discountBlock->find('.discount_final_price', 0)->plaintext;
+ $appPrice = $appNewPrice;
} else {
@@ -118,15 +103,55 @@ class SteamBridge extends BridgeAbstract {
continue;
}
- $item['price'] = $app["subs"][0]["price"] / 100;
- $item['hasPromo'] = false;
+ $appPrice = $element->subs[0]->price / 100;
+ }
+
+ } else {
+
+ if($this->getInput('only_discount')) {
+ continue;
+ }
+
+ if(isset($element->free) && $element->free = 1) {
+ $appIsFree = 1;
}
+ }
+ $item = array();
+ $item['uri'] = "http://store.steampowered.com/app/$id/";
+ $item['title'] = $element->name;
+ $item['type'] = $appType;
+ $item['cover'] = str_replace('_292x136', '', $element->capsule);
+ $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));
}
- $this->items[] = $item;
+ if($appHasDiscount) {
+
+ $item['discount']['value'] = $appDiscountValue;
+ $item['discount']['oldPrice'] = floatval(str_replace(',', '.', $appOldPrice));
+ $item['discount']['newPrice'] = floatval(str_replace(',', '.', $appNewPrice));
+
+ }
+
+ $item['enclosures'] = array();
+ $item['enclosures'][] = str_replace('_292x136', '', $element->capsule);
+
+ foreach($element->screenshots as $screenshot) {
+ $item['enclosures'][] = substr($element->capsule, 0, -31) . $screenshot;
+ }
+
+ $sort[$id] = $element->priority;
+ $this->items[] = $item;
}
+ array_multisort($sort, SORT_ASC, $this->items);
}
}
diff --git a/bridges/SupInfoBridge.php b/bridges/SupInfoBridge.php
new file mode 100644
index 0000000..40160e4
--- /dev/null
+++ b/bridges/SupInfoBridge.php
@@ -0,0 +1,57 @@
+<?php
+class SupInfoBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'SupInfoBridge';
+ const URI = 'https://www.supinfo.com';
+ const DESCRIPTION = 'Returns the newest articles.';
+
+ const PARAMETERS = array(array(
+ 'tag' => array(
+ 'name' => 'Category (not mandatory)',
+ 'type' => 'text',
+ )
+ ));
+
+ public function collectData() {
+
+ if(empty($this->getInput('tag'))) {
+ $html = getSimpleHTMLDOM(self::URI . '/articles/')
+ or returnServerError('Unable to fetch articles !');
+ } else {
+ $html = getSimpleHTMLDOM(self::URI . '/articles/tag/' . $this->getInput('tag'))
+ or returnServerError('Unable to fetch articles !');
+ }
+ $content = $html->find('#latest', 0)->find('ul[class=courseContent]', 0);
+
+ for($i = 0; $i < 5; $i++) {
+
+ $this->items[] = $this->fetchArticle($content->find('h4', $i)->find('a', 0)->href);
+
+ }
+ }
+
+ public function fetchArticle($link) {
+
+ $articleHTML = getSimpleHTMLDOM(self::URI . $link)
+ or returnServerError('Unable to fetch article !');
+
+ $article = $articleHTML->find('div[id=courseDocZero]', 0);
+ $item = array();
+ $item['author'] = $article->find('#courseMetas', 0)->find('a', 0)->plaintext;
+ $item['id'] = $link;
+ $item['uri'] = self::URI . $link;
+ $item['title'] = $article->find('h1', 0)->plaintext;
+ $date = explode(' ', $article->find('#courseMetas', 0)->find('span', 1)->plaintext);
+ $item['timestamp'] = DateTime::createFromFormat('d/m/Y H:i:s', $date[2] . ' ' . $date[4])->getTimestamp();
+
+ $article->find('div[id=courseHeader]', 0)->innertext = '';
+ $article->find('div[id=author-infos]', 0)->innertext = '';
+ $article->find('div[id=cartouche-tete]', 0)->innertext = '';
+ $item['content'] = $article;
+
+ return $item;
+
+ }
+
+}
diff --git a/bridges/SuperSmashBlogBridge.php b/bridges/SuperSmashBlogBridge.php
new file mode 100644
index 0000000..9216ef6
--- /dev/null
+++ b/bridges/SuperSmashBlogBridge.php
@@ -0,0 +1,45 @@
+<?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) or returnServerError('Error while downloading the website content');
+ $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;
+ }
+ }
+}
diff --git a/bridges/T411Bridge.php b/bridges/T411Bridge.php
deleted file mode 100644
index 15cf628..0000000
--- a/bridges/T411Bridge.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-class T411Bridge extends BridgeAbstract {
-
- const MAINTAINER = 'ORelio';
- const NAME = 'T411 Bridge';
- const URI = 'https://www.t411.al/';
- const DESCRIPTION = 'Returns the 10 newest torrents with specified search
- terms <br /> Use url part after "?" mark when using their search engine.';
-
- const PARAMETERS = array( array(
- 'search' => array(
- 'name' => 'Search criteria',
- 'required' => true
- )
- ));
-
- public function collectData(){
-
- //Utility function for retrieving text based on start and end delimiters
- 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;
- }
-
- //Retrieve torrent listing from search results, which does not contain torrent description
- $url = self::URI
- . 'torrents/search/?search='
- . urlencode($this->getInput('search'))
- . '&order=added&type=desc';
-
- $html = getSimpleHTMLDOM($url)
- or returnServerError('Could not request t411: ' . $url);
-
- $results = $html->find('table.results', 0);
- if (is_null($results))
- returnServerError('No results from t411: ' . $url);
- $limit = 0;
-
- //Process each item individually
- foreach($results->find('tr') as $element) {
-
- //Limit total amount of requests and ignore table header
- if($limit >= 10) {
- break;
- }
- if(is_object($element->find('th', 0))) {
- continue;
- }
-
- //Requests are rate-limited
- usleep(500000); //So we need to wait (500ms)
-
- //Retrieve data from RSS entry
- $item_uri = self::URI
- . 'torrents/details/?id='
- . extractFromDelimiters($element->find('a.nfo', 0)->outertext, '?id=', '"');
-
- $item_title = extractFromDelimiters($element->outertext, '" title="', '"');
- $item_date = strtotime($element->find('dd', 0)->plaintext);
-
- //Retrieve full description from torrent page
- $item_html = getSimpleHTMLDOM($item_uri);
- if(!$item_html) {
- continue;
- }
-
- //Retrieve data from page contents
- $item_desc = $item_html->find('div.description', 0);
- $item_author = $item_html->find('a.profile', 0)->innertext;
-
- //Cleanup advertisments
- $divs = explode('<div class="align-center">', $item_desc->innertext);
- $item_desc = '';
- foreach ($divs as $text)
- if (strpos($text, 'adprovider.adlure.net') === false)
- $item_desc = $item_desc . '<div class="align-center">' . $text;
-
- $item_desc = preg_replace('/<h2 class="align-center">LIENS DE T..?L..?CHARGEMENT<\/h2>/i', '', $item_desc);
-
- //Build and add final item
- $item = array();
- $item['uri'] = $item_uri;
- $item['title'] = $item_title;
- $item['author'] = $item_author;
- $item['timestamp'] = $item_date;
- $item['content'] = $item_desc;
- $this->items[] = $item;
- $limit++;
- }
- }
-}
diff --git a/bridges/VkBridge.php b/bridges/VkBridge.php
index 4eba961..70c0db4 100644
--- a/bridges/VkBridge.php
+++ b/bridges/VkBridge.php
@@ -43,42 +43,263 @@ class VkBridge extends BridgeAbstract
or returnServerError('No results for group or user name "' . $this->getInput('u') . '".');
$text_html = iconv('windows-1251', 'utf-8', $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)->plaintext;
- $this->pageName = $pageName;
+ $pageName = $html->find('.page_name', 0);
+ if (is_object($pageName)) {
+ $pageName = $pageName->plaintext;
+ $this->pageName = htmlspecialchars_decode($pageName);
+ }
+ $pinned_post_item = null;
+ $last_post_id = 0;
foreach ($html->find('.post') as $post) {
+ $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 = '';
}
- $item = array();
- $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<br><img>');
- if (is_object($post->find('a.page_media_link_title', 0))) {
- $link = $post->find('a.page_media_link_title', 0)->getAttribute('href');
- //external link in the post
- $item['content'] .= "\n\rExternal link: "
- . str_replace('/away.php?to=', '', urldecode($link));
+ $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 = '';
+ }
}
- //get video on post
- if (is_object($post->find('span.post_video_title_content', 0))) {
- $titleVideo = $post->find('span.post_video_title_content', 0)->plaintext;
- $linkToVideo = self::URI . $post->find('a.page_post_thumb_video', 0)->getAttribute('href');
- $item['content'] .= "\n\r {$titleVideo}: {$linkToVideo}";
+ // 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 = self::URI . ltrim($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);
+ if (is_object($video)) {
+ $video_title = $video->find('div.post_video_title', 0)->plaintext;
+ $video_link = self::URI . ltrim( $video->find('a.lnk', 0)->getAttribute('href'), '/' );
+ $content_suffix .= "<br>Video: <a href='$video_link'>$video_title</a>";
+ $video->outertext = '';
}
+ // get all other videos
+ foreach($post->find('a.page_post_thumb_video') as $a) {
+ $video_title = $a->getAttribute('aria-label');
+ $temp = explode(' ', $video_title, 2);
+ if (count($temp) > 1) $video_title = $temp[1];
+ $video_link = self::URI . ltrim( $a->getAttribute('href'), '/' );
+ $content_suffix .= "<br>Video: <a href='$video_link'>$video_title</a>";
+ $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 = self::URI . ltrim($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 = self::URI . ltrim($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 = self::URI . ltrim($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 = $pageName;
+ foreach($post->find('a.wall_signed_by') as $a) {
+ $post_author = $a->innertext;
+ $a->outertext = '';
+ }
+
+ if (is_object($post->find('div.copy_quote', 0))) {
+ $copy_quote = $post->find('div.copy_quote', 0);
+ if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
+ $copy_post_header->outertext = '';
+ }
+ $copy_quote_content = $copy_quote->innertext;
+ $copy_quote->outertext = "<br>Reposted: <br>$copy_quote_content";
+ }
+
+ $item = array();
+ $item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<br><img>');
+ $item['content'] .= $content_suffix;
+
// get post link
- $item['uri'] = self::URI . $post->find('a.post_link', 0)->getAttribute('href');
+ $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]);
+ if (substr(self::URI, -1) == '/') {
+ $post_link = self::URI . ltrim($post_link, '/');
+ } else {
+ $post_link = self::URI . $post_link;
+ }
+ $item['uri'] = $post_link;
$item['timestamp'] = $this->getTime($post);
- $item['author'] = $pageName;
- $this->items[] = $item;
+ $item['title'] = $this->getTitle($item['content']);
+ $item['author'] = $post_author;
+ 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)) {
+ return;
+ } else 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'];
+ });
}
}
+ 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] . '.jpg';
+ $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] . '.jpg';
+ }
+
+ if ($original) {
+ return "<a href='$original'><img src='$thumb'></a>";
+ } else {
+ return "<img src='$thumb'>";
+ }
+ }
+
+ private function getTitle($content)
+ {
+ preg_match('/^["\w\ \p{Cyrillic}\(\)\?#«»-]+/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')) {
@@ -109,19 +330,9 @@ class VkBridge extends BridgeAbstract
{
ini_set('user-agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
- $opts = array(
- 'http' => array(
- 'method' => "GET",
- 'user_agent' => ini_get('user_agent'),
- 'accept_encoding' => 'gzip',
- 'header' => "Accept-language: en\r\n
- Cookie: remixlang=3\r\n"
- )
- );
-
- $context = stream_context_create($opts);
+ $header = array('Accept-language: en', 'Cookie: remixlang=3');
- return getContents($this->getURI(), false, $context);
+ return getContents($this->getURI(), $header);
}
diff --git a/bridges/WhydBridge.php b/bridges/WhydBridge.php
index accdb75..347db6e 100644
--- a/bridges/WhydBridge.php
+++ b/bridges/WhydBridge.php
@@ -18,10 +18,10 @@ class WhydBridge extends BridgeAbstract {
public function collectData(){
$html = '';
- if(strlen(preg_replace("/[^0-9a-f]/", '', $this->getInput('u'))) == 24) {
+ 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'))
+ self::URI . 'u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))
) or returnServerError('No results for this query.');
} else { // input may be the username
$html = getSimpleHTMLDOM(
diff --git a/bridges/WorldOfTanksBridge.php b/bridges/WorldOfTanksBridge.php
index f783e29..46dd588 100644
--- a/bridges/WorldOfTanksBridge.php
+++ b/bridges/WorldOfTanksBridge.php
@@ -1,16 +1,12 @@
<?php
-class WorldOfTanksBridge extends BridgeAbstract {
+class WorldOfTanksBridge extends FeedExpander {
- const MAINTAINER = 'mitsukarenai';
+ const MAINTAINER = 'Riduidel';
const NAME = 'World of Tanks';
const URI = 'http://worldoftanks.eu/';
const DESCRIPTION = 'News about the tank slaughter game.';
const PARAMETERS = array( array(
- 'category' => array(
- // TODO: should be a list
- 'name' => 'nom de la catégorie'
- ),
'lang' => array(
'name' => 'Langue',
'type' => 'list',
@@ -26,47 +22,31 @@ class WorldOfTanksBridge extends BridgeAbstract {
)
));
- private $title = '';
-
- public function getURI(){
- if(!is_null($this->getInput('lang'))) {
- $lang = $this->getInput('lang');
- $uri = self::URI . $lang . '/news/';
- if(!empty($this->getInput('category'))) {
- $uri .= 'pc-browser/' . $this->getInput('category') . '/';
- }
- return $uri;
- }
-
- return parent::getURI();
+ public function collectData() {
+ $this->collectExpandableDatas(sprintf('https://worldoftanks.eu/%s/rss/news/', $this->getInput('lang')));
}
- public function getName(){
- return $this->title ?: parent::getName();
+ protected function parseItem($newsItem){
+ $item = parent::parseItem($newsItem);
+ $item['content'] = $this->loadFullArticle($item['uri']);
+ return $item;
}
- public function collectData(){
- $html = getSimpleHTMLDOM($this->getURI())
- or returnServerError('Could not request ' . $this->getURI());
- debugMessage("loaded HTML from " . $this->getURI());
- // customize name
- $this->title = $html->find('title', 0)->innertext;
- foreach($html->find('.b-imgblock_ico') as $infoLink) {
- $this->parseLine($infoLink);
+ /**
+ * 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('article', 0);
+
+ // Remove the scripts, please
+ foreach($content->find('script') as $script) {
+ $script->outertext = '';
}
- }
- private function parseLine($infoLink){
- $item = array();
- $item['uri'] = self::URI . $infoLink->href;
- // now load that uri from cache
- debugMessage('loading page ' . $item['uri']);
- $articlePage = getSimpleHTMLDOMCached($item['uri']);
- $content = $articlePage->find('.l-content', 0);
- defaultLinkTo($content, self::URI);
- $item['title'] = $content->find('h1', 0)->innertext;
- $item['content'] = $content->find('.b-content', 0)->innertext;
- $item['timestamp'] = $content->find('.b-statistic_time', 0)->getAttribute("data-timestamp");
- $this->items[] = $item;
+ return $content->innertext;
}
}
diff --git a/bridges/YGGTorrentBridge.php b/bridges/YGGTorrentBridge.php
new file mode 100644
index 0000000..bc434d3
--- /dev/null
+++ b/bridges/YGGTorrentBridge.php
@@ -0,0 +1,143 @@
+<?php
+
+/* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge
+ * by erwang.providing the functionality of both in one.
+ */
+class YGGTorrentBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'teromene';
+ const NAME = 'Yggtorrent Bridge';
+ const URI = 'https://yggtorrent.is';
+ const DESCRIPTION = 'Returns torrent search from Yggtorrent';
+
+ const PARAMETERS = array(
+ array(
+ 'cat' => array(
+ 'name' => 'category',
+ 'type' => 'list',
+ 'values' => array(
+ 'Toute 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'
+ ),
+ '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];
+
+ $html = getSimpleHTMLDOM(self::URI . '/engine/search?name='
+ . $this->getInput('nom')
+ . '&description='
+ . $this->getInput('description')
+ . '&fichier='
+ . $this->getInput('fichier')
+ . '&file='
+ . $this->getInput('uploader')
+ . '&category='
+ . $category
+ . '&sub_category='
+ . $subcategory
+ . '&do=search')
+ or returnServerError('Unable to query Yggtorrent !');
+
+ $count = 0;
+ $results = $html->find('.results', 0);
+ if(!$results) return;
+
+ foreach($results->find('tr') as $row) {
+ $count++;
+ if($count == 1) continue;
+ if($count == 12) break;
+ $item = array();
+ $item['timestamp'] = $row->find('.hidden', 1)->plaintext;
+ $item['title'] = $row->find('a', 1)->plaintext;
+ $torrentData = $this->collectTorrentData($row->find('a', 1)->href);
+ $item['author'] = $torrentData['author'];
+ $item['content'] = $torrentData['content'];
+ $item['seeders'] = $row->find('td', 7)->plaintext;
+ $item['leechers'] = $row->find('td', 8)->plaintext;
+ $item['size'] = $row->find('td', 5)->plaintext;
+
+ $this->items[] = $item;
+ }
+
+ }
+
+ public function collectTorrentData($url) {
+
+ //For weird reason, the link we get can be invalid, we fix it.
+ $url_full = explode('/', $url);
+ $url_full[4] = urlencode($url_full[4]);
+ $url_full[5] = urlencode($url_full[5]);
+ $url_full[6] = urlencode($url_full[6]);
+ $url = implode('/', $url_full);
+ $page = getSimpleHTMLDOM($url) or returnServerError('Unable to query Yggtorrent page !');
+ $author = $page->find('.informations', 0)->find('a', 4)->plaintext;
+ $content = $page->find('.default', 1);
+ return array('author' => $author, 'content' => $content);
+ }
+}
diff --git a/bridges/YoutubeBridge.php b/bridges/YoutubeBridge.php
index 321fb26..e597fe3 100644
--- a/bridges/YoutubeBridge.php
+++ b/bridges/YoutubeBridge.php
@@ -25,14 +25,14 @@ class YoutubeBridge extends BridgeAbstract {
'By channel id' => array(
'c' => array(
'name' => 'channel id',
- 'exampleValue' => "15",
+ 'exampleValue' => '15',
'required' => true
)
),
'By playlist Id' => array(
'p' => array(
'name' => 'playlist id',
- 'exampleValue' => "15"
+ 'exampleValue' => '15'
)
),
'Search result' => array(
@@ -110,8 +110,8 @@ class YoutubeBridge extends BridgeAbstract {
$this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
}
- private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector){
- $limit = 10;
+ private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector, $add_parsed_items = true) {
+ $limit = $add_parsed_items ? 10 : INF;
$count = 0;
foreach($html->find($element_selector) as $element) {
if($count < $limit) {
@@ -122,12 +122,15 @@ class YoutubeBridge extends BridgeAbstract {
$vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid));
$title = $this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext);
if($title != '[Private Video]' && strpos($vid, 'googleads') === false) {
- $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
- $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ if ($add_parsed_items) {
+ $this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
+ $this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
+ }
$count++;
}
}
}
+ return $count;
}
private function ytBridgeFixTitle($title) {
@@ -137,10 +140,8 @@ class YoutubeBridge extends BridgeAbstract {
private function ytGetSimpleHTMLDOM($url){
return getSimpleHTMLDOM($url,
- $use_include_path = false,
- $context = null,
- $offset = 0,
- $maxLen = null,
+ $header = array(),
+ $opts = array(),
$lowercase = true,
$forceTagsClosed = true,
$target_charset = DEFAULT_TARGET_CHARSET,
@@ -176,16 +177,25 @@ class YoutubeBridge extends BridgeAbstract {
}
} elseif($this->getInput('p')) { /* playlist mode */
$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)
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
- $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a');
+ $item_count = $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a', false);
+ if ($item_count <= 15 && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
+ $this->ytBridgeParseXmlFeed($xml);
+ } else {
+ $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a');
+ }
$this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
+ usort($this->items, function ($item1, $item2) {
+ return $item2['timestamp'] - $item1['timestamp'];
+ });
} elseif($this->getInput('s')) { /* search mode */
$this->request = $this->getInput('s');
$page = 1;
if($this->getInput('pa'))
- $page = (int)preg_replace("/[^0-9]/", '', $this->getInput('pa'));
+ $page = (int)preg_replace('/[^0-9]/', '', $this->getInput('pa'));
$url_listing = self::URI
. 'results?search_query='
@@ -197,7 +207,7 @@ class YoutubeBridge extends BridgeAbstract {
$html = $this->ytGetSimpleHTMLDOM($url_listing)
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
- $this->ytBridgeParseHtmlListing($html, 'div.yt-lockup', 'h3');
+ $this->ytBridgeParseHtmlListing($html, 'div.yt-lockup', 'h3 > a');
$this->feedName = 'Search: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
} else { /* no valid mode */
returnClientError("You must either specify either:\n - YouTube
@@ -216,5 +226,5 @@ class YoutubeBridge extends BridgeAbstract {
default:
return parent::getName();
}
- }
+ }
}
diff --git a/bridges/ZenodoBridge.php b/bridges/ZenodoBridge.php
new file mode 100644
index 0000000..18ef91c
--- /dev/null
+++ b/bridges/ZenodoBridge.php
@@ -0,0 +1,55 @@
+<?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())
+ or returnServerError('zenodo.org not reachable.');
+
+ foreach($html->find('div.record-elem') as $element) {
+ $item = array();
+ $item['uri'] = self::URI . $element->find('h4', 0)->find('a', 0)->href;
+ $item['title'] = trim(
+ htmlspecialchars_decode($element->find('h4', 0)->find('a', 0)->innertext,
+ ENT_QUOTES
+ )
+ );
+ foreach($element->find('p', 0)->find('span') as $authors) {
+ $item['author'] = $item['author'] . $authors . '; ';
+ }
+ $content = $element->find('p.hidden-xs', 0)->find('a', 0)->innertext . '<br>';
+ $type = '<br>Type: ' . $element->find('span.label-default', 0)->innertext;
+
+ $raw_date = $element->find('small.text-muted', 0)->innertext;
+ $clean_date = date_parse(str_replace('Uploaded on ', '', $raw_date));
+
+ $content = $content . date_parse($clean_date);
+
+ $item['timestamp'] = mktime(
+ $clean_date['hour'],
+ $clean_date['minute'],
+ $clean_date['second'],
+ $clean_date['month'],
+ $clean_date['day'],
+ $clean_date['year']
+ );
+
+ $access = '';
+ if ($element->find('span.label-success', 0)->innertext) {
+ $access = 'Open Access';
+ } elseif ($element->find('span.label-warning', 0)->innertext) {
+ $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 20323aa..de17d52 100644
--- a/caches/FileCache.php
+++ b/caches/FileCache.php
@@ -8,7 +8,9 @@ class FileCache implements CacheInterface {
protected $param;
public function loadData(){
- return unserialize(file_get_contents($this->getCacheFile()));
+ if(file_exists($this->getCacheFile())) {
+ return unserialize(file_get_contents($this->getCacheFile()));
+ }
}
public function saveData($datas){
@@ -17,7 +19,7 @@ class FileCache implements CacheInterface {
$writeStream = file_put_contents($this->getCacheFile(), serialize($datas));
if($writeStream === false) {
- throw new \Exception("Cannot write the cache... Do you have the right permissions ?");
+ throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
}
return $this;
diff --git a/config.default.ini.php b/config.default.ini.php
new file mode 100644
index 0000000..5909ad8
--- /dev/null
+++ b/config.default.ini.php
@@ -0,0 +1,44 @@
+; <?php exit; ?> DO NOT REMOVE THIS LINE
+
+; This file contains the default settings for RSS-Bridge. Do not change this
+; file, it will be replaced on the next update of RSS-Bridge! You can specify
+; your own configuration in 'config.ini.php' (copy this file).
+
+[cache]
+
+; Allow users to specify custom timeout for specific requests.
+; true = enabled
+; false = disabled (default)
+custom_timeout = false
+
+[proxy]
+
+; Sets the proxy url (i.e. "tcp://192.168.0.0:32")
+; "" = Proxy disabled (default)
+url = ""
+
+; Sets the proxy name that is shown on the bridge instead of the proxy url.
+; "" = Show proxy url
+name = "Hidden proxy name"
+
+; Allow users to disable proxy usage for specific requests.
+; true = enabled
+; false = disabled (default)
+by_bridge = false
+
+[authentication]
+
+; Enables authentication for all requests to this RSS-Bridge instance.
+;
+; Warning: You'll have to upgrade existing feeds after enabling this option!
+;
+; true = enabled
+; false = disabled (default)
+enable = false
+
+; The username for authentication. Insert this name when prompted for login.
+username = ""
+
+; The password for authentication. Insert this password when prompted for login.
+; Use a strong password to prevent others from guessing your login!
+password = ""
diff --git a/formats/AtomFormat.php b/formats/AtomFormat.php
index 66f0d2b..9bd08bd 100644
--- a/formats/AtomFormat.php
+++ b/formats/AtomFormat.php
@@ -15,8 +15,11 @@ class AtomFormat extends FormatAbstract{
$extraInfos = $this->getExtraInfos();
$title = $this->xml_encode($extraInfos['name']);
- $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : 'https://github.com/sebsauvage/rss-bridge';
- $icon = $this->xml_encode('http://icons.better-idea.org/icon?url='. $uri .'&size=64');
+ $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : 'https://github.com/RSS-Bridge/rss-bridge';
+
+ $uriparts = parse_url($uri);
+ $icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] .'/favicon.ico');
+
$uri = $this->xml_encode($uri);
$entries = '';
@@ -37,6 +40,16 @@ class AtomFormat extends FormatAbstract{
}
}
+ $entryCategories = '';
+ if(isset($item['categories'])) {
+ foreach($item['categories'] as $category) {
+ $entryCategories .= '<category term="'
+ . $this->xml_encode($category)
+ . '"/>'
+ . PHP_EOL;
+ }
+ }
+
$entries .= <<<EOD
<entry>
@@ -49,6 +62,7 @@ class AtomFormat extends FormatAbstract{
<updated>{$entryTimestamp}</updated>
<content type="html">{$entryContent}</content>
{$entryEnclosures}
+ {$entryCategories}
</entry>
EOD;
diff --git a/formats/HtmlFormat.php b/formats/HtmlFormat.php
index fc4adcc..c4c5012 100644
--- a/formats/HtmlFormat.php
+++ b/formats/HtmlFormat.php
@@ -47,6 +47,20 @@ class HtmlFormat extends FormatAbstract {
$entryEnclosures .= '</div>';
}
+ $entryCategories = '';
+ if(isset($item['categories'])) {
+ $entryCategories = '<div class="categories"><p>Categories:</p>';
+
+ foreach($item['categories'] as $category) {
+
+ $entryCategories .= '<li class="category">'
+ . $this->sanitizeHtml($category)
+ . '</li>';
+ }
+
+ $entryCategories .= '</div>';
+ }
+
$entries .= <<<EOD
<section class="feeditem">
@@ -55,6 +69,7 @@ class HtmlFormat extends FormatAbstract {
{$entryAuthor}
{$entryContent}
{$entryEnclosures}
+ {$entryCategories}
</section>
EOD;
diff --git a/formats/MrssFormat.php b/formats/MrssFormat.php
index 2fcf283..6d07928 100644
--- a/formats/MrssFormat.php
+++ b/formats/MrssFormat.php
@@ -18,10 +18,11 @@ class MrssFormat extends FormatAbstract {
if(!empty($extraInfos['uri'])) {
$uri = $this->xml_encode($extraInfos['uri']);
} else {
- $uri = 'https://github.com/sebsauvage/rss-bridge';
+ $uri = 'https://github.com/RSS-Bridge/rss-bridge';
}
- $icon = $this->xml_encode('http://icons.better-idea.org/icon?url='. $uri .'&size=64');
+ $uriparts = parse_url($uri);
+ $icon = $this->xml_encode($uriparts['scheme'] . '://' . $uriparts['host'] .'/favicon.ico');
$items = '';
foreach($this->getItems() as $item) {
@@ -50,6 +51,16 @@ Some media files might not be shown to you. Consider using the ATOM format inste
}
}
+ $entryCategories = '';
+ if(isset($item['categories'])) {
+
+ foreach($item['categories'] as $category) {
+ $entryCategories .= '<category>'
+ . $category . '</category>'
+ . PHP_EOL;
+ }
+ }
+
$items .= <<<EOD
<item>
@@ -60,6 +71,7 @@ Some media files might not be shown to you. Consider using the ATOM format inste
<description>{$itemContent}{$entryEnclosuresWarning}</description>
<author>{$itemAuthor}</author>
{$entryEnclosures}
+ {$entryCategories}
</item>
EOD;
diff --git a/index.php b/index.php
index 851f0f9..4a1e2aa 100644
--- a/index.php
+++ b/index.php
@@ -1,33 +1,21 @@
<?php
-/*
-TODO :
-- factorize the annotation system
-- factorize to adapter : Format, Bridge, Cache(actually code is almost the same)
-- implement annotation cache for entrance page
-- Cache : I think logic must be change as least to avoid to reconvert object from json in FileCache case.
-- add namespace to avoid futur problem ?
-- see FIXME mentions in the code
-- implement header('X-Cached-Version: '.date(DATE_ATOM, filemtime($cachefile)));
-*/
+require_once __DIR__ . '/lib/RssBridge.php';
-// Defines the minimum required PHP version for RSS-Bridge
define('PHP_VERSION_REQUIRED', '5.6.0');
-//define('PROXY_URL', 'tcp://192.168.0.0:28');
-// Set to true if you allow users to disable proxy usage for specific bridges
-define('PROXY_BYBRIDGE', false);
-// Comment this line or keep PROXY_NAME empty to display PROXY_URL instead
-define('PROXY_NAME', 'Hidden Proxy Name');
-
-date_default_timezone_set('UTC');
-error_reporting(0);
-
// Specify directory for cached files (using FileCache)
define('CACHE_DIR', __DIR__ . '/cache');
// Specify path for whitelist file
define('WHITELIST_FILE', __DIR__ . '/whitelist.txt');
+Configuration::verifyInstallation();
+Configuration::loadConfiguration();
+
+Authentication::showPromptIfNeeded();
+
+date_default_timezone_set('UTC');
+error_reporting(0);
/*
Move the CLI arguments to the $_GET array, in order to be able to use
@@ -57,34 +45,6 @@ if(file_exists('DEBUG')) {
}
}
-require_once __DIR__ . '/lib/RssBridge.php';
-
-// Check PHP version
-if(version_compare(PHP_VERSION, PHP_VERSION_REQUIRED) === -1)
- die('RSS-Bridge requires at least PHP version ' . PHP_VERSION_REQUIRED . '!');
-
-// extensions check
-if(!extension_loaded('openssl'))
- die('"openssl" extension not loaded. Please check "php.ini"');
-
-if(!extension_loaded('libxml'))
- die('"libxml" extension not loaded. Please check "php.ini"');
-
-if(!extension_loaded('mbstring'))
- die('"mbstring" extension not loaded. Please check "php.ini"');
-
-// configuration checks
-if(ini_get('allow_url_fopen') !== "1")
- die('"allow_url_fopen" is not set to "1". Please check "php.ini');
-
-// Check cache folder permissions (write permissions required)
-if(!is_writable(CACHE_DIR))
- die('RSS-Bridge does not have write permissions for ' . CACHE_DIR . '!');
-
-// Check whitelist file permissions (only in DEBUG mode)
-if(!file_exists(WHITELIST_FILE) && !is_writable(dirname(WHITELIST_FILE)))
- die('RSS-Bridge does not have write permissions for ' . WHITELIST_FILE . '!');
-
// FIXME : beta test UA spoofing, please report any blacklisting by PHP-fopen-unfriendly websites
$userAgent = 'Mozilla/5.0(X11; Linux x86_64; rv:30.0)';
@@ -168,6 +128,16 @@ try {
define('NOPROXY', true);
}
+ // Custom cache timeout
+ $cache_timeout = -1;
+ if(array_key_exists('_cache_timeout', $params)) {
+ if(!CUSTOM_CACHE_TIMEOUT) {
+ throw new \HttpException('This server doesn\'t support "_cache_timeout"!');
+ }
+
+ $cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
+ }
+
// Initialize cache
$cache = Cache::create('FileCache');
$cache->setPath(CACHE_DIR);
@@ -178,11 +148,17 @@ try {
unset($params['bridge']);
unset($params['format']);
unset($params['_noproxy']);
+ unset($params['_cache_timeout']);
// Load cache & data
try {
$bridge->setCache($cache);
+ $bridge->setCacheTimeout($cache_timeout);
$bridge->setDatas($params);
+ } catch(Error $e) {
+ http_response_code($e->getCode());
+ header('Content-Type: text/html');
+ die(buildBridgeException($e, $bridge));
} catch(Exception $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
@@ -195,10 +171,14 @@ try {
$format->setItems($bridge->getItems());
$format->setExtraInfos($bridge->getExtraInfos());
$format->display();
- } catch(Exception $e) {
+ } catch(Error $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildTransformException($e, $bridge));
+ } catch(Exception $e) {
+ http_response_code($e->getCode());
+ header('Content-Type: text/html');
+ die(buildBridgeException($e, $bridge));
}
die;
@@ -273,7 +253,8 @@ EOD;
echo $inactiveBridges;
?>
<section class="footer">
- <a href="https://github.com/RSS-Bridge/rss-bridge">RSS-Bridge 2017-08-19 ~ Public Domain</a><br />
+ <a href="https://github.com/RSS-Bridge/rss-bridge">RSS-Bridge ~ Public Domain</a><br />
+ <p class="version"> <?= Configuration::getVersion() ?> </p>
<?= $activeFoundBridgeCount; ?>/<?= count($bridgeList) ?> active bridges. <br />
<?php
if($activeFoundBridgeCount !== count($bridgeList)) {
diff --git a/lib/Authentication.php b/lib/Authentication.php
new file mode 100644
index 0000000..dc75d28
--- /dev/null
+++ b/lib/Authentication.php
@@ -0,0 +1,31 @@
+<?php
+class Authentication {
+
+ public static function showPromptIfNeeded() {
+
+ if(Configuration::getConfig('authentication', 'enable') === true) {
+ if(!Authentication::verifyPrompt()) {
+ header('WWW-Authenticate: Basic realm="RSS-Bridge"');
+ header('HTTP/1.0 401 Unauthorized');
+ die('Please authenticate in order to access this instance !');
+ }
+
+ }
+
+ }
+
+ 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 cd2ff63..0bd1c7c 100644
--- a/lib/BridgeAbstract.php
+++ b/lib/BridgeAbstract.php
@@ -14,6 +14,7 @@ abstract class BridgeAbstract implements BridgeInterface {
protected $items = array();
protected $inputs = array();
protected $queriedContext = '';
+ protected $cacheTimeout;
/**
* Return cachable datas (extrainfos and items) stored in the bridge
@@ -171,7 +172,7 @@ abstract class BridgeAbstract implements BridgeInterface {
if(!is_null($this->cache)) {
$time = $this->cache->getTime();
if($time !== false
- && (time() - static::CACHE_TIMEOUT < $time)
+ && (time() - $this->getCacheTimeout() < $time)
&& (!defined('DEBUG') || DEBUG !== true)) {
$cached = $this->cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
@@ -268,4 +269,17 @@ abstract class BridgeAbstract implements BridgeInterface {
public function setCache(\CacheInterface $cache){
$this->cache = $cache;
}
+
+ public function setCacheTimeout($timeout){
+ if(is_numeric($timeout) && ($timeout < 1 || $timeout > 86400)) {
+ $this->cacheTimeout = static::CACHE_TIMEOUT;
+ return;
+ }
+
+ $this->cacheTimeout = $timeout;
+ }
+
+ public function getCacheTimeout(){
+ return isset($this->cacheTimeout) ? $this->cacheTimeout : static::CACHE_TIMEOUT;
+ }
}
diff --git a/lib/BridgeInterface.php b/lib/BridgeInterface.php
index a4add2c..b8f5cf4 100644
--- a/lib/BridgeInterface.php
+++ b/lib/BridgeInterface.php
@@ -68,4 +68,20 @@ interface BridgeInterface {
* @param object CacheInterface The cache instance
*/
public function setCache(\CacheInterface $cache);
+
+ /**
+ * Sets the timeout for clearing the cache files. The timeout must be
+ * specified between 1..86400 seconds (max. 24 hours). The default timeout
+ * (specified by the bridge maintainer) applies for invalid values.
+ *
+ * @param int $timeout The cache timeout in seconds
+ */
+ public function setCacheTimeout($timeout);
+
+ /**
+ * Returns the cache timeout
+ *
+ * @return int Cache timeout
+ */
+ public function getCacheTimeout();
}
diff --git a/lib/Configuration.php b/lib/Configuration.php
new file mode 100644
index 0000000..620b0e4
--- /dev/null
+++ b/lib/Configuration.php
@@ -0,0 +1,120 @@
+<?php
+class Configuration {
+
+ public static $VERSION = '2018-07-17';
+
+ public static $config = null;
+
+ public static function verifyInstallation() {
+
+ // Check PHP version
+ if(version_compare(PHP_VERSION, PHP_VERSION_REQUIRED) === -1)
+ die('RSS-Bridge requires at least PHP version ' . PHP_VERSION_REQUIRED . '!');
+
+ // extensions check
+ if(!extension_loaded('openssl'))
+ die('"openssl" extension not loaded. Please check "php.ini"');
+
+ if(!extension_loaded('libxml'))
+ die('"libxml" extension not loaded. Please check "php.ini"');
+
+ if(!extension_loaded('mbstring'))
+ die('"mbstring" extension not loaded. Please check "php.ini"');
+
+ if(!extension_loaded('simplexml'))
+ die('"simplexml" extension not loaded. Please check "php.ini"');
+
+ if(!extension_loaded('curl'))
+ die('"curl" extension not loaded. Please check "php.ini"');
+
+ // Check cache folder permissions (write permissions required)
+ if(!is_writable(CACHE_DIR))
+ die('RSS-Bridge does not have write permissions for ' . CACHE_DIR . '!');
+
+ // Check whitelist file permissions (only in DEBUG mode)
+ if(!file_exists(WHITELIST_FILE) && !is_writable(dirname(WHITELIST_FILE)))
+ die('RSS-Bridge does not have write permissions for ' . WHITELIST_FILE . '!');
+
+ }
+
+ public static function loadConfiguration() {
+
+ if(!file_exists('config.default.ini.php'))
+ die('The default configuration file "config.default.ini.php" is missing!');
+
+ Configuration::$config = parse_ini_file('config.default.ini.php', true, INI_SCANNER_TYPED);
+ if(!Configuration::$config)
+ die('Error parsing config.default.ini.php');
+
+ if(file_exists('config.ini.php')) {
+ // Replace default configuration with custom settings
+ foreach(parse_ini_file('config.ini.php', true, INI_SCANNER_TYPED) as $header => $section) {
+ foreach($section as $key => $value) {
+ // Skip unknown sections and keys
+ if(array_key_exists($header, Configuration::$config) && array_key_exists($key, Configuration::$config[$header])) {
+ Configuration::$config[$header][$key] = $value;
+ }
+ }
+ }
+ }
+
+ if(!is_string(self::getConfig('proxy', 'url')))
+ die('Parameter [proxy] => "url" is not a valid string! Please check "config.ini.php"!');
+
+ if(!empty(self::getConfig('proxy', 'url')))
+ define('PROXY_URL', self::getConfig('proxy', 'url'));
+
+ if(!is_bool(self::getConfig('proxy', 'by_bridge')))
+ die('Parameter [proxy] => "by_bridge" is not a valid Boolean! Please check "config.ini.php"!');
+
+ define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge'));
+
+ if(!is_string(self::getConfig('proxy', 'name')))
+ die('Parameter [proxy] => "name" is not a valid string! Please check "config.ini.php"!');
+
+ define('PROXY_NAME', self::getConfig('proxy', 'name'));
+
+ if(!is_bool(self::getConfig('cache', 'custom_timeout')))
+ die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');
+
+ define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout'));
+
+ if(!is_bool(self::getConfig('authentication', 'enable')))
+ die('Parameter [authentication] => "enable" is not a valid Boolean! Please check "config.ini.php"!');
+
+ if(!is_string(self::getConfig('authentication', 'username')))
+ die('Parameter [authentication] => "username" is not a valid string! Please check "config.ini.php"!');
+
+ if(!is_string(self::getConfig('authentication', 'password')))
+ die('Parameter [authentication] => "password" is not a valid string! Please check "config.ini.php"!');
+
+ }
+
+ public static function getConfig($category, $key) {
+
+ if(array_key_exists($category, self::$config) && array_key_exists($key, self::$config[$category])) {
+ return self::$config[$category][$key];
+ }
+
+ return null;
+
+ }
+
+ public static function getVersion() {
+
+ $headFile = '.git/HEAD';
+
+ if(file_exists($headFile)) {
+
+ $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1);
+ $branchName = explode('/', $revisionHashFile)[3];
+ if(file_exists($revisionHashFile)) {
+ return 'git.' . $branchName . '.' . substr(file_get_contents($revisionHashFile), 0, 7);
+ }
+ }
+
+ return Configuration::$VERSION;
+
+ }
+
+}
diff --git a/lib/Exceptions.php b/lib/Exceptions.php
index 252d1e3..9b89320 100644
--- a/lib/Exceptions.php
+++ b/lib/Exceptions.php
@@ -54,7 +54,7 @@ function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null
* provided parameter are invalid
*/
function buildBridgeException($e, $bridge){
- if(!($e instanceof \Exception) || !($bridge instanceof \BridgeInterface)) {
+ if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
return null;
}
@@ -64,7 +64,10 @@ function buildBridgeException($e, $bridge){
$body = 'Error message: `'
. $e->getMessage()
. "`\nQuery string: `"
- . $_SERVER['QUERY_STRING'] . '`';
+ . $_SERVER['QUERY_STRING']
+ . "`\nVersion: `"
+ . Configuration::getVersion()
+ . '`';
$link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
@@ -85,7 +88,7 @@ unable to receive or process the remote website's content!";
* provided parameter are invalid
*/
function buildTransformException($e, $bridge){
- if(!($e instanceof \Exception) || !($bridge instanceof \BridgeInterface)) {
+ if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
return null;
}
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
index 9702ce3..6e1f16f 100644
--- a/lib/FeedExpander.php
+++ b/lib/FeedExpander.php
@@ -24,15 +24,15 @@ abstract class FeedExpander extends BridgeAbstract {
switch(true) {
case isset($rssContent->item[0]):
debugMessage('Detected RSS 1.0 format');
- $this->feedType = "RSS_1_0";
+ $this->feedType = 'RSS_1_0';
break;
case isset($rssContent->channel[0]):
debugMessage('Detected RSS 0.9x or 2.0 format');
- $this->feedType = "RSS_2_0";
+ $this->feedType = 'RSS_2_0';
break;
case isset($rssContent->entry[0]):
debugMessage('Detected ATOM format');
- $this->feedType = "ATOM_1_0";
+ $this->feedType = 'ATOM_1_0';
break;
default:
debugMessage('Unknown feed format/version');
diff --git a/lib/RssBridge.php b/lib/RssBridge.php
index e2f9be3..8d0ef90 100644
--- a/lib/RssBridge.php
+++ b/lib/RssBridge.php
@@ -14,6 +14,8 @@ require __DIR__ . '/Bridge.php';
require __DIR__ . '/BridgeAbstract.php';
require __DIR__ . '/FeedExpander.php';
require __DIR__ . '/Cache.php';
+require __DIR__ . '/Authentication.php';
+require __DIR__ . '/Configuration.php';
require __DIR__ . '/validation.php';
require __DIR__ . '/html.php';
diff --git a/lib/contents.php b/lib/contents.php
index ec62c8d..416fb7d 100644
--- a/lib/contents.php
+++ b/lib/contents.php
@@ -1,77 +1,47 @@
<?php
-function getContents($url,
-$use_include_path = false,
-$context = null,
-$offset = 0,
-$maxlen = null){
- $contextOptions = array(
- 'http' => array(
- 'user_agent' => ini_get('user_agent'),
- 'accept_encoding' => 'gzip'
- )
- );
+function getContents($url, $header = array(), $opts = array()){
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
- if(defined('PROXY_URL') && !defined('NOPROXY')) {
- $contextOptions['http']['proxy'] = PROXY_URL;
- $contextOptions['http']['request_fulluri'] = true;
+ if(is_array($header) && count($header) !== 0)
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
+
+ curl_setopt($ch, CURLOPT_USERAGENT, ini_get('user_agent'));
+ curl_setopt($ch, CURLOPT_ENCODING, '');
+ curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
- if(is_null($context)) {
- $context = stream_context_create($contextOptions);
- } else {
- $prevContext = $context;
- if(!stream_context_set_option($context, $contextOptions)) {
- $context = $prevContext;
- }
+ if(is_array($opts)) {
+ foreach($opts as $key => $value) {
+ curl_setopt($ch, $key, $value);
}
}
- if(is_null($maxlen)) {
- $content = file_get_contents($url, $use_include_path, $context, $offset);
- } else {
- $content = file_get_contents($url, $use_include_path, $context, $offset, $maxlen);
+ if(defined('PROXY_URL') && !defined('NOPROXY')) {
+ curl_setopt($ch, CURLOPT_PROXY, PROXY_URL);
}
- if($content === false)
- debugMessage('Cant\'t download ' . $url);
+ $content = curl_exec($ch);
+ $curlError = curl_error($ch);
+ $curlErrno = curl_errno($ch);
+ curl_close($ch);
- // handle compressed data
- foreach($http_response_header as $header) {
- if(stristr($header, 'content-encoding')) {
- switch(true) {
- case stristr($header, 'gzip'):
- $content = gzinflate(substr($content, 10, -8));
- break;
- case stristr($header, 'compress'):
- //TODO
- case stristr($header, 'deflate'):
- //TODO
- case stristr($header, 'brotli'):
- //TODO
- returnServerError($header . '=> Not implemented yet');
- break;
- case stristr($header, 'identity'):
- break;
- default:
- returnServerError($header . '=> Unknown compression');
- }
- }
- }
+ if($content === false)
+ debugMessage('Cant\'t download ' . $url . ' cUrl error: ' . $curlError . ' (' . $curlErrno . ')');
return $content;
}
function getSimpleHTMLDOM($url,
-$use_include_path = false,
-$context = null,
-$offset = 0,
-$maxLen = null,
+$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, $use_include_path, $context, $offset, $maxLen);
+ $content = getContents($url, $header, $opts);
return str_get_html($content,
$lowercase,
$forceTagsClosed,
@@ -89,10 +59,8 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
*/
function getSimpleHTMLDOMCached($url,
$duration = 86400,
-$use_include_path = false,
-$context = null,
-$offset = 0,
-$maxLen = null,
+$header = array(),
+$opts = array(),
$lowercase = true,
$forceTagsClosed = true,
$target_charset = DEFAULT_TARGET_CHARSET,
@@ -116,7 +84,7 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
&& (!defined('DEBUG') || DEBUG !== true)) { // Contents within duration
$content = $cache->loadData();
} else { // Content not within duration
- $content = getContents($url, $use_include_path, $context, $offset, $maxLen);
+ $content = getContents($url, $header, $opts);
if($content !== false) {
$cache->saveData($content);
}
diff --git a/lib/html.php b/lib/html.php
index 4c6c320..297ab80 100644
--- a/lib/html.php
+++ b/lib/html.php
@@ -26,7 +26,7 @@ EOD;
$bridge = Bridge::create($bridgeName);
if($bridge == false)
- return "";
+ return '';
$HTTPSWarning = '';
if(strpos($bridge->getURI(), 'https') !== 0) {
@@ -75,8 +75,24 @@ CARD;
. ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL)
. ')</label><br />'
. PHP_EOL;
- }
+ } if(CUSTOM_CACHE_TIMEOUT) {
+ $idArg = 'arg-'
+ . urlencode($bridgeName)
+ . '-'
+ . urlencode('_cache_timeout');
+ $card .= '<label for="'
+ . $idArg
+ . '">Cache timeout in seconds : </label>'
+ . PHP_EOL;
+
+ $card .= '<input id="'
+ . $idArg
+ . '" type="number" value="'
+ . $bridge->getCacheTimeout()
+ . '" name="_cache_timeout" /><br />'
+ . PHP_EOL;
+ }
$card .= $getHelperButtonsFormat($formats);
} else {
$card .= '<span style="font-weight: bold;">Inactive</span>';
@@ -251,6 +267,23 @@ CARD;
. ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL)
. ')</label><br />'
. PHP_EOL;
+ } if(CUSTOM_CACHE_TIMEOUT) {
+ $idArg = 'arg-'
+ . urlencode($bridgeName)
+ . '-'
+ . urlencode('_cache_timeout');
+
+ $card .= '<label for="'
+ . $idArg
+ . '">Cache timeout in seconds : </label>'
+ . PHP_EOL;
+
+ $card .= '<input id="'
+ . $idArg
+ . '" type="number" value="'
+ . $bridge->getCacheTimeout()
+ . '" name="_cache_timeout" /><br />'
+ . PHP_EOL;
}
$card .= $getHelperButtonsFormat($formats);
} else {
diff --git a/phpcs.xml b/phpcs.xml
index c88b0f5..a67262c 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -70,4 +70,8 @@
<rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
<!-- Do not add whitespace at start or end of a file or end of a line -->
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
+ <!-- Whenever possible use single quote strings -->
+ <rule ref="Squiz.Strings.DoubleQuoteUsage">
+ <exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar" />
+ </rule>
</ruleset>
diff --git a/static/HtmlFormat.css b/static/HtmlFormat.css
index b7e0d0e..195a9b0 100644
--- a/static/HtmlFormat.css
+++ b/static/HtmlFormat.css
@@ -5,7 +5,6 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
border: 0;
outline: 0;
font-size: 100%;
- font: inherit;
vertical-align: baseline;
}
@@ -70,12 +69,14 @@ a.backlink, a.backlink:link, a.backlink:visited, a.itemtitle, a.itemtitle:link,
}
+section > div.content, section > div.categories,
section > div.content, section > div.attachments {
padding: 10px;
}
+section > div.categories > li.category,
section > div.attachments > li.enclosure {
list-style-type: circle;
@@ -111,3 +112,8 @@ button.backbutton, button.rss-feed {
}
+img {
+
+ max-width: 100%;
+
+}
diff --git a/static/style.css b/static/style.css
index c7a278d..ac2f469 100644
--- a/static/style.css
+++ b/static/style.css
@@ -143,6 +143,11 @@ section.footer:hover {
}
+section.footer .version {
+
+ font-size: 80%;
+
+}
section > h2 {