1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-17 22:02:09 +02:00

Compare commits

..

44 Commits

Author SHA1 Message Date
Lyra
b4f393a5cc [Configuration] Bump version to 2019-07-06 2019-09-12 17:08:15 +02:00
Lyra
29126ebe29 [README] Update list of contributors 2019-09-12 17:07:04 +02:00
triatic
50c971d545 [TwitterBridge] Enable cookies with curl (#1245)
* [TwitterBridge] Enable cookies with curl

Enable cookies in curl, or fall back to `file_get_contents` if in CLI mode with no curl root certificates.
2019-09-12 16:14:48 +02:00
Lyra
7aba7992aa [InstagramBridge] Remove condition that forces cache ignoring 2019-09-11 19:28:46 +02:00
Lyra
48ebed7b38 [InstagramBridge] Fix Instagram stories and user id finding. 2019-09-11 19:08:12 +02:00
Lyra
ccef6b95ad [InstagramBridge] Attempt to fix the queries in order to bypass rate limits 2019-09-10 14:37:50 +02:00
Antoine Turmel
dd5da99a30 [StoriesIGBridge] New bridge (#1187)
* Create StoriesIGBridge.php
2019-09-07 18:43:06 +02:00
Joseph
2ff27b92ff [DailymotionBridge] Use API for playlist and user account feeds (#1217) 2019-09-07 18:42:45 +02:00
Joseph
b47189921f [CuriousCatBridge] Add new bridge (#1216)
* Create CuriousCatBridge.php
2019-09-07 18:37:30 +02:00
floviolleau
f1d3e8c9c9 [AtmoNouvelleAquitaineBridge] Add new bridge for air quality in Bordeaux (#1229)
* Add new bridge for air quality in Bordeaux
2019-09-07 18:36:55 +02:00
triatic
53fbd2a5a0 [FacebookBridge] Prevent sending empty header (#1239)
* [FacebookBridge] Prevent sending empty header

When running in CLI mode, `getEnv('HTTP_ACCEPT_LANGUAGE')` returns `false`. In that case, don't send the `Accept-Language` header.
2019-09-07 18:32:06 +02:00
ORelio
3254a4d7bc [WIRED] Add WIRED Bridge (#1244)
* [WIRED] Add WIRED Bridge
2019-09-07 18:31:19 +02:00
Roliga
52d2d21da5 [TwitchBridge] Add new bridge (#1253)
* [TwitchBridge] Add new bridge
2019-09-07 18:27:44 +02:00
Roliga
abb74f056c [PatreonBridge] Add new bridge (#1254)
* [PatreonBridge] Add new bridge

* [PatreonBridge] Add UID to articles

Patreon changes post URLs when the post title is updated, so set a UID
based on the post ID instead.
2019-09-07 18:26:58 +02:00
dawidsowa
25548b6757 [Rule34pahealBridge] Fix thumbnail uri (#1278) 2019-09-07 18:26:08 +02:00
sysadminstory
cfe433e9e2 [AutoJMBridge] Fix the bridge to follow website changes (#1255)
The Website changed in two way :
- The filter about availability disappeared (and this leads to a
  parameters change, which will break existing bridges, sorry)
- Some HTML change
2019-09-06 10:52:58 +02:00
Nicolas Delsaux
0dfc4ea2c5 [GQMagazineBridge] Adapt to changes, fixes #1280 2019-09-06 10:51:13 +02:00
Lyra
38960df180 [ThePirateBayBridge] Fix PHPCS code violations 2019-09-06 10:55:15 +02:00
Eugene Molotov
b440a6fdc6 [PikabuBridge] Added filtering by user (#1266) 2019-08-28 16:29:49 +02:00
somini
48d0385653 [core] Fix double XML encoding on Atom feed title (#1247) 2019-08-28 16:29:13 +02:00
Roliga
b68c0e0df8 [PirateCommunityBridge] Add new bridge (#1252)
* [PirateCommunityBridge] Add new bridge
2019-08-28 16:28:39 +02:00
Anchit Bajaj
f27b267614 [GuardianBridge] - New bridge for the Guardian (#1249)
* [GuardianBridge] - New bridge for the Guardian
2019-08-28 16:27:45 +02:00
Mitsu
8bff63d9c6 [ThePirateBay] URI fix, add magnet link 2019-08-27 01:18:43 +02:00
Mitsu
2b4a030158 [ThePirateBay] switch back TLD to .org
And the "whack-a-mole" game continues
2019-08-27 00:55:36 +02:00
Rudolf M. Schreier
6a99904e64 [DanbooruBridge] Decode href of HTML element to avoid double escaping. (#1262)
Directly accessing ...->href resulted in a string that contained '&'
instead of '&'. This was later escaped again to '&' in some
formats (e.g. Atom).
2019-08-26 14:26:19 +02:00
sysadminstory
f3c687604f [DealabsBridge] Follow website change (#1256)
A minor website change broke the Bridge. This commit fix it
2019-08-26 14:25:47 +02:00
Lyra
a86a94555d [LeBonCoinBridge] Submit user agent to LBC to get results. 2019-08-26 14:22:58 +02:00
Anchit Bajaj
acc0787b00 [IGNBridge] - New bridge for IGN (#1233)
* [IGNBridge]: New Bridge for IGN
2019-07-31 14:26:43 +02:00
johnnygroovy
c8992650a1 [DavesTrailerPageBridge] Add new bridge (#1246) 2019-07-31 14:17:34 +02:00
Anchit Bajaj
f9f511a849 [NYTBridge] : New bridge for the new york times (#1235) 2019-07-29 12:15:08 +02:00
somini
990719d614 [FabriceBellard]: New Bridge (#1220)
* [FabriceBellard]: New Bridge
2019-07-29 12:12:55 +02:00
triatic
b6be18d585 [contents] Respect passed headers for file_get_contents() (#1234)
* [contents] Respect passed headers for file_get_contents()
2019-07-29 12:05:13 +02:00
Roliga
cf525c964a [WIP][FurAffinityBridge] Add new bridge (#1083)
* [FurAffinityBridge] Add new bridge
2019-07-26 11:02:58 +02:00
Antoine Cadoret
52a4f0860c [LaCentraleBridge] Add new bridge (#1201)
* [LaCentraleBridge] Introduce new bridge
2019-07-26 11:00:55 +02:00
triatic
21b27a1042 [FacebookBridge] Remove relative date from content (#1212)
Remove relative date from content, as well as the separator after it.

As mentioned in #1188.
2019-07-26 10:56:34 +02:00
Léo Maradan
2eee535171 CNET France Bridge (#1214)
CNET France News but with filters on title or url
2019-07-26 10:53:09 +02:00
Anchit Bajaj
da51fc065f [EngadgetBridge] New bridge for Engadget (#1215)
* [EngadgetBridge] New bridge for Engadget
2019-07-26 10:51:20 +02:00
Joseph
e032705c9a [HaveIBeenPwnedBridge] Add item limit parameter, set default limit to 20 (#1219)
* Add `item_limit` parameter to allow user to control number of item returned by bridge. Suggested by @triatic and @somini (code).
2019-07-26 10:47:20 +02:00
Joseph
be27bc9250 Fix malformed URLs (#1222)
Removes 'self::URI' from processUpload() which was creating malformed URLs. Relative URLs are handled by defaultLinkTo() making 'self::URI' unnecessary.
2019-07-26 10:43:18 +02:00
Albirew
75edc1b2b7 [NovelUpdatesBridge] now in https (#1228) 2019-07-26 10:42:41 +02:00
Albirew
c9ea53806d [HentaiHavenBridge] now in https (#1227) 2019-07-26 10:42:19 +02:00
triatic
2bb9480555 [TwitterBridge] Get cookies before sending request (#1232)
* [TwitterBridge] Get cookies before sending request

Twitter now requires cookies to be set before requesting a page. This will fetch the cookies and send them to `getSimpleHTMLDOM()`.

* Formatting fixes
2019-07-26 10:36:59 +02:00
Corentin Garcia
eb942bc498 [UnsplashBridge] Fix bridge (fix issue #965) (#1208) 2019-07-16 16:50:14 +02:00
logmanoriginal
5a0ea423c4 [Configuration] Bump version to dev.2019-07-06 2019-07-06 12:35:36 +02:00
37 changed files with 7450 additions and 110 deletions

View File

@@ -133,6 +133,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [couraudt](https://github.com/couraudt)
* [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei)
* [dawidsowa](https://github.com/dawidsowa)
* [disk0x](https://github.com/disk0x)
* [DJCrashdummy](https://github.com/DJCrashdummy)
* [Djuuu](https://github.com/Djuuu)
@@ -142,6 +143,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [floviolleau](https://github.com/floviolleau)
* [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
@@ -152,11 +154,13 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [Grummfy](https://github.com/Grummfy)
* [hunhejj](https://github.com/hunhejj)
* [husim0](https://github.com/husim0)
* [IceWreck](https://github.com/IceWreck)
* [j0k3r](https://github.com/j0k3r)
* [JackNUMBER](https://github.com/JackNUMBER)
* [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z)
* [johnnygroovy](https://github.com/johnnygroovy)
* [killruana](https://github.com/killruana)
* [klimplant](https://github.com/klimplant)
* [kranack](https://github.com/kranack)
@@ -165,6 +169,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [laBecasse](https://github.com/laBecasse)
* [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev)
* [Leomaradan](https://github.com/Leomaradan)
* [ldidry](https://github.com/ldidry)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
@@ -207,6 +212,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene)
* [thefranke](https://github.com/thefranke)
* [ThePadawan](https://github.com/ThePadawan)
* [TheRadialActive](https://github.com/TheRadialActive)
* [triatic](https://github.com/triatic)
* [VerifiedJoseph](https://github.com/VerifiedJoseph)

File diff suppressed because it is too large Load Diff

View File

@@ -15,16 +15,6 @@ class AutoJMBridge extends BridgeAbstract {
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
'exampleValue' => 'achat-voitures-neuves-peugeot-nouvelle-308-5p'
),
'isDispo' => array(
'name' => 'Disponibilité',
'type' => 'list',
'values' => array(
'-' => '',
'En stock' => 1,
'Sur commande' => 0
),
'title' => 'Critère de disponibilité'
),
'energy' => array(
'name' => 'Carburant',
'type' => 'list',
@@ -92,7 +82,6 @@ class AutoJMBridge extends BridgeAbstract {
// Build the form
$post_data = array(
'form[isDispo]' => $this->getInput('isDispo'),
'form[energy]' => $this->getInput('energy'),
'form[transmission]' => $this->getInput('transmission'),
'form[priceMin]' => $this->getInput('priceMin'),
@@ -121,7 +110,7 @@ class AutoJMBridge extends BridgeAbstract {
$html = str_get_html($data->content);
// Go through every finisha of the model
$list = $html->find('h2');
$list = $html->find('h3');
foreach ($list as $finish) {
$finish_name = $finish->plaintext;
$motorizations = $finish->next_sibling()->find('li');

View File

@@ -0,0 +1,63 @@
<?php
class CNETFranceBridge extends FeedExpander
{
const MAINTAINER = 'leomaradan';
const NAME = 'CNET France';
const URI = 'https://www.cnetfrance.fr/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'CNET France RSS with filters';
const PARAMETERS = array(
'filters' => array(
'title' => array(
'name' => 'Exclude by title',
'required' => false,
'title' => 'Title term, separated by semicolon (;)',
'defaultValue' => 'bon plan;bons plans;au meilleur prix;des meilleures offres;Amazon Prime Day;RED by SFR ou B&You'
),
'url' => array(
'name' => 'Exclude by url',
'required' => false,
'title' => 'URL term, separated by semicolon (;)',
'defaultValue' => 'bon-plan;bons-plans'
)
)
);
private $bannedTitle = [];
private $bannedURL = [];
public function collectData()
{
$title = $this->getInput('title');
$url = $this->getInput('url');
if ($title !== null) {
$this->bannedTitle = explode(';', $title);
}
if ($url !== null) {
$this->bannedURL = explode(';', $url);
}
$this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/');
}
protected function parseItem($feedItem)
{
$item = parent::parseItem($feedItem);
foreach ($this->bannedTitle as $term) {
if (preg_match('/' . $term . '/mi', $item['title']) === 1) {
return null;
}
}
foreach ($this->bannedURL as $term) {
if (preg_match('/' . $term . '/mi', $item['uri']) === 1) {
return null;
}
}
return $item;
}
}

View File

@@ -0,0 +1,109 @@
<?php
class CuriousCatBridge extends BridgeAbstract {
const NAME = 'Curious Cat Bridge';
const URI = 'https://curiouscat.me';
const DESCRIPTION = 'Returns list of newest questions and answers for a user profile';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'username' => array(
'name' => 'Username',
'type' => 'text',
'required' => true,
'exampleValue' => 'koethekoethe',
)
));
const CACHE_TIMEOUT = 3600;
public function collectData() {
$url = self::URI . '/api/v2/profile?username=' . urlencode($this->getInput('username'));
$apiJson = getContents($url)
or returnServerError('Could not request: ' . $url);
$apiData = json_decode($apiJson, true);
foreach($apiData['posts'] as $post) {
$item = array();
$item['author'] = 'Anonymous';
if ($post['senderData']['id'] !== false) {
$item['author'] = $post['senderData']['username'];
}
$item['uri'] = $this->getURI() . '/post/' . $post['id'];
$item['title'] = $this->ellipsisTitle($post['comment']);
$item['content'] = $this->processContent($post);
$item['timestamp'] = $post['timestamp'];
$this->items[] = $item;
}
}
public function getURI() {
if (!is_null($this->getInput('username'))) {
return self::URI . '/' . $this->getInput('username');
}
return parent::getURI();
}
public function getName() {
if (!is_null($this->getInput('username'))) {
return $this->getInput('username') . ' - Curious Cat';
}
return parent::getName();
}
private function processContent($post) {
$author = 'Anonymous';
if ($post['senderData']['id'] !== false) {
$authorUrl = self::URI . '/' . $post['senderData']['username'];
$author = <<<EOD
<a href="{$authorUrl}">{$post['senderData']['username']}</a>
EOD;
}
$question = $this->formatUrls($post['comment']);
$answer = $this->formatUrls($post['reply']);
$content = <<<EOD
<p>{$author} asked:</p>
<blockquote>{$question}</blockquote><br/>
<p>{$post['addresseeData']['username']} answered:</p>
<blockquote>{$answer}</blockquote>
EOD;
return $content;
}
private function ellipsisTitle($text) {
$length = 150;
if (strlen($text) > $length) {
$text = explode('<br>', wordwrap($text, $length, '<br>'));
return $text[0] . '...';
}
return $text;
}
private function formatUrls($content) {
return preg_replace(
'/(http[s]{0,1}\:\/\/[a-zA-Z0-9.\/\?\&=\-_]{4,})/ims',
'<a target="_blank" href="$1" target="_blank">$1</a> ',
$content
);
}
}

View File

@@ -4,7 +4,7 @@ class DailymotionBridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Dailymotion Bridge';
const URI = 'https://www.dailymotion.com/';
const CACHE_TIMEOUT = 10800; // 3h
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns the 5 newest videos by username/playlist or search';
const PARAMETERS = array (
@@ -27,74 +27,99 @@ class DailymotionBridge extends BridgeAbstract {
),
'pa' => array(
'name' => 'Page',
'type' => 'number'
'type' => 'number',
'defaultValue' => 1,
)
)
);
protected function getMetadata($id){
$metadata = array();
$html2 = getSimpleHTMLDOM(self::URI . 'video/' . $id);
if(!$html2) {
return $metadata;
}
private $feedName = '';
$metadata['title'] = $html2->find('meta[property=og:title]', 0)->getAttribute('content');
$metadata['timestamp'] = strtotime(
$html2->find('meta[property=video:release_date]', 0)->getAttribute('content')
);
$metadata['thumbnailUri'] = $html2->find('meta[property=og:image]', 0)->getAttribute('content');
$metadata['uri'] = $html2->find('meta[property=og:url]', 0)->getAttribute('content');
return $metadata;
}
private $apiUrl = 'https://api.dailymotion.com';
private $apiFields = 'created_time,description,id,owner.screenname,tags,thumbnail_url,title,url';
public function getIcon() {
return 'https://static1-ssl.dmcdn.net/images/neon/favicons/android-icon-36x36.png.vf806ca4ed0deed812';
}
public function collectData(){
$html = '';
$limit = 5;
$count = 0;
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request Dailymotion.');
if ($this->queriedContext === 'By username' || $this->queriedContext === 'By playlist id') {
foreach($html->find('div.media a.preview_link') as $element) {
if($count < $limit) {
$apiJson = getContents($this->getApiUrl())
or returnServerError('Could not request: ' . $this->getApiUrl());
$apiData = json_decode($apiJson, true);
$this->feedName = $this->getPlaylistTitle($this->getInput('p'));
foreach ($apiData['list'] as $apiItem) {
$item = array();
$item['uri'] = $apiItem['url'];
$item['uid'] = $apiItem['id'];
$item['title'] = $apiItem['title'];
$item['timestamp'] = $apiItem['created_time'];
$item['author'] = $apiItem['owner.screenname'];
$item['content'] = '<p><a href="' . $apiItem['url'] . '">
<img src="' . $apiItem['thumbnail_url'] . '"></a></p><p>' . $apiItem['description'] . '</p>';
$item['categories'] = $apiItem['tags'];
$item['enclosures'][] = $apiItem['thumbnail_url'];
$this->items[] = $item;
}
}
if ($this->queriedContext === 'From search results') {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request Dailymotion.');
foreach($html->find('div.media a.preview_link') as $element) {
$item = array();
$item['id'] = str_replace('/video/', '', strtok($element->href, '_'));
$metadata = $this->getMetadata($item['id']);
if(empty($metadata)) {
continue;
}
$item['uri'] = $metadata['uri'];
$item['title'] = $metadata['title'];
$item['timestamp'] = $metadata['timestamp'];
$item['content'] = '<a href="'
. $item['uri']
. '"><img src="'
. $metadata['thumbnailUri']
. '" /></a><br><a href="'
. $item['uri']
. '">'
. $item['title']
. '</a>';
. $item['uri']
. '"><img src="'
. $metadata['thumbnailUri']
. '" /></a><br><a href="'
. $item['uri']
. '">'
. $item['title']
. '</a>';
$this->items[] = $item;
$count++;
if (count($this->items) >= 5) {
break;
}
}
}
}
public function getName(){
public function getName() {
switch($this->queriedContext) {
case 'By username':
$specific = $this->getInput('u');
break;
case 'By playlist id':
$specific = strtok($this->getInput('p'), '_');
if ($this->feedName) {
$specific = $this->feedName;
}
break;
case 'From search results':
$specific = $this->getInput('s');
@@ -102,26 +127,77 @@ class DailymotionBridge extends BridgeAbstract {
default: return parent::getName();
}
return $specific . ' : Dailymotion Bridge';
return $specific . ' : Dailymotion';
}
public function getURI(){
$uri = self::URI;
switch($this->queriedContext) {
case 'By username':
$uri .= 'user/' . urlencode($this->getInput('u')) . '/1';
$uri .= 'user/' . urlencode($this->getInput('u'));
break;
case 'By playlist id':
$uri .= 'playlist/' . urlencode(strtok($this->getInput('p'), '_'));
break;
case 'From search results':
$uri .= 'search/' . urlencode($this->getInput('s'));
if($this->getInput('pa')) {
$uri .= '/' . $this->getInput('pa');
if(!is_null($this->getInput('pa'))) {
$pa = $this->getInput('pa');
if ($this->getInput('pa') < 1) {
$pa = 1;
}
$uri .= '/' . $pa;
}
break;
default: return parent::getURI();
}
return $uri;
}
private function getMetadata($id) {
$metadata = array();
$html = getSimpleHTMLDOM(self::URI . 'video/' . $id);
if(!$html) {
return $metadata;
}
$metadata['title'] = $html->find('meta[property=og:title]', 0)->getAttribute('content');
$metadata['timestamp'] = strtotime(
$html->find('meta[property=video:release_date]', 0)->getAttribute('content')
);
$metadata['thumbnailUri'] = $html->find('meta[property=og:image]', 0)->getAttribute('content');
$metadata['uri'] = $html->find('meta[property=og:url]', 0)->getAttribute('content');
return $metadata;
}
private function getPlaylistTitle($id) {
$title = '';
$url = self::URI . 'playlist/' . $id;
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request: ' . $url);
$title = $html->find('meta[property=og:title]', 0)->getAttribute('content');
return $title;
}
private function getApiUrl() {
switch($this->queriedContext) {
case 'By username':
return $this->apiUrl . '/user/' . $this->getInput('u')
. '/videos?fields=' . urlencode($this->apiFields) . '&availability=1&sort=recent&limit=5';
break;
case 'By playlist id':
return $this->apiUrl . '/playlist/' . $this->getInput('p')
. '/videos?fields=' . urlencode($this->apiFields) . '&limit=5';
break;
}
}
}

View File

@@ -40,7 +40,7 @@ class DanbooruBridge extends BridgeAbstract {
defaultLinkTo($element, $this->getURI());
$item = array();
$item['uri'] = $element->find('a', 0)->href;
$item['uri'] = html_entity_decode($element->find('a', 0)->href);
$item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src;

View File

@@ -0,0 +1,27 @@
<?php
class DavesTrailerPageBridge extends BridgeAbstract {
const MAINTAINER = 'johnnygroovy';
const NAME = 'Daves Trailer Page Bridge';
const URI = 'https://www.davestrailerpage.co.uk/';
const DESCRIPTION = 'Last trailers in HD thanks to Dave.';
public function collectData(){
$html = getSimpleHTMLDOM(static::URI)
or returnClientError('No results for this query.');
foreach ($html->find('tr[!align]') as $tr) {
$item = array();
// title
$item['title'] = $tr->find('td', 0)->find('b', 0)->plaintext;
// content
$item['content'] = $tr->find('ul', 1);
// uri
$item['uri'] = $tr->find('a', 3)->getAttribute('href');
$this->items[] = $item;
}
}
}

View File

@@ -1145,7 +1145,7 @@ class PepperBridgeAbstract extends BridgeAbstract {
} else {
foreach ($list as $deal) {
$item = array();
$item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href;
$item['uri'] = $deal->find('div[class*=threadGrid-title]', 0)->find('a', 0)->href;
$item['title'] = $deal->find('a[class*=' . $selectorLink . ']', 0
)->plaintext;
$item['author'] = $deal->find('span.thread-username', 0)->plaintext;

View File

@@ -0,0 +1,26 @@
<?php
class EngadgetBridge extends FeedExpander {
const MAINTAINER = 'IceWreck';
const NAME = 'Engadget Bridge';
const URI = 'https://www.engadget.com/';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'Article content for Engadget.';
public function collectData(){
$this->collectExpandableDatas(static::URI . 'rss.xml', 15);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
// $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link);
// figure contain's the main article image
$article = $articlePage->find('figure', 0);
// .article-text has the actual article
foreach($articlePage->find('.article-text') as $element)
$article = $article . $element;
$item['content'] = $article;
return $item;
}
}

View File

@@ -0,0 +1,36 @@
<?php
class FabriceBellardBridge extends BridgeAbstract {
const NAME = 'Fabrice Bellard';
const URI = 'https://bellard.org/';
const DESCRIPTION = "Fabrice Bellard's Home Page";
const MAINTAINER = 'somini';
public function collectData() {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not load content');
foreach ($html->find('p') as $obj) {
$item = array();
$html = defaultLinkTo($html, $this->getURI());
$links = $obj->find('a');
if (count($links) > 0) {
$link_uri = $links[0]->href;
} else {
$link_uri = $this->getURI();
}
/* try to make sure the link is valid */
if ($link_uri[-1] !== '/' && strpos($link_uri, '/') === false) {
$link_uri = $link_uri . '/';
}
$item['title'] = strip_tags($obj->innertext);
$item['uri'] = $link_uri;
$item['content'] = $obj->innertext;
$this->items[] = $item;
}
}
}

View File

@@ -142,7 +142,11 @@ class FacebookBridge extends BridgeAbstract {
private function collectGroupData() {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
if(getEnv('HTTP_ACCEPT_LANGUAGE')) {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
} else {
$header = array();
}
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('Failed loading facebook page: ' . $this->getURI());
@@ -505,7 +509,11 @@ EOD;
// Retrieve page contents
if(is_null($html)) {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
if(getEnv('HTTP_ACCEPT_LANGUAGE')) {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
} else {
$header = array();
}
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('No results for this query.');
@@ -580,6 +588,8 @@ EOD;
'._5mly', // Remove embedded videos (the preview image remains)
'._2ezg', // Remove "Views ..."
'.hidden_elem', // Remove hidden elements (they are hidden anyway)
'.timestampContent', // Remove relative timestamp
'._6spk', // Remove redundant separator
);
foreach($content_filters as $filter) {

View File

@@ -0,0 +1,918 @@
<?php
class FurAffinityBridge extends BridgeAbstract {
const NAME = 'FurAffinity Bridge';
const URI = 'https://www.furaffinity.net';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Returns posts from various sections of FurAffinity';
const MAINTAINER = 'Roliga';
const PARAMETERS = array(
'Search' => array(
'q' => array(
'name' => 'Query',
'required' => true
),
'rating-general' => array(
'name' => 'General',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'rating-mature' => array(
'name' => 'Mature',
'type' => 'checkbox',
),
'rating-adult' => array(
'name' => 'Adult',
'type' => 'checkbox',
),
'range' => array(
'name' => 'Time range',
'type' => 'list',
'values' => array(
'A Day' => 'day',
'3 Days' => '3days',
'A Week' => 'week',
'A Month' => 'month',
'All time' => 'all'
),
'defaultValue' => 'all'
),
'type-art' => array(
'name' => 'Art',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'type-flash' => array(
'name' => 'Flash',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'type-photo' => array(
'name' => 'Photography',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'type-music' => array(
'name' => 'Music',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'type-story' => array(
'name' => 'Story',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'type-poetry' => array(
'name' => 'Poetry',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'mode' => array(
'name' => 'Match mode',
'type' => 'list',
'values' => array(
'All of the words' => 'all',
'Any of the words' => 'any',
'Extended' => 'extended'
),
'defaultValue' => 'extended'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 10,
'title' => 'Limit number of submissions to return. -1 for unlimited.'
),
'full' => array(
'name' => 'Full view',
'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'cache' => array(
'name' => 'Cache submission pages',
'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
'type' => 'checkbox',
'defaultValue' => 'checked'
)
),
'Browse' => array(
'cat' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'Visual Art' => array(
'All' => 1,
'Artwork (Digital)' => 2,
'Artwork (Traditional)' => 3,
'Cellshading' => 4,
'Crafting' => 5,
'Designs' => 6,
'Flash' => 7,
'Fursuiting' => 8,
'Icons' => 9,
'Mosaics' => 10,
'Photography' => 11,
'Sculpting' => 12
),
'Readable Art' => array(
'Story' => 13,
'Poetry' => 14,
'Prose' => 15
),
'Audio Art' => array(
'Music' => 16,
'Podcasts' => 17
),
'Downloadable' => array(
'Skins' => 18,
'Handhelds' => 19,
'Resources' => 20
),
'Other Stuff' => array(
'Adoptables' => 21,
'Auctions' => 22,
'Contests' => 23,
'Current Events' => 24,
'Desktops' => 25,
'Stockart' => 26,
'Screenshots' => 27,
'Scraps' => 28,
'Wallpaper' => 29,
'YCH / Sale' => 30,
'Other' => 31
)
),
'defaultValue' => 1
),
'atype' => array(
'name' => 'Type',
'type' => 'list',
'values' => array(
'General Things' => array(
'All' => 1,
'Abstract' => 2,
'Animal related (non-anthro)' => 3,
'Anime' => 4,
'Comics' => 5,
'Doodle' => 6,
'Fanart' => 7,
'Fantasy' => 8,
'Human' => 9,
'Portraits' => 10,
'Scenery' => 11,
'Still Life' => 12,
'Tutorials' => 13,
'Miscellaneous' => 14
),
'Fetish / Furry specialty' => array(
'Baby fur' => 101,
'Bondage' => 102,
'Digimon' => 103,
'Fat Furs' => 104,
'Fetish Other' => 105,
'Fursuit' => 106,
'Gore / Macabre Art' => 119,
'Hyper' => 107,
'Inflation' => 108,
'Macro / Micro' => 109,
'Muscle' => 110,
'My Little Pony / Brony' => 111,
'Paw' => 112,
'Pokemon' => 113,
'Pregnancy' => 114,
'Sonic' => 115,
'Transformation' => 116,
'Vore' => 117,
'Water Sports' => 118,
'General Furry Art' => 100
),
'Music' => array(
'Techno' => 201,
'Trance' => 202,
'House' => 203,
'90s' => 204,
'80s' => 205,
'70s' => 206,
'60s' => 207,
'Pre-60s' => 208,
'Classical' => 209,
'Game Music' => 210,
'Rock' => 211,
'Pop' => 212,
'Rap' => 213,
'Industrial' => 214,
'Other Music' => 200
)
),
'defaultValue' => 1
),
'species' => array(
'name' => 'Species',
'type' => 'list',
'values' => array(
'Unspecified / Any' => 1,
'Amphibian' => array(
'Frog' => 1001,
'Newt' => 1002,
'Salamander' => 1003,
'Amphibian (Other)' => 1000
),
'Aquatic' => array(
'Cephalopod' => 2001,
'Dolphin' => 2002,
'Fish' => 2005,
'Porpoise' => 2004,
'Seal' => 6068,
'Shark' => 2006,
'Whale' => 2003,
'Aquatic (Other)' => 2000
),
'Avian' => array(
'Corvid' => 3001,
'Crow' => 3002,
'Duck' => 3003,
'Eagle' => 3004,
'Falcon' => 3005,
'Goose' => 3006,
'Gryphon' => 3007,
'Hawk' => 3008,
'Owl' => 3009,
'Phoenix' => 3010,
'Swan' => 3011,
'Avian (Other)' => 3000
),
'Bears &amp; Ursines' => array(
'Bear' => 6002
),
'Camelids' => array(
'Camel' => 6074,
'Llama' => 6036
),
'Canines &amp; Lupines' => array(
'Coyote' => 6008,
'Doberman' => 6009,
'Dog' => 6010,
'Dingo' => 6011,
'German Shepherd' => 6012,
'Jackal' => 6013,
'Husky' => 6014,
'Wolf' => 6016,
'Canine (Other)' => 6017
),
'Cervines' => array(
'Cervine (Other)' => 6018
),
'Cows &amp; Bovines' => array(
'Antelope' => 6004,
'Cows' => 6003,
'Gazelle' => 6005,
'Goat' => 6006,
'Bovines (General)' => 6007
),
'Dragons' => array(
'Eastern Dragon' => 4001,
'Hydra' => 4002,
'Serpent' => 4003,
'Western Dragon' => 4004,
'Wyvern' => 4005,
'Dragon (Other)' => 4000
),
'Equestrians' => array(
'Donkey' => 6019,
'Horse' => 6034,
'Pony' => 6073,
'Zebra' => 6071
),
'Exotic &amp; Mythicals' => array(
'Argonian' => 5002,
'Chakat' => 5003,
'Chocobo' => 5004,
'Citra' => 5005,
'Crux' => 5006,
'Daemon' => 5007,
'Digimon' => 5008,
'Dracat' => 5009,
'Draenei' => 5010,
'Elf' => 5011,
'Gargoyle' => 5012,
'Iksar' => 5013,
'Kaiju/Monster' => 5015,
'Langurhali' => 5014,
'Moogle' => 5017,
'Naga' => 5016,
'Orc' => 5018,
'Pokemon' => 5019,
'Satyr' => 5020,
'Sergal' => 5021,
'Tanuki' => 5022,
'Unicorn' => 5023,
'Xenomorph' => 5024,
'Alien (Other)' => 5001,
'Exotic (Other)' => 5000
),
'Felines' => array(
'Domestic Cat' => 6020,
'Cheetah' => 6021,
'Cougar' => 6022,
'Jaguar' => 6023,
'Leopard' => 6024,
'Lion' => 6025,
'Lynx' => 6026,
'Ocelot' => 6027,
'Panther' => 6028,
'Tiger' => 6029,
'Feline (Other)' => 6030
),
'Insects' => array(
'Arachnid' => 8000,
'Mantid' => 8004,
'Scorpion' => 8005,
'Insect (Other)' => 8003
),
'Mammals (Other)' => array(
'Bat' => 6001,
'Giraffe' => 6031,
'Hedgehog' => 6032,
'Hippopotamus' => 6033,
'Hyena' => 6035,
'Panda' => 6052,
'Pig/Swine' => 6053,
'Rabbit/Hare' => 6059,
'Raccoon' => 6060,
'Red Panda' => 6062,
'Meerkat' => 6043,
'Mongoose' => 6044,
'Rhinoceros' => 6063,
'Mammals (Other)' => 6000
),
'Marsupials' => array(
'Opossum' => 6037,
'Kangaroo' => 6038,
'Koala' => 6039,
'Quoll' => 6040,
'Wallaby' => 6041,
'Marsupial (Other)' => 6042
),
'Mustelids' => array(
'Badger' => 6045,
'Ferret' => 6046,
'Mink' => 6048,
'Otter' => 6047,
'Skunk' => 6069,
'Weasel' => 6049,
'Mustelid (Other)' => 6051
),
'Primates' => array(
'Gorilla' => 6054,
'Human' => 6055,
'Lemur' => 6056,
'Monkey' => 6057,
'Primate (Other)' => 6058
),
'Reptillian' => array(
'Alligator &amp; Crocodile' => 7001,
'Gecko' => 7003,
'Iguana' => 7004,
'Lizard' => 7005,
'Snakes &amp; Serpents' => 7006,
'Turtle' => 7007,
'Reptilian (Other)' => 7000
),
'Rodents' => array(
'Beaver' => 6064,
'Mouse' => 6065,
'Rat' => 6061,
'Squirrel' => 6070,
'Rodent (Other)' => 6067
),
'Vulpines' => array(
'Fennec' => 6072,
'Fox' => 6075,
'Vulpine (Other)' => 6015
),
'Other' => array(
'Dinosaur' => 8001,
'Wolverine' => 6050
)
),
'defaultValue' => 1
),
'gender' => array(
'name' => 'Gender',
'type' => 'list',
'values' => array(
'Any' => 0,
'Male' => 2,
'Female' => 3,
'Herm' => 4,
'Transgender' => 5,
'Multiple characters' => 6,
'Other / Not Specified' => 7
),
'defaultValue' => 0
),
'rating_general' => array(
'name' => 'General',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'rating_mature' => array(
'name' => 'Mature',
'type' => 'checkbox',
),
'rating_adult' => array(
'name' => 'Adult',
'type' => 'checkbox',
),
'limit-browse' => array(
'name' => 'Limit',
'type' => 'number',
'required' => true,
'defaultValue' => 10,
'title' => 'Limit number of submissions to return. -1 for unlimited.'
),
'full' => array(
'name' => 'Full view',
'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'cache' => array(
'name' => 'Cache submission pages',
'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
'type' => 'checkbox',
'defaultValue' => 'checked'
)
),
'Journals' => array(
'username-journals' => array(
'name' => 'Username',
'required' => true,
'title' => 'Lowercase username as seen in URLs'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => -1,
'title' => 'Limit number of journals to return. -1 for unlimited.'
)
),
'Single Journal' => array(
'journal-id' => array(
'name' => 'Journal ID',
'required' => true,
'type' => 'number',
'title' => 'Number seen in journal URL'
)
),
'Gallery' => array(
'username-gallery' => array(
'name' => 'Username',
'required' => true,
'title' => 'Lowercase username as seen in URLs'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 10,
'title' => 'Limit number of submissions to return. -1 for unlimited.'
),
'full' => array(
'name' => 'Full view',
'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'cache' => array(
'name' => 'Cache submission pages',
'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
'type' => 'checkbox',
'defaultValue' => 'checked'
)
),
'Scraps' => array(
'username-scraps' => array(
'name' => 'Username',
'required' => true,
'title' => 'Lowercase username as seen in URLs'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 10,
'title' => 'Limit number of submissions to return. -1 for unlimited.'
),
'full' => array(
'name' => 'Full view',
'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'cache' => array(
'name' => 'Cache submission pages',
'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
'type' => 'checkbox',
'defaultValue' => 'checked'
)
),
'Favorites' => array(
'username-favorites' => array(
'name' => 'Username',
'required' => true,
'title' => 'Lowercase username as seen in URLs'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 10,
'title' => 'Limit number of submissions to return. -1 for unlimited.'
),
'full' => array(
'name' => 'Full view',
'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'cache' => array(
'name' => 'Cache submission pages',
'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
'type' => 'checkbox',
'defaultValue' => 'checked'
)
),
'Gallery Folder' => array(
'username-folder' => array(
'name' => 'Username',
'required' => true,
'title' => 'Lowercase username as seen in URLs'
),
'folder-id' => array(
'name' => 'Folder ID',
'required' => true,
'type' => 'number',
'title' => 'Number seen in folder URL'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 10,
'title' => 'Limit number of submissions to return. -1 for unlimited.'
),
'full' => array(
'name' => 'Full view',
'title' => 'Include description, tags, date and larger image in article. Uses more bandwidth.',
'type' => 'checkbox',
'defaultValue' => 'checked'
),
'cache' => array(
'name' => 'Cache submission pages',
'title' => 'Reduces requests to FA when Full view is enabled. Changes to submission details may be delayed.',
'type' => 'checkbox',
'defaultValue' => 'checked'
)
)
);
/*
* This was aquired by creating a new user on FA then
* extracting the cookie from the browsers dev console.
*/
const FA_AUTH_COOKIE = 'b=4ce65691-b50f-4742-a990-bf28d6de16ee; a=ca6e4566-9d81-4263-9444-653b142e35f8';
public function detectParameters($url) {
$params = array();
// Single journal
$regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journal\/(\d+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['journal-id'] = urldecode($matches[3]);
return $params;
}
// Journals
$regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/journals\/([^\/&?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['username-journals'] = urldecode($matches[3]);
return $params;
}
// Gallery folder
$regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/gallery\/([^\/&?\n]+)\/folder\/(\d+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['username-folder'] = urldecode($matches[3]);
$params['folder-id'] = urldecode($matches[4]);
$params['full'] = 'on';
return $params;
}
// Gallery (must be after gallery folder)
$regex = '/^(https?:\/\/)?(www\.)?furaffinity.net\/(gallery|scraps|favorites)\/([^\/&?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['username-' . $matches[3]] = urldecode($matches[4]);
$params['full'] = 'on';
return $params;
}
return null;
}
public function getName() {
switch($this->queriedContext) {
case 'Search':
return 'Search For '
. $this->getInput('q');
case 'Browse':
return 'Browse';
case 'Journals':
return $this->getInput('username-journals');
case 'Single Journal':
return 'Journal '
. $this->getInput('journal-id');
case 'Gallery':
return $this->getInput('username-gallery');
case 'Scraps':
return $this->getInput('username-scraps');
case 'Favorites':
return $this->getInput('username-favorites');
case 'Gallery Folder':
return $this->getInput('username-folder')
. '\'s Folder '
. $this->getInput('folder-id');
default: return parent::getName();
}
}
public function getDescription() {
switch($this->queriedContext) {
case 'Search':
return 'FurAffinity Search For '
. $this->getInput('q');
case 'Browse':
return 'FurAffinity Browse';
case 'Journals':
return 'FurAffinity Journals By '
. $this->getInput('username-journals');
case 'Single Journal':
return 'FurAffinity Journal '
. $this->getInput('journal-id');
case 'Gallery':
return 'FurAffinity Gallery By '
. $this->getInput('username-gallery');
case 'Scraps':
return 'FurAffinity Scraps By '
. $this->getInput('username-scraps');
case 'Favorites':
return 'FurAffinity Favorites By '
. $this->getInput('username-favorites');
case 'Gallery Folder':
return 'FurAffinity Gallery Folder '
. $this->getInput('folder-id')
. ' By '
. $this->getInput('username-folder');
default: return parent::getDescription();
}
}
public function getURI() {
switch($this->queriedContext) {
case 'Search':
return SELF::URI
. '/search';
case 'Browse':
return SELF::URI
. '/browse';
case 'Journals':
return SELF::URI
. '/journals/'
. $this->getInput('username-journals');
case 'Single Journal':
return SELF::URI
. '/journal/'
. $this->getInput('journal-id');
case 'Gallery':
return SELF::URI
. '/gallery/'
. $this->getInput('username-gallery');
case 'Scraps':
return SELF::URI
. '/scraps/'
. $this->getInput('username-scraps');
case 'Favorites':
return SELF::URI
. '/favorites/'
. $this->getInput('username-favorites');
case 'Gallery Folder':
return SELF::URI
. '/gallery/'
. $this->getInput('username-folder')
. '/folder/'
. $this->getInput('folder-id');
default: return parent::getURI();
}
}
public function collectData() {
switch($this->queriedContext) {
case 'Search':
$data = array(
'q' => $this->getInput('q'),
'perpage' => 72,
'rating-general' => ($this->getInput('rating-general') === true ? 'on' : 0),
'rating-mature' => ($this->getInput('rating-mature') === true ? 'on' : 0),
'rating-adult' => ($this->getInput('rating-adult') === true ? 'on' : 0),
'range' => $this->getInput('range'),
'type-art' => ($this->getInput('type-art') === true ? 'on' : 0),
'type-flash' => ($this->getInput('type-flash') === true ? 'on' : 0),
'type-photo' => ($this->getInput('type-photo') === true ? 'on' : 0),
'type-music' => ($this->getInput('type-music') === true ? 'on' : 0),
'type-story' => ($this->getInput('type-story') === true ? 'on' : 0),
'type-poetry' => ($this->getInput('type-poetry') === true ? 'on' : 0),
'mode' => $this->getInput('mode')
);
$html = $this->postFASimpleHTMLDOM($data);
$limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10);
$this->itemsFromSubmissionList($html, $limit);
break;
case 'Browse':
$data = array(
'cat' => $this->getInput('cat'),
'atype' => $this->getInput('atype'),
'species' => $this->getInput('species'),
'gender' => $this->getInput('gender'),
'perpage' => 72,
'rating_general' => ($this->getInput('rating_general') === true ? 'on' : 0),
'rating_mature' => ($this->getInput('rating_mature') === true ? 'on' : 0),
'rating_adult' => ($this->getInput('rating_adult') === true ? 'on' : 0)
);
$html = $this->postFASimpleHTMLDOM($data);
$limit = (is_int($this->getInput('limit-browse')) ? $this->getInput('limit-browse') : 10);
$this->itemsFromSubmissionList($html, $limit);
break;
case 'Journals':
$html = $this->getFASimpleHTMLDOM($this->getURI());
$limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : -1);
$this->itemsFromJournalList($html, $limit);
break;
case 'Single Journal':
$html = $this->getFASimpleHTMLDOM($this->getURI());
$this->itemsFromJournal($html);
break;
case 'Gallery':
case 'Scraps':
case 'Favorites':
case 'Gallery Folder':
$html = $this->getFASimpleHTMLDOM($this->getURI());
$limit = (is_int($this->getInput('limit')) ? $this->getInput('limit') : 10);
$this->itemsFromSubmissionList($html, $limit);
break;
}
}
private function postFASimpleHTMLDOM($data) {
$opts = array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => http_build_query($data)
);
$header = array(
'Host: ' . parse_url(self::URI, PHP_URL_HOST),
'Content-Type: application/x-www-form-urlencoded',
'Cookie: ' . self::FA_AUTH_COOKIE
);
$html = getSimpleHTMLDOM($this->getURI(), $header, $opts);
$html = defaultLinkTo($html, $this->getURI());
return $html;
}
private function getFASimpleHTMLDOM($url, $cache = false) {
$header = array(
'Cookie: ' . self::FA_AUTH_COOKIE
);
if($cache) {
$html = getSimpleHTMLDOMCached($url, 86400, $header); // 24 hours
} else {
$html = getSimpleHTMLDOM($url, $header);
}
$html = defaultLinkTo($html, $url);
return $html;
}
private function itemsFromJournalList($html, $limit) {
foreach($html->find('table[id^=jid:]') as $journal) {
# allows limit = -1 to mean 'unlimited'
if($limit-- === 0) break;
$item = array();
$this->setReferrerPolicy($journal);
$item['uri'] = $journal->find('a', 0)->href;
$item['title'] = html_entity_decode($journal->find('a', 0)->plaintext);
$item['author'] = $this->getInput('username-journals');
$item['timestamp'] = strtotime(
$journal->find('span.popup_date', 0)->plaintext);
$item['content'] = $journal
->find('.alt1 table div.no_overflow', 0)
->innertext;
$this->items[] = $item;
}
}
private function itemsFromJournal($html) {
$this->setReferrerPolicy($html);
$item = array();
$item['uri'] = $this->getURI();
$title = $html->find('.journal-title-box .no_overflow', 0)->plaintext;
$title = html_entity_decode($title);
$title = trim($title, " \t\n\r\0\x0B" . chr(0xC2) . chr(0xA0));
$item['title'] = $title;
$item['author'] = $html->find('.journal-title-box a', 0)->plaintext;
$item['timestamp'] = strtotime(
$html->find('.journal-title-box span.popup_date', 0)->plaintext);
$item['content'] = $html->find('.journal-body', 0)->innertext;
$this->items[] = $item;
}
private function itemsFromSubmissionList($html, $limit) {
$cache = ($this->getInput('cache') === true);
foreach($html->find('section.gallery figure') as $figure) {
# allows limit = -1 to mean 'unlimited'
if($limit-- === 0) break;
$item = array();
$submissionURL = $figure->find('b u a', 0)->href;
$imgURL = 'https:' . $figure->find('b u a img', 0)->src;
$item['uri'] = $submissionURL;
$item['title'] = html_entity_decode(
$figure->find('figcaption p a[href*=/view/]', 0)->title);
$item['author'] = $figure->find('figcaption p a[href*=/user/]', 0)->title;
if($this->getInput('full') === true) {
$submissionHTML = $this->getFASimpleHTMLDOM($submissionURL, $cache);
$stats = $submissionHTML->find('.stats-container', 0);
$item['timestamp'] = strtotime($stats->find('.popup_date', 0)->title);
$item['enclosures'] = array(
$submissionHTML->find('.actions a[href^=https://d.facdn]', 0)->href
);
foreach($stats->find('#keywords a') as $keyword) {
$item['categories'][] = $keyword->plaintext;
}
$previewSrc = $submissionHTML->find('#submissionImg', 0)
->{'data-preview-src'};
if($previewSrc) {
$imgURL = 'https:' . $previewSrc;
}
$description = $submissionHTML
->find('.maintable .maintable tr td.alt1', -1);
$this->setReferrerPolicy($description);
$description = $description->innertext;
$item['content'] = <<<EOD
<a href="$submissionURL">
<img src="{$imgURL}" referrerpolicy="no-referrer" />
</a>
<p>
{$description}
</p>
EOD;
} else {
$item['content'] = <<<EOD
<a href="$submissionURL">
<img src="$imgURL" referrerpolicy="no-referrer" />
</a>
EOD;
}
$this->items[] = $item;
}
}
private function setReferrerPolicy(&$html) {
foreach($html->find('img') as $img) {
/*
* Note: Without the no-referrer policy their CDN sometimes denies requests.
* We can't control this for enclosures sadly.
* At least tt-rss adds the referrerpolicy on its own.
* Alternatively we could not use https for images, but that's not ideal.
*/
$img->referrerpolicy = 'no-referrer';
}
}
}

View File

@@ -61,7 +61,7 @@ class GQMagazineBridge extends BridgeAbstract
private function findTitleOf($link) {
foreach (self::POSSIBLE_TITLES as $tag) {
$title = $link->find($tag, 0);
$title = $link->parent()->find($tag, 0);
if($title !== null) {
if($title->plaintext !== null) {
return $title->plaintext;
@@ -77,11 +77,13 @@ class GQMagazineBridge extends BridgeAbstract
// Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content !
$main = $html->find('main', 0);
foreach ($main->find('a') as $link) {
if(strpos($link, $this->getInput('page')))
continue;
$uri = $link->href;
$date = $link->find('time', 0);
$date = $link->parent()->find('time', 0);
$item = array();
$author = $link->find('span[itemprop=name]', 0);
$author = $link->parent()->find('span[itemprop=name]', 0);
if($author !== null) {
$item['author'] = $author->plaintext;
$item['title'] = $this->findTitleOf($link);

View File

@@ -13,6 +13,11 @@ class HaveIBeenPwnedBridge extends BridgeAbstract {
'Date added to HIBP' => 'dateAdded',
),
'defaultValue' => 'dateAdded',
),
'item_limit' => array(
'name' => 'Limit number of returned items',
'type' => 'number',
'defaultValue' => 20,
)
));
@@ -109,6 +114,12 @@ class HaveIBeenPwnedBridge extends BridgeAbstract {
*/
private function createItems() {
$limit = $this->getInput('item_limit');
if ($limit < 1) {
$limit = 20;
}
foreach ($this->breaches as $breach) {
$item = array();
@@ -118,6 +129,10 @@ class HaveIBeenPwnedBridge extends BridgeAbstract {
$item['content'] = $breach['content'];
$this->items[] = $item;
if (count($this->items) >= $limit) {
break;
}
}
}
}

View File

@@ -3,7 +3,7 @@ class HentaiHavenBridge extends BridgeAbstract {
const MAINTAINER = 'albirew';
const NAME = 'Hentai Haven';
const URI = 'http://hentaihaven.org/';
const URI = 'https://hentaihaven.org/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns releases from Hentai Haven';

55
bridges/IGNBridge.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
class IGNBridge extends FeedExpander {
const MAINTAINER = 'IceWreck';
const NAME = 'IGN Bridge';
const URI = 'https://www.ign.com/';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'RSS Feed For IGN';
public function collectData(){
$this->collectExpandableDatas('http://feeds.ign.com/ign/all', 15);
}
// IGNs feed is both hidden and incomplete. This bridge tries to fix this.
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
// $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link);
/*
* NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism
* for handling them seperately as it just ignores the DOM querys which it does not find.
* (and their scraping)
*/
// For Articles
$article = $articlePage->find('section.article-page', 0);
// add in verdicts in articles, reviews etc
foreach($articlePage->find('div.article-section') as $element) {
$article = $article . $element;
}
// For Wikis and HowTos
$uselessWikiElements = array(
'.wiki-page-tools',
'.feedback-container',
'.paging-container'
);
foreach($articlePage->find('.wiki-page') as $wikiContents) {
$copy = clone $wikiContents;
// Remove useless elements present in IGN wiki/howtos
foreach($uselessWikiElements as $uslElement) {
$toRemove = $wikiContents->find($uslElement, 0);
$copy = str_replace($toRemove, '', $copy);
}
$article = $article . $copy;
}
// Add content to feed
$item['content'] = $article;
return $item;
}
}

View File

@@ -42,6 +42,38 @@ class InstagramBridge extends BridgeAbstract {
);
const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951';
const TAG_QUERY_HASH = '174a5243287c5f3a7de741089750ab3b';
const STORY_QUERY_HASH = '865589822932d1b43dfe312121dd353a';
protected function getInstagramUserId($username) {
if(is_numeric($username)) return $username;
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope(get_called_class());
$cache->setKey([$username]);
$key = $cache->loadData();
if($key == null) {
$data = getContents(self::URI . 'web/search/topsearch/?query=' . $username);
foreach(json_decode($data)->users as $user) {
if($user->user->username === $username) {
$key = $user->user->pk;
}
}
if($key == null) {
returnServerError('Unable to find username in search result.');
}
$cache->saveData($key);
}
return $key;
}
public function collectData(){
if(is_null($this->getInput('u')) && $this->getInput('media_type') == 'story') {
@@ -51,9 +83,9 @@ class InstagramBridge extends BridgeAbstract {
$data = $this->getInstagramJSON($this->getURI());
if(!is_null($this->getInput('u'))) {
$userMedia = $data->entry_data->ProfilePage[0]->graphql->user->edge_owner_to_timeline_media->edges;
$userMedia = $data->data->user->edge_owner_to_timeline_media->edges;
} elseif(!is_null($this->getInput('h'))) {
$userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges;
$userMedia = $data->data->hashtag->edge_hashtag_to_media->edges;
} elseif(!is_null($this->getInput('l'))) {
$userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges;
}
@@ -99,6 +131,7 @@ class InstagramBridge extends BridgeAbstract {
}
if(!is_null($this->getInput('u')) && $media->__typename == 'GraphSidecar') {
$data = $this->getInstagramStory($item['uri']);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
@@ -118,8 +151,15 @@ class InstagramBridge extends BridgeAbstract {
protected function getInstagramStory($uri) {
$data = $this->getInstagramJSON($uri);
$mediaInfo = $data->entry_data->PostPage[0]->graphql->shortcode_media;
$shortcode = explode('/', $uri)[4];
$data = getContents(self::URI .
'graphql/query/?query_hash=' .
self::STORY_QUERY_HASH .
'&variables={"shortcode"%3A"' .
$shortcode .
'"}');
$mediaInfo = json_decode($data)->data->shortcode_media;
//Process the first element, that isn't in the node graph
if (count($mediaInfo->edge_media_to_caption->edges) > 0) {
@@ -145,13 +185,38 @@ class InstagramBridge extends BridgeAbstract {
protected function getInstagramJSON($uri) {
$html = getContents($uri)
or returnServerError('Could not request Instagram.');
$scriptRegex = '/window\._sharedData = (.*);<\/script>/';
if(!is_null($this->getInput('u'))) {
preg_match($scriptRegex, $html, $matches, PREG_OFFSET_CAPTURE, 0);
$userId = $this->getInstagramUserId($this->getInput('u'));
return json_decode($matches[1][0]);
$data = getContents(self::URI .
'graphql/query/?query_hash=' .
self::USER_QUERY_HASH .
'&variables={"id"%3A"' .
$userId .
'"%2C"first"%3A10}');
return json_decode($data);
} elseif(!is_null($this->getInput('h'))) {
$data = getContents(self::URI .
'graphql/query/?query_hash=' .
self::TAG_QUERY_HASH .
'&variables={"tag_name"%3A"' .
$this->getInput('h') .
'"%2C"first"%3A10}');
return json_decode($data);
} else {
$html = getContents($uri)
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]);
}
}

View File

@@ -133,7 +133,7 @@ class InternetArchiveBridge extends BridgeAbstract {
$item['title'] = trim($result->find('div.ttl', 0)->innertext);
$item['timestamp'] = strtotime($result->find('div.hidden-tiles.pubdate.C.C3', 0)->children(0)->plaintext);
$item['uri'] = self::URI . $result->find('div.item-ttl.C.C2 > a', 0)->href;
$item['uri'] = $result->find('div.item-ttl.C.C2 > a', 0)->href;
if ($result->find('div.by.C.C4', 0)->children(2)) {
$item['author'] = $result->find('div.by.C.C4', 0)->children(2)->plaintext;

View File

@@ -0,0 +1,477 @@
<?php
class LaCentraleBridge extends BridgeAbstract {
const MAINTAINER = 'jacknumber';
const NAME = 'La Centrale';
const URI = 'https://www.lacentrale.fr/';
const DESCRIPTION = 'Returns most recent vehicules ads from LaCentrale';
const PARAMETERS = array( array(
'type' => array(
'name' => 'Type de véhicule',
'type' => 'list',
'values' => array(
'Voiture' => 'car',
'Camion/Pickup' => 'truck',
'Moto' => 'moto',
'Scooter' => 'scooter',
'Quad' => 'quad',
'Caravane/Camping-car' => 'mobileHome'
)
),
'brand' => array(
'name' => 'Marque',
'type' => 'list',
'values' => array(
'' => '',
'ABARTH' => 'ABARTH',
'AC' => 'AC',
'AIXAM' => 'AIXAM',
'ALFA ROMEO' => 'ALFA ROMEO',
'ALKE' => 'ALKE',
'ALPINA' => 'ALPINA',
'ALPINE' => 'ALPINE',
'AMC' => 'AMC',
'ANAIG' => 'ANAIG',
'APRILIA' => 'APRILIA',
'ARIEL' => 'ARIEL',
'ASTON MARTIN' => 'ASTON MARTIN',
'AUDI' => 'AUDI',
'AUSTIN HEALEY' => 'AUSTIN HEALEY',
'AUSTIN' => 'AUSTIN',
'AUTOBIANCHI' => 'AUTOBIANCHI',
'AVINTON' => 'AVINTON',
'BELLIER' => 'BELLIER',
'BENELLI' => 'BENELLI',
'BENTLEY' => 'BENTLEY',
'BETA' => 'BETA',
'BMW' => 'BMW',
'BOLLORE' => 'BOLLORE',
'BRIXTON' => 'BRIXTON',
'BUELL' => 'BUELL',
'BUGATTI' => 'BUGATTI',
'BUICK' => 'BUICK',
'BULLIT' => 'BULLIT',
'CADILLAC' => 'CADILLAC',
'CASALINI' => 'CASALINI',
'CATERHAM' => 'CATERHAM',
'CHATENET' => 'CHATENET',
'CHEVROLET' => 'CHEVROLET',
'CHRYSLER' => 'CHRYSLER',
'CHUNLAN' => 'CHUNLAN',
'CITROEN' => 'CITROEN',
'COURB' => 'COURB',
'CR&S' => 'CR&S',
'CUPRA' => 'CUPRA',
'CYCLONE' => 'CYCLONE',
'DACIA' => 'DACIA',
'DAELIM' => 'DAELIM',
'DAEWOO' => 'DAEWOO',
'DAF' => 'DAF',
'DAIHATSU' => 'DAIHATSU',
'DANGEL' => 'DANGEL',
'DATSUN' => 'DATSUN',
'DE SOTO' => 'DE SOTO',
'DE TOMASO' => 'DE TOMASO',
'DERBI' => 'DERBI',
'DEVINCI' => 'DEVINCI',
'DODGE' => 'DODGE',
'DONKERVOORT' => 'DONKERVOORT',
'DS' => 'DS',
'DUCATI' => 'DUCATI',
'DUCATY' => 'DUCATY',
'DUE' => 'DUE',
'ENFIELD' => 'ENFIELD',
'EXCALIBUR' => 'EXCALIBUR',
'FACEL VEGA' => 'FACEL VEGA',
'FANTIC MOTOR' => 'FANTIC MOTOR',
'FERRARI' => 'FERRARI',
'FIAT' => 'FIAT',
'FISKER' => 'FISKER',
'FORD' => 'FORD',
'FUSO' => 'FUSO',
'GAS GAS' => 'GAS GAS',
'GILERA' => 'GILERA',
'GMC' => 'GMC',
'GOWINN' => 'GOWINN',
'GRANDIN' => 'GRANDIN',
'HARLEY DAVIDSON' => 'HARLEY DAVIDSON',
'HOMMELL' => 'HOMMELL',
'HONDA' => 'HONDA',
'HUMMER' => 'HUMMER',
'HUSABERG' => 'HUSABERG',
'HUSQVARNA' => 'HUSQVARNA',
'HYOSUNG' => 'HYOSUNG',
'HYUNDAI' => 'HYUNDAI',
'INDIAN' => 'INDIAN',
'INFINITI' => 'INFINITI',
'INNOCENTI' => 'INNOCENTI',
'ISUZU' => 'ISUZU',
'IVECO' => 'IVECO',
'JAGUAR' => 'JAGUAR',
'JDM SIMPA' => 'JDM SIMPA',
'JEEP' => 'JEEP',
'JENSEN' => 'JENSEN',
'JIAYUAN' => 'JIAYUAN',
'KAWASAKI' => 'KAWASAKI',
'KEEWAY' => 'KEEWAY',
'KIA' => 'KIA',
'KSR' => 'KSR',
'KTM' => 'KTM',
'KYMCO' => 'KYMCO',
'LADA' => 'LADA',
'LAMBORGHINI' => 'LAMBORGHINI',
'LANCIA' => 'LANCIA',
'LAND ROVER' => 'LAND ROVER',
'LEXUS' => 'LEXUS',
'LIGIER' => 'LIGIER',
'LINCOLN' => 'LINCOLN',
'LONDON TAXI COMPANY' => 'LONDON TAXI COMPANY',
'LOTUS' => 'LOTUS',
'MAGPOWER' => 'MAGPOWER',
'MAN' => 'MAN',
'MASAI' => 'MASAI',
'MASERATI' => 'MASERATI',
'MASH' => 'MASH',
'MATRA' => 'MATRA',
'MAYBACH' => 'MAYBACH',
'MAZDA' => 'MAZDA',
'MCLAREN' => 'MCLAREN',
'MEGA' => 'MEGA',
'MERCEDES' => 'MERCEDES',
'MERCEDES-AMG' => 'MERCEDES-AMG',
'MERCURY' => 'MERCURY',
'MEYERS MANX' => 'MEYERS MANX',
'MG' => 'MG',
'MIA ELECTRIC' => 'MIA ELECTRIC',
'MICROCAR' => 'MICROCAR',
'MINAUTO' => 'MINAUTO',
'MINI' => 'MINI',
'MITSUBISHI' => 'MITSUBISHI',
'MORGAN' => 'MORGAN',
'MORRIS' => 'MORRIS',
'MOTO GUZZI' => 'MOTO GUZZI',
'MOTO MORINI' => 'MOTO MORINI',
'MOTOBECANE' => 'MOTOBECANE',
'MPM MOTORS' => 'MPM MOTORS',
'MV AGUSTA' => 'MV AGUSTA',
'NISSAN' => 'NISSAN',
'NORTON' => 'NORTON',
'NSU' => 'NSU',
'OLDSMOBILE' => 'OLDSMOBILE',
'OPEL' => 'OPEL',
'ORCAL' => 'ORCAL',
'OSSA' => 'OSSA',
'PACKARD' => 'PACKARD',
'PANTHER' => 'PANTHER',
'PEUGEOT' => 'PEUGEOT',
'PGO' => 'PGO',
'PIAGGIO' => 'PIAGGIO',
'PLYMOUTH' => 'PLYMOUTH',
'POLARIS' => 'POLARIS',
'PONTIAC' => 'PONTIAC',
'PORSCHE' => 'PORSCHE',
'REALM' => 'REALM',
'REGAL RAPTOR' => 'REGAL RAPTOR',
'RENAULT' => 'RENAULT',
'RIEJU' => 'RIEJU',
'ROLLS ROYCE' => 'ROLLS ROYCE',
'ROVER' => 'ROVER',
'ROYAL ENFIELD' => 'ROYAL ENFIELD',
'SAAB' => 'SAAB',
'SANTANA' => 'SANTANA',
'SCANIA' => 'SCANIA',
'SEAT' => 'SEAT',
'SECMA' => 'SECMA',
'SHELBY' => 'SHELBY',
'SHERCO' => 'SHERCO',
'SIMCA' => 'SIMCA',
'SKODA' => 'SKODA',
'SMART' => 'SMART',
'SPYKER' => 'SPYKER',
'SSANGYONG' => 'SSANGYONG',
'STUDEBAKER' => 'STUDEBAKER',
'SUBARU' => 'SUBARU',
'SUNBEAM' => 'SUNBEAM',
'SUZUKI' => 'SUZUKI',
'SWM' => 'SWM',
'SYM' => 'SYM',
'TALBOT SIMCA' => 'TALBOT SIMCA',
'TALBOT' => 'TALBOT',
'TEILHOL' => 'TEILHOL',
'TESLA' => 'TESLA',
'TM' => 'TM',
'TNT MOTOR' => 'TNT MOTOR',
'TOYOTA' => 'TOYOTA',
'TRIUMPH' => 'TRIUMPH',
'TVR' => 'TVR',
'VAUXHALL' => 'VAUXHALL',
'VESPA' => 'VESPA',
'VICTORY' => 'VICTORY',
'VOLKSWAGEN' => 'VOLKSWAGEN',
'VOLVO' => 'VOLVO',
'VOXAN' => 'VOXAN',
'WIESMANN' => 'WIESMANN',
'YAMAHA' => 'YAMAHA',
'YCF' => 'YCF',
'ZERO' => 'ZERO',
'ZONGSHEN' => 'ZONGSHEN'
)
),
'model' => array(
'name' => 'Modèle',
'type' => 'text',
'title' => 'Get the exact name on LaCentrale'
),
'versions' => array(
'name' => 'Version(s)',
'type' => 'text',
'title' => 'Get the exact name(s) on LaCentrale. Separate by comma'
),
'category' => array(
'name' => 'Catégorie',
'type' => 'list',
'values' => array(
'' => '',
'Voiture' => array(
'4x4, SUV & Crossover' => '47',
'Citadine' => '40',
'Berline' => '41_42',
'Break' => '43',
'Cabriolet' => '46',
'Coupé' => '45',
'Monospace' => '44',
'Bus et minibus' => '82',
'Fourgonnette' => '85',
'Fourgon (< 3,5 tonnes)' => '81',
'Pick-up' => '50',
'Voiture société, commerciale' => '80',
'Sans permis' => '48',
'Camion (> 3,5 tonnes)' => '83',
),
'Camion/Pickup' => array(
'Camion (> 3,5 tonnes)' => '83',
'Fourgon (< 3,5 tonnes)' => '81',
'Bus et minibus' => '82',
'Fourgonnette' => '85',
'Pick-up' => '50',
'Voiture société, commerciale' => '80'
),
'Moto' => array(
'Custom' => '60',
'Offroad' => '61',
'Roadster' => '62',
'GT' => '63',
'Mini moto' => '64',
'Mobylette' => '65',
'Supermotard' => '66',
'Trail' => '67',
'Side-car' => '69',
'Sportive' => '68'
),
'Caravane/Camping-car' => array(
'Caravane' => '423',
'Profilé' => '506',
'Fourgon aménagé' => '507',
'Intégral' => '508',
'Capucine' => '510'
)
)
),
'pricemin' => array(
'name' => 'Prix min',
'type' => 'number'
),
'pricemax' => array(
'name' => 'Prix max',
'type' => 'number'
),
'location' => array(
'name' => 'CP ou département',
'type' => 'number',
'title' => 'Only one'
),
'distance' => array(
'name' => 'Rayon de recherche',
'type' => 'list',
'values' => array(
'' => '',
'10 km' => '1',
'20 km' => '2',
'50 km' => '3',
'100 km' => '4',
'200 km' => '5'
)
),
'region' => array(
'name' => 'Région',
'type' => 'list',
'values' => array(
'' => '',
'Auvergne-Rhône-Alpes' => 'FR-ARA',
'Bourgogne-Franche-Comté' => 'FR-BFC',
'Bretagne' => 'FR-BRE',
'Centre-Val de Loire' => 'FR-CVL',
'Corse' => 'FR-COR',
'Grand Est' => 'FR-GES',
'Hauts-de-France' => 'FR-HDF',
'Île-de-France' => 'FR-IDF',
'Normandie' => 'FR-NOR',
'Nouvelle-Aquitaine' => 'FR-PAC',
'Occitanie' => 'FR-PDL',
'Pays de la Loire' => 'FR-OCC',
'Provence-Alpes-Côte d\'Azur' => 'FR-NAQ'
)
),
'mileagemin' => array(
'name' => 'Kilométrage min',
'type' => 'number'
),
'mileagemax' => array(
'name' => 'Kilométrage max',
'type' => 'number'
),
'yearmin' => array(
'name' => 'Année min',
'type' => 'number'
),
'yearmax' => array(
'name' => 'Année max',
'type' => 'number'
),
'cubiccapacitymin' => array(
'name' => 'Cylindrée min',
'type' => 'number'
),
'cubiccapacitymax' => array(
'name' => 'Cylindrée max',
'type' => 'number'
),
'fuel' => array(
'name' => 'Énergie',
'type' => 'list',
'values' => array(
'' => '',
'Diesel' => 'dies',
'Essence' => 'ess',
'Électrique' => 'elec',
'Hybride' => 'hyb',
'GPL' => 'gpl',
'Bioéthanol' => 'eth',
'Autre' => 'alt'
)
),
'gearbox' => array(
'name' => 'Boite de vitesse',
'type' => 'list',
'values' => array(
'' => '',
'Boite automatique' => 'AUTO',
'Boite mécanique' => 'MANUAL'
)
),
'doors' => array(
'name' => 'Nombre de portes',
'type' => 'list',
'values' => array(
'' => '',
'2 portes' => '2',
'3 portes' => '3',
'4 portes' => '4',
'5 portes' => '5',
'6 portes ou plus' => '6'
)
),
'firsthand' => array(
'name' => 'Première main',
'type' => 'checkbox'
),
'seller' => array(
'name' => 'Vendeur',
'type' => 'list',
'values' => array(
'' => '',
'Particulier' => 'PART',
'Professionel' => 'PRO'
)
),
'sort' => array(
'name' => 'Tri',
'type' => 'list',
'values' => array(
'Prix (croissant)' => 'priceAsc',
'Prix (décroissant)' => 'priceDesc',
'Marque (croissant)' => 'makeAsc',
'Marque (décroissant)' => 'makeDesc',
'Kilométrage (croissant)' => 'mileageAsc',
'Kilométrage (décroissant)' => 'mileageDesc',
'Année (croissant)' => 'yearAsc',
'Année (décroissant)' => 'yearDesc',
'Département (croissant)' => 'visitPlaceAsc',
'Département (décroissant)' => 'visitPlaceDesc'
)
),
));
public function collectData(){
// check data
if(!empty($this->getInput('distance'))
&& is_null($this->getInput('location'))) {
returnClientError('You need a place ("CP ou département") to search arround.');
}
$params = array(
'vertical' => $this->getInput('type'),
'makesModelsCommercialNames' => $this->getInput('brand') . ':' . $this->getInput('model'),
'versions' => $this->getInput('versions'),
'categories' => $this->getInput('category'),
'priceMin' => $this->getInput('pricemin'),
'priceMax' => $this->getInput('pricemax'),
'dptCp' => $this->getInput('location'),
'distance' => $this->getInput('distance'),
'regions' => $this->getInput('region'),
'mileageMin' => $this->getInput('mileagemin'),
'mileageMax' => $this->getInput('mileagemax'),
'yearMin' => $this->getInput('yearmin'),
'yearMax' => $this->getInput('yearmax'),
'cubicMin' => $this->getInput('cubiccapacitymin'),
'cubicMax' => $this->getInput('cubiccapacitymax'),
'energies' => $this->getInput('fuel'),
'firstHand' => $this->getInput('firsthand') ? 'true' : 'false',
'gearbox' => $this->getInput('gearbox'),
'doors' => $this->getInput('doors'),
'sortBy' => $this->getInput('sort')
);
$url = self::URI . 'listing?' . http_build_query($params);
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request LaCentrale.');
foreach($html->find('.linkAd') as $element) {
$item = array();
$item['uri'] = trim(self::URI, '/') . $element->href;
$item['title'] = $element->find('.brandModel', 0)->plaintext;
$item['sellerType'] = $element->find('.typeSeller', 0)->plaintext;
$item['author'] = $item['sellerType'];
$item['version'] = $element->find('.version', 0)->plaintext;
$item['price'] = $element->find('.fieldPrice', 0)->plaintext;
$item['year'] = $element->find('.fieldYear', 0)->plaintext;
$item['mileage'] = $element->find('.fieldMileage', 0)->plaintext;
$item['departement'] = str_replace(',', '', $element->find('.dptCont', 0)->plaintext);
$item['thumbnail'] = $element->find('.imgContent img', 0)->src;
$item['enclosures'] = array($item['thumbnail']);
$item['content'] = '
<img src="' . $item['thumbnail'] . '">
<br>Variation : ' . $item['version']
. '<br>Prix : ' . $item['price']
. '<br>Année : ' . $item['year']
. '<br>Kilométrage : ' . $item['mileage']
. '<br>Département : ' . $item['departement']
. '<br>Type de vendeur : ' . $item['sellerType'];
$this->items[] = $item;
}
}
}

View File

@@ -356,6 +356,7 @@ class LeBonCoinBridge extends BridgeAbstract {
$data = $this->buildRequestJson();
$header = array(
'User-Agent: LBC;Android;Null;Null;Null;Null;Null;Null;Null;Null',
'Content-Type: application/json',
'Content-Length: ' . strlen($data),
'api_key: ' . self::$LBC_API_KEY

26
bridges/NYTBridge.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
class NYTBridge extends FeedExpander {
const MAINTAINER = 'IceWreck';
const NAME = 'New York Times Bridge';
const URI = 'https://www.nytimes.com/';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'RSS feed for the New York Times';
public function collectData(){
$this->collectExpandableDatas('https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', 15);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
// $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link);
// figure contain's the main article image
$article = $articlePage->find('figure', 0);
// p > css-exrw3m has the actual article
foreach($articlePage->find('p.css-exrw3m') as $element)
$article = $article . $element;
$item['content'] = $article;
return $item;
}
}

View File

@@ -3,7 +3,7 @@ class NovelUpdatesBridge extends BridgeAbstract {
const MAINTAINER = 'albirew';
const NAME = 'Novel Updates';
const URI = 'http://www.novelupdates.com/';
const URI = 'https://www.novelupdates.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns releases from Novel Updates';
const PARAMETERS = array( array(

203
bridges/PatreonBridge.php Normal file
View File

@@ -0,0 +1,203 @@
<?php
class PatreonBridge extends BridgeAbstract {
const NAME = 'Patreon Bridge';
const URI = 'https://www.patreon.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Returns posts by creators on Patreon';
const MAINTAINER = 'Roliga';
const PARAMETERS = array( array(
'creator' => array(
'name' => 'Creator',
'type' => 'text',
'required' => true,
'title' => 'Creator name as seen in their page URL'
)
));
public function collectData(){
$html = getSimpleHTMLDOMCached($this->getURI(), 86400)
or returnServerError('Failed to load creator page at ' . $this->getURI());
$regex = '#/api/campaigns/([0-9]+)#';
if(preg_match($regex, $html->save(), $matches) > 0) {
$campaign_id = $matches[1];
} else {
returnServerError('Could not find campaign ID');
}
$query = array(
'include' => implode(',', array(
'user',
'attachments',
'user_defined_tags',
//'campaign',
//'poll.choices',
//'poll.current_user_responses.user',
//'poll.current_user_responses.choice',
//'poll.current_user_responses.poll',
//'access_rules.tier.null',
//'images.null',
//'audio.null'
)),
'fields' => array(
'post' => implode(',', array(
//'change_visibility_at',
//'comment_count',
'content',
//'current_user_can_delete',
//'current_user_can_view',
//'current_user_has_liked',
//'embed',
'image',
//'is_paid',
//'like_count',
//'min_cents_pledged_to_view',
//'patreon_url',
//'patron_count',
//'pledge_url',
//'post_file',
//'post_metadata',
//'post_type',
'published_at',
'teaser_text',
//'thumbnail_url',
'title',
//'upgrade_url',
'url',
//'was_posted_by_campaign_owner'
)),
'user' => implode(',', array(
//'image_url',
'full_name',
//'url'
))
),
'filter' => array(
'contains_exclusive_posts' => true,
'is_draft' => false,
'campaign_id' => $campaign_id
),
'sort' => '-published_at'
);
$posts = $this->apiGet('posts', $query);
foreach($posts->data as $post) {
$item = array(
'uri' => $post->attributes->url,
'title' => $post->attributes->title,
'timestamp' => $post->attributes->published_at,
'content' => '',
'uid' => 'patreon.com/' . $post->id
);
$user = $this->findInclude($posts,
'user',
$post->relationships->user->data->id);
$item['author'] = $user->full_name;
if(isset($post->attributes->image))
$item['content'] .= '<p><a href="'
. $post->attributes->url
. '"><img src="'
. $post->attributes->image->thumb_url
. '" /></a></p>';
if(isset($post->attributes->content)) {
$item['content'] .= $post->attributes->content;
} elseif (isset($post->attributes->teaser_text)) {
$item['content'] .= '<p>'
. $post->attributes->teaser_text
. '</p>';
}
if(isset($post->relationships->user_defined_tags)) {
$item['categories'] = array();
foreach($post->relationships->user_defined_tags->data as $tag) {
$attrs = $this->findInclude($posts, 'post_tag', $tag->id);
$item['categories'][] = $attrs->value;
}
}
if(isset($post->relationships->attachments)) {
$item['enclosures'] = array();
foreach($post->relationships->attachments->data as $attachment) {
$attrs = $this->findInclude($posts, 'attachment', $attachment->id);
$item['enclosures'][] = $attrs->url;
}
}
$this->items[] = $item;
}
}
/*
* Searches the "included" array in an API response and returns attributes
* for the first match.
*/
private function findInclude($data, $type, $id) {
foreach($data->included as $include)
if($include->type === $type && $include->id === $id)
return $include->attributes;
}
private function apiGet($endpoint, $query_data = array()) {
$query_data['json-api-version'] = 1.0;
$query_data['json-api-use-default-includes'] = 0;
$url = 'https://www.patreon.com/api/'
. $endpoint
. '?'
. http_build_query($query_data);
/*
* Accept-Language header and the CURL cipher list are for bypassing the
* Cloudflare anti-bot protection on the Patreon API. If this ever breaks,
* here are some other project that also deal with this:
* https://github.com/mikf/gallery-dl/issues/342
* https://github.com/daemionfox/patreon-feed/issues/7
* https://www.patreondevelopers.com/t/api-returning-cloudflare-challenge/2025
* https://github.com/splitbrain/patreon-rss/issues/4
*/
$header = array(
'Accept-Language: en-US',
'Content-Type: application/json'
);
$opts = array(
CURLOPT_SSL_CIPHER_LIST => implode(':', array(
'DEFAULT',
'!DHE-RSA-CHACHA20-POLY1305'
))
);
$data = json_decode(getContents($url, $header, $opts))
or returnServerError('API request to "' . $url . '" failed.');
return $data;
}
public function getName(){
if(!is_null($this->getInput('creator')))
return $this->getInput('creator') . ' posts';
return parent::getName();
}
public function getURI(){
if(!is_null($this->getInput('creator')))
return self::URI . $this->getInput('creator');
return parent::getURI();
}
public function detectParameters($url){
$params = array();
// Matches e.g. https://www.patreon.com/SomeCreator
$regex = '/^(https?:\/\/)?(www\.)?patreon\.com\/([^\/&?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['creator'] = urldecode($matches[3]);
return $params;
}
return null;
}
}

View File

@@ -32,6 +32,13 @@ class PikabuBridge extends BridgeAbstract {
'required' => true
),
'filter' => self::PARAMETERS_FILTER
),
'По пользователю' => array(
'user' => array(
'name' => 'Пользователь',
'exampleValue' => 'admin',
'required' => true
)
)
);
@@ -40,6 +47,8 @@ class PikabuBridge extends BridgeAbstract {
public function getURI() {
if ($this->getInput('tag')) {
return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter'));
} else if ($this->getInput('user')) {
return self::URI . '/@' . rawurlencode($this->getInput('user'));
} else if ($this->getInput('community')) {
$uri = self::URI . '/community/' . rawurlencode($this->getInput('community'));
if ($this->getInput('filter') != 'hot') {

View File

@@ -0,0 +1,88 @@
<?php
class PirateCommunityBridge extends BridgeAbstract {
const NAME = 'Pirate-Community Bridge';
const URI = 'https://raymanpc.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Returns replies to topics';
const MAINTAINER = 'Roliga';
const PARAMETERS = array( array(
't' => array(
'name' => 'Topic ID',
'type' => 'number',
'title' => 'Topic ID from topic URL. If the URL contains t=12 the ID is 12.',
'required' => true
)));
private $feedName = '';
public function detectParameters($url){
$parsed_url = parse_url($url);
if($parsed_url['host'] !== 'raymanpc.com')
return null;
parse_str($parsed_url['query'], $parsed_query);
if($parsed_url['path'] === '/forum/viewtopic.php'
&& array_key_exists('t', $parsed_query)) {
return array('t' => $parsed_query['t']);
}
return null;
}
public function getName() {
if(!empty($this->feedName))
return $this->feedName;
return parent::getName();
}
public function getURI(){
if(!is_null($this->getInput('t'))) {
return self::URI
. 'forum/viewtopic.php?t='
. $this->getInput('t')
. '&sd=d'; // sort posts decending by ate so first page has latest posts
}
return parent::getURI();
}
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not retrieve topic page at ' . $this->getURI());
$this->feedName = $html->find('head title', 0)->plaintext;
foreach($html->find('.post') as $reply) {
$item = array();
$item['uri'] = $this->getURI()
. $reply->find('h3 a', 0)->getAttribute('href');
$item['title'] = $reply->find('h3 a', 0)->plaintext;
$author_html = $reply->find('.author', 0);
// author_html contains the timestamp as text directly inside it,
// so delete all other child elements
foreach($author_html->children as $child)
$child->outertext = '';
// Timestamps are always in UTC+1
$item['timestamp'] = trim($author_html->innertext) . ' +01:00';
$item['author'] = $reply
->find('.username, .username-coloured', 0)
->plaintext;
$item['content'] = defaultLinkTo($reply->find('.content', 0)->innertext,
$this->getURI());
$item['enclosures'] = array();
foreach($reply->find('.attachbox img.postimage') as $img)
$item['enclosures'][] = urljoin($this->getURI(), $img->src);
$this->items[] = $item;
}
}
}

View File

@@ -7,4 +7,21 @@ class Rule34pahealBridge extends Shimmie2Bridge {
const NAME = 'Rule34paheal';
const URI = 'http://rule34.paheal.net/';
const DESCRIPTION = 'Returns images from given page';
protected function getItemFromElement($element){
$item = array();
$item['uri'] = $this->getURI() . $element->href;
$item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src;
$item['tags'] = $element->getAttribute('data-tags');
$item['title'] = $this->getName() . ' | ' . $item['id'];
$item['content'] = '<a href="'
. $item['uri']
. '"><img src="'
. $thumbnailUri
. '" /></a><br>Tags: '
. $item['tags'];
return $item;
}
}

View File

@@ -0,0 +1,47 @@
<?php
class StoriesIGBridge extends BridgeAbstract {
const NAME = 'Instagram Stories';
const URI = 'https://storiesig.com';
const DESCRIPTION = 'Display Instagram Stories';
const MAINTAINER = 'antoineturmel';
const PARAMETERS = array(
array(
'username' => array(
'name' => 'Instagram username',
'type' => 'text',
'required' => true,
'title' => 'Insert the username here'
),
)
);
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Failed to receive ' . $this->getURI());
$results = $html->find('article');
foreach($results as $result) {
$item = array();
$item['title'] = $this->getInput('username') . ' story';
$item['uri'] = $result->find('div.download', 0)->find('a', 0)->href;
$item['author'] = $this->getInput('username');
$item['uid'] = $result->find('time', 0)->datetime;
$item['content'] = $result;
$this->items[] = $item;
}
}
public function getURI(){
$uri = self::URI . '/stories/';
$uri .= urlencode($this->getInput('username'));
return $uri;
return parent::getURI();
}
}

View File

@@ -0,0 +1,96 @@
<?php
class TheGuardianBridge extends FeedExpander {
const MAINTAINER = 'IceWreck';
const NAME = 'The Guardian Bridge';
const URI = 'https://www.theguardian.com/';
const CACHE_TIMEOUT = 600; // This is a news site, so don't cache for more than 10 mins
const DESCRIPTION = 'RSS feed for The Guardian';
const PARAMETERS = array( array(
'feed' => array(
'name' => 'Feed',
'type' => 'list',
'values' => array(
'World News' => 'world/rss',
'US News' => '/us-news/rss',
'UK News' => '/uk-news/rss',
'Europe News' => '/world/europe-news/rss',
'Asia News' => '/world/asia/rss',
'Tech' => '/uk/technology/rss',
'Business News' => '/uk/business/rss',
'Opinion' => '/uk/commentisfree/rss',
'Lifestyle' => '/uk/lifeandstyle/rss',
'Culture' => '/uk/culture/rss',
'Sports' => '/uk/sport/rss'
)
)
/*
Topicwise Links
You can find the base feed for any topic by appending /rss to the url.
Example:
https://feeds.theguardian.com/theguardian/uk-news/rss
https://feeds.theguardian.com/theguardian/us-news/rss
Or simply
https://www.theguardian.com/world/rss
Just add that topic as a value in the PARAMETERS const.
*/
));
public function collectData(){
$feed = $this->getInput('feed');
$feedURL = 'https://feeds.theguardian.com/theguardian/' . $feed;
$this->collectExpandableDatas($feedURL, 10);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
// --- Recovering the article ---
// $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link);
// figure contain's the main article image
$article = $articlePage->find('figure', 0);
// content__article-body has the actual article
foreach($articlePage->find('.content__article-body') as $element)
$article = $article . $element;
// --- Fixing ugly elements ---
// Replace the image viewer and BS with the image itself
foreach($articlePage->find('a.article__img-container') as $uslElementLoc) {
$main_img = $uslElementLoc->find('img', 0);
$article = str_replace($uslElementLoc, $main_img, $article);
}
// List of all the crap in the article
$uselessElements = array(
'#show-caption',
'.element-atom',
'.submeta',
'youtube-media-atom',
'svg'
);
// Remove the listed crap
foreach($uselessElements as $uslElement) {
foreach($articlePage->find($uslElement) as $uslElementLoc) {
$article = str_replace($uslElementLoc, '', $article);
}
}
$item['content'] = $article;
return $item;
}
}

View File

@@ -3,7 +3,7 @@ class ThePirateBayBridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
const NAME = 'The Pirate Bay';
const URI = 'https://thepiratebay.wf/';
const URI = 'https://thepiratebay.org/';
const DESCRIPTION = 'Returns results for the keywords. You can put several
list of keywords by separating them with a semicolon (e.g. "one show;another
show"). Category based search needs the category number as input. User based
@@ -149,11 +149,12 @@ class ThePirateBayBridge extends BridgeAbstract {
|| !is_null($element->find('img[alt=VIP]', 0))
|| !is_null($element->find('img[alt=Trusted]', 0))) {
$item = array();
$item['uri'] = $element->find('a', 3)->href;
$item['uri'] = self::URI . $element->find('a.detLink', 0)->href;
$item['id'] = self::URI . $element->find('a.detLink', 0)->href;
$item['timestamp'] = parseDateTimestamp($element);
$item['author'] = $element->find('a.detDesc', 0)->plaintext;
$item['title'] = $element->find('a.detLink', 0)->plaintext;
$item['magnet'] = $element->find('a', 3)->href;
$item['seeders'] = (int)$element->find('td', 2)->plaintext;
$item['leechers'] = (int)$element->find('td', 3)->plaintext;
$item['content'] = $element->find('font', 0)->plaintext
@@ -163,7 +164,9 @@ class ThePirateBayBridge extends BridgeAbstract {
. $item['leechers']
. '<br><a href="'
. $item['id']
. '">info page</a>';
. '">info page</a><br><a href="'
. $item['magnet']
. '">magnet link</a>';
if(isset($item['title']))
$this->items[] = $item;

202
bridges/TwitchBridge.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
class TwitchBridge extends BridgeAbstract {
const MAINTAINER = 'Roliga';
const NAME = 'Twitch Bridge';
const URI = 'https://twitch.tv/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Twitch channel videos';
const PARAMETERS = array( array(
'channel' => array(
'name' => 'Channel',
'type' => 'text',
'required' => true,
'title' => 'Lowercase channel name as seen in channel URL'
),
'type' => array(
'name' => 'Type',
'type' => 'list',
'values' => array(
'All' => 'all',
'Archive' => 'archive',
'Highlights' => 'highlight',
'Uploads' => 'upload'
),
'defaultValue' => 'archive'
)
));
/*
* Official instructions for obtaining your own client ID can be found here:
* https://dev.twitch.tv/docs/v5/#getting-a-client-id
*/
const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
public function collectData(){
// get channel user
$query_data = array(
'login' => $this->getInput('channel')
);
$users = $this->apiGet('users', $query_data)->users;
if(count($users) === 0)
returnClientError('User "'
. $this->getInput('channel')
. '" could not be found');
$user = $users[0];
// get video list
$query_endpoint = 'channels/' . $user->_id . '/videos';
$query_data = array(
'broadcast_type' => $this->getInput('type'),
'limit' => 10
);
$videos = $this->apiGet($query_endpoint, $query_data)->videos;
foreach($videos as $video) {
$item = array(
'uri' => $video->url,
'title' => $video->title,
'timestamp' => $video->published_at,
'author' => $video->channel->display_name,
);
// Add categories for tags and played game
$item['categories'] = array_filter(explode(' ', $video->tag_list));
if(!empty($video->game))
$item['categories'][] = $video->game;
// Add enclosures for thumbnails from a few points in the video
$item['enclosures'] = array();
foreach($video->thumbnails->large as $thumbnail)
$item['enclosures'][] = $thumbnail->url;
/*
* Content format example:
*
* [Preview Image]
*
* Some optional video description.
*
* Duration: 1:23:45
* Views: 123
*
* Played games:
* * 00:00:00 Game 1
* * 00:12:34 Game 2
*
*/
$item['content'] = '<p><a href="'
. $video->url
. '"><img src="'
. $video->preview->large
. '" /></a></p><p>'
. $video->description_html
. '</p><p><b>Duration:</b> '
. $this->formatTimestampTime($video->length)
. '<br/><b>Views:</b> '
. $video->views
. '</p>';
// Add played games list to content
$video_id = trim($video->_id, 'v'); // _id gives 'v1234' but API wants '1234'
$markers = $this->apiGet('videos/' . $video_id . '/markers')->markers;
$item['content'] .= '<p><b>Played games:</b></b><ul><li><a href="'
. $video->url
. '">00:00:00</a> - '
. $video->game
. '</li>';
if(isset($markers->game_changes)) {
usort($markers->game_changes, function($a, $b) {
return $a->time - $b->time;
});
foreach($markers->game_changes as $game_change) {
$item['categories'][] = $game_change->label;
$item['content'] .= '<li><a href="'
. $video->url
. '?t='
. $this->formatQueryTime($game_change->time)
. '">'
. $this->formatTimestampTime($game_change->time)
. '</a> - '
. $game_change->label
. '</li>';
}
}
$item['content'] .= '</ul></p>';
$this->items[] = $item;
}
}
// e.g. 01:53:27
private function formatTimestampTime($seconds) {
return sprintf('%02d:%02d:%02d',
floor($seconds / 3600),
($seconds / 60) % 60,
$seconds % 60);
}
// e.g. 01h53m27s
private function formatQueryTime($seconds) {
return sprintf('%02dh%02dm%02ds',
floor($seconds / 3600),
($seconds / 60) % 60,
$seconds % 60);
}
/*
* Ideally the new 'helix' API should be used as v5/'kraken' is deprecated.
* The new API however still misses many features (markers, played game..) of
* the old one, so let's use the old one for as long as it's available.
*/
private function apiGet($endpoint, $query_data = array()) {
$query_data['api_version'] = 5;
$url = 'https://api.twitch.tv/kraken/'
. $endpoint
. '?'
. http_build_query($query_data);
$header = array(
'Client-ID: ' . self::CLIENT_ID
);
$data = json_decode(getContents($url, $header))
or returnServerError('API request to "' . $url . '" failed.');
return $data;
}
public function getName(){
if(!is_null($this->getInput('channel'))) {
return $this->getInput('channel') . ' twitch videos';
}
return parent::getName();
}
public function getURI(){
if(!is_null($this->getInput('channel'))) {
return self::URI . $this->getInput('channel');
}
return parent::getURI();
}
public function detectParameters($url){
$params = array();
// Matches e.g. https://www.twitch.tv/someuser/videos?filter=archives
$regex = '/^(https?:\/\/)?
(www\.)?
twitch\.tv\/
([^\/&?\n]+)
\/videos\?.*filter=
(all|archive|highlight|upload)/x';
if(preg_match($regex, $url, $matches) > 0) {
$params['channel'] = urldecode($matches[3]);
$params['type'] = $matches[4];
return $params;
}
return null;
}
}

View File

@@ -170,8 +170,15 @@ EOD
public function collectData(){
$html = '';
$page = $this->getURI();
if(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))) {
$cookies = $this->getCookies($page);
$html = getSimpleHTMLDOM($page, array("Cookie: $cookies"));
} else {
$html = getSimpleHTMLDOM($page, array(), array(CURLOPT_COOKIEFILE => ''));
}
$html = getSimpleHTMLDOM($this->getURI());
if(!$html) {
switch($this->queriedContext) {
case 'By keyword or hashtag':
@@ -428,4 +435,27 @@ EOD;
return null;
}
private function getCookies($pageURL){
$ctx = stream_context_create(array(
'http' => array(
'follow_location' => false
)
)
);
$a = file_get_contents($pageURL, 0, $ctx);
//First request to get the cookie
$cookies = '';
foreach($http_response_header as $hdr) {
if(stripos($hdr, 'Set-Cookie') !== false) {
$cLine = explode(':', $hdr)[1];
$cLine = explode(';', $cLine)[0];
$cookies .= ';' . $cLine;
}
}
return substr($cookies, 2);
}
}

View File

@@ -3,7 +3,7 @@ class UnsplashBridge extends BridgeAbstract {
const MAINTAINER = 'nel50n';
const NAME = 'Unsplash Bridge';
const URI = 'http://unsplash.com/';
const URI = 'https://unsplash.com/';
const CACHE_TIMEOUT = 43200; // 12h
const DESCRIPTION = 'Returns the latests photos from Unsplash';
@@ -27,51 +27,42 @@ class UnsplashBridge extends BridgeAbstract {
public function collectData(){
$width = $this->getInput('w');
$num = 0;
$max = $this->getInput('m');
$quality = $this->getInput('q');
$lastpage = 1;
for($page = 1; $page <= $lastpage; $page++) {
$link = self::URI . '/grid?page=' . $page;
$html = getSimpleHTMLDOM($link)
or returnServerError('No results for this query.');
$api_response = getContents('https://unsplash.com/napi/photos?page=1&per_page=' . $max)
or returnServerError('Could not request Unsplash API.');
$json = json_decode($api_response, true);
if($page === 1) {
preg_match(
'/=(\d+)$/',
$html->find('.pagination > a[!class]', -1)->href,
$matches
);
foreach ($json as $json_item) {
$item = array();
$lastpage = min($matches[1], ceil($max / 40));
// Get image URI
$uri = $json_item['urls']['regular'] . '.jpg'; // '.jpg' only for format hint
$uri = str_replace('q=80', 'q=' . $quality, $uri);
$uri = str_replace('w=1080', 'w=' . $width, $uri);
$item['uri'] = $uri;
// Get title from description
if (is_null($json_item['alt_description'])) {
if (is_null($json_item['description'])) {
$item['title'] = 'Unsplash picture from ' . $json_item['user']['name'];
} else {
$item['title'] = $json_item['description'];
}
} else {
$item['title'] = $json_item['alt_description'];
}
foreach($html->find('.photo') as $element) {
$thumbnail = $element->find('img', 0);
$thumbnail->src = str_replace('https://', 'http://', $thumbnail->src);
$item = array();
$item['uri'] = str_replace(
array('q=75', 'w=400'),
array("q=$quality", "w=$width"),
$thumbnail->src) . '.jpg'; // '.jpg' only for format hint
$item['timestamp'] = time();
$item['title'] = $thumbnail->alt;
$item['content'] = $item['title']
$item['timestamp'] = time();
$item['content'] = $item['title']
. '<br><a href="'
. $item['uri']
. '"><img src="'
. $thumbnail->src
. $json_item['urls']['thumb']
. '" /></a>';
$this->items[] = $item;
$num++;
if ($num >= $max)
break 2;
}
$this->items[] = $item;
}
}
}

102
bridges/WiredBridge.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
class WiredBridge extends FeedExpander {
const MAINTAINER = 'ORelio';
const NAME = 'WIRED Bridge';
const URI = 'https://www.wired.com/';
const DESCRIPTION = 'Returns the newest articles from WIRED';
const PARAMETERS = array( array(
'feed' => array(
'name' => 'Feed',
'type' => 'list',
'values' => array(
'WIRED Top Stories' => 'rss', // /feed/rss
'Business' => 'business', // /feed/category/business/latest/rss
'Culture' => 'culture', // /feed/category/culture/latest/rss
'Gear' => 'gear', // /feed/category/gear/latest/rss
'Ideas' => 'ideas', // /feed/category/ideas/latest/rss
'Science' => 'science', // /feed/category/science/latest/rss
'Security' => 'security', // /feed/category/security/latest/rss
'Transportation' => 'transportation', // /feed/category/transportation/latest/rss
'Backchannel' => 'backchannel', // /feed/category/backchannel/latest/rss
'WIRED Guides' => 'wired-guide', // /feed/tag/wired-guide/latest/rss
'Photo' => 'photo' // /feed/category/photo/latest/rss
)
)
));
public function collectData(){
$feed = $this->getInput('feed');
if(empty($feed) || !ctype_alpha(str_replace('-', '', $feed))) {
returnClientError('Invalid feed, please check the "feed" parameter.');
}
$feed_url = $this->getURI() . 'feed/';
if ($feed != 'rss') {
if ($feed != 'wired-guide') {
$feed_url .= 'category/';
} else {
$feed_url .= 'tag/';
}
$feed_url .= "$feed/latest/";
}
$feed_url .= 'rss';
$this->collectExpandableDatas($feed_url);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
$article = getSimpleHTMLDOMCached($item['uri'])
or returnServerError('Could not request WIRED: ' . $item['uri']);
$item['content'] = $this->extractArticleContent($article);
$headline = strval($newsItem->description);
if(!empty($headline)) {
$item['content'] = '<p><b>' . $headline . '</b></p>' . $item['content'];
}
$item_image = $article->find('meta[property="og:image"]', 0);
if(!empty($item_image)) {
$item['enclosures'] = array($item_image->content);
$item['content'] = '<p><img src="' . $item_image->content . '" /></p>' . $item['content'];
}
return $item;
}
private function extractArticleContent($article){
$content = $article->find('article', 0);
$truncate = true;
if (empty($content)) {
$content = $article->find('div.listicle-main-component__container', 0);
$truncate = false;
}
if (!empty($content)) {
$content = $content->innertext;
}
foreach (array(
'<div class="content-header',
'<div class="mid-banner-wrap',
'<div class="related',
'<div class="social-icons',
'<div class="recirc-most-popular',
'<div class="grid--item article-related-video',
'<div class="row full-bleed-ad',
) as $div_start) {
$content = stripRecursiveHTMLSection($content, 'div', $div_start);
}
if ($truncate) {
//Clutter after standard article is too hard to clean properly
$content = trim(explode('<hr', $content)[0]);
}
$content = str_replace('href="/', 'href="' . $this->getURI() . '/', $content);
return $content;
}
}

View File

@@ -37,7 +37,7 @@ class AtomFormat extends FormatAbstract{
$entries = '';
foreach($this->getItems() as $item) {
$entryTimestamp = $item->getTimestamp();
$entryTitle = $this->xml_encode($item->getTitle());
$entryTitle = $item->getTitle();
$entryContent = $item->getContent();
$entryUri = $item->getURI();
$entryID = '';

View File

@@ -28,7 +28,7 @@ final class Configuration {
*
* @todo Replace this property by a constant.
*/
public static $VERSION = '2019-07-06';
public static $VERSION = '2019-09-12';
/**
* Holds the configuration data.

View File

@@ -56,7 +56,20 @@ function getContents($url, $header = array(), $opts = array()){
// Use file_get_contents if in CLI mode with no root certificates defined
if(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))) {
$data = @file_get_contents($url);
$httpHeaders = '';
foreach ($header as $headerL) {
$httpHeaders .= $headerL . "\r\n";
}
$ctx = stream_context_create(array(
'http' => array(
'header' => $httpHeaders
)
));
$data = @file_get_contents($url, 0, $ctx);
if($data === false) {
$errorCode = 500;