Merge remote-tracking branch 'origin/master' into kt_bridge

This commit is contained in:
Knah Tsaeb 2021-01-29 10:00:29 +01:00
commit aed4f94ba2
133 changed files with 7696 additions and 1026 deletions

35
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Lint
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
phpcs:
runs-on: ubuntu-16.04
strategy:
matrix:
php-versions: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4']
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpcs
- run: phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p
phpcompatibility:
runs-on: ubuntu-16.04
strategy:
matrix:
php-versions: ['5.6', '7.4']
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- run: composer global require dealerdirect/phpcodesniffer-composer-installer
- run: composer global require phpcompatibility/php-compatibility
- run: ~/.composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --warning-severity=0 --extensions=php -p

47
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
phpunit6:
runs-on: ubuntu-16.04
strategy:
matrix:
php-versions: ['7.0', '7.1', '7.2']
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- run: composer global require phpunit/phpunit ^6
- run: phpunit --configuration=phpunit.xml --include-path=lib/
phpunit7:
runs-on: ubuntu-16.04
strategy:
matrix:
php-versions: ['7.1', '7.2', '7.3']
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- run: composer global require phpunit/phpunit ^7
- run: phpunit --configuration=phpunit.xml --include-path=lib/
phpunit8:
runs-on: ubuntu-16.04
strategy:
matrix:
php-versions: ['7.3', '7.4']
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- run: composer global require phpunit/phpunit ^8
- run: phpunit --configuration=phpunit.xml --include-path=lib/

View file

@ -1,46 +0,0 @@
dist: trusty
language: php
install:
- composer global require dealerdirect/phpcodesniffer-composer-installer;
- composer global require phpcompatibility/php-compatibility;
- if [[ "$PHPUNIT" ]]; then
composer global require phpunit/phpunit ^$PHPUNIT;
fi
script:
- phpenv rehash
# Run PHP_CodeSniffer on all versions
- ~/.config/composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
# Check PHP compatibility for the lowest and highest supported version
- if [[ $TRAVIS_PHP_VERSION == "5.6" || $TRAVIS_PHP_VERSION == "7.3" ]]; then
~/.config/composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --extensions=php -p;
fi
# Run unit tests on highest major version
- if [[ ${TRAVIS_PHP_VERSION:0:1} == "7" ]]; then
~/.config/composer/vendor/bin/phpunit --configuration=phpunit.xml --include-path=lib/;
fi
php:
- 7.3
env:
- PHPUNIT=6
- PHPUNIT=7
- PHPUNIT=8
matrix:
fast_finish: true
include:
- php: 5.6
env: PHPUNIT=
- php: 7.0
- php: 7.1
- php: 7.2
allow_failures:
- php: 7.3
env: PHPUNIT=7
- php: 7.3
env: PHPUNIT=8

View file

@ -3,8 +3,13 @@ FROM php:7-apache
ENV APACHE_DOCUMENT_ROOT=/app
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
&& apt-get --yes update && apt-get --yes install libxml2-dev \
&& docker-php-ext-install -j$(nproc) simplexml \
&& apt-get --yes update \
&& apt-get --yes --no-install-recommends install \
zlib1g-dev \
libmemcached-dev \
&& rm -rf /var/lib/apt/lists/* \
&& pecl install memcached \
&& docker-php-ext-enable memcached \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
&& sed -ri -e 's/(MinProtocol\s*=\s*)TLSv1\.2/\1None/' /etc/ssl/openssl.cnf \

View file

@ -65,6 +65,7 @@ RSS-Bridge requires PHP 5.6 or higher with following extensions enabled:
- [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
- [`json`](https://secure.php.net/manual/en/book.json.php)
- [`filter`](https://secure.php.net/manual/en/book.filter.php)
- [`sqlite3`](http://php.net/manual/en/book.sqlite3.php) (only when using SQLiteCache)
Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
@ -119,19 +120,24 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [alexAubin](https://github.com/alexAubin)
* [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel)
* [arnd-s](https://github.com/arnd-s)
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
* [AxorPL](https://github.com/AxorPL)
* [ayacoo](https://github.com/ayacoo)
* [az5he6ch](https://github.com/az5he6ch)
* [azdkj532](https://github.com/azdkj532)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
* [Binnette](https://github.com/Binnette)
* [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel)
* [ckiw](https://github.com/ckiw)
* [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting)
* [couraudt](https://github.com/couraudt)
* [csisoap](https://github.com/csisoap)
* [cyberjacob](https://github.com/cyberjacob)
* [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei)
@ -146,27 +152,36 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [fanch317](https://github.com/fanch317)
* [floviolleau](https://github.com/floviolleau)
* [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
* [ggiessen](https://github.com/ggiessen)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos)
* [gloony](https://github.com/gloony)
* [GregThib](https://github.com/GregThib)
* [griffaurel](https://github.com/griffaurel)
* [Grummfy](https://github.com/Grummfy)
* [gsantner](https://github.com/gsantner)
* [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)
* [jannyba](https://github.com/jannyba)
* [JasonGhent](https://github.com/JasonGhent)
* [jdesgats](https://github.com/jdesgats)
* [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)
* [johnpc](https://github.com/johnpc)
* [joni1993](https://github.com/joni1993)
* [joshcoales](https://github.com/joshcoales)
* [klimplant](https://github.com/klimplant)
* [kolarcz](https://github.com/kolarcz)
* [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc)
* [l1n](https://github.com/l1n)
@ -175,6 +190,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry)
* [Leomaradan](https://github.com/Leomaradan)
* [liamka](https://github.com/liamka)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos)
@ -185,18 +201,25 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [mdemoss](https://github.com/mdemoss)
* [melangue](https://github.com/melangue)
* [metaMMA](https://github.com/metaMMA)
* [mibe](https://github.com/mibe)
* [mightymt](https://github.com/mightymt)
* [mitsukarenai](https://github.com/mitsukarenai)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro)
* [mschwld](https://github.com/mschwld)
* [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [Niehztog](https://github.com/Niehztog)
* [Nono-m0le](https://github.com/Nono-m0le)
* [ObsidianWitch](https://github.com/ObsidianWitch)
* [OliverParoczai](https://github.com/OliverParoczai)
* [oratosquilla-oratoria](https://github.com/oratosquilla-oratoria)
* [Ololbu](https://github.com/Ololbu)
* [ORelio](https://github.com/ORelio)
* [otakuf](https://github.com/otakuf)
* [Park0](https://github.com/Park0)
* [Paroleen](https://github.com/Paroleen)
* [PaulVayssiere](https://github.com/PaulVayssiere)
* [pellaeon](https://github.com/pellaeon)
* [Piranhaplant](https://github.com/Piranhaplant)
@ -206,24 +229,31 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [Pofilo](https://github.com/Pofilo)
* [prysme01](https://github.com/prysme01)
* [quentinus95](https://github.com/quentinus95)
* [RawkBob](https://github.com/RawkBob)
* [regisenguehard](https://github.com/regisenguehard)
* [Riduidel](https://github.com/Riduidel)
* [rogerdc](https://github.com/rogerdc)
* [Roliga](https://github.com/Roliga)
* [ronansalmon](https://github.com/ronansalmon)
* [rremizov](https://github.com/rremizov)
* [sebsauvage](https://github.com/sebsauvage)
* [shutosg](https://github.com/shutosg)
* [Simounet](https://github.com/Simounet)
* [somini](https://github.com/somini)
* [squeek502](https://github.com/squeek502)
* [stjohnjohnson](https://github.com/stjohnjohnson)
* [Strubbl](https://github.com/Strubbl)
* [sublimz](https://github.com/sublimz)
* [sunchaserinfo](https://github.com/sunchaserinfo)
* [SuperSandro2000](https://github.com/SuperSandro2000)
* [sysadminstory](https://github.com/sysadminstory)
* [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene)
* [tgkenney](https://github.com/tgkenney)
* [thefranke](https://github.com/thefranke)
* [ThePadawan](https://github.com/ThePadawan)
* [TheRadialActive](https://github.com/TheRadialActive)
* [theScrabi](https://github.com/theScrabi)
* [TitiTestScalingo](https://github.com/TitiTestScalingo)
* [triatic](https://github.com/triatic)
* [VerifiedJoseph](https://github.com/VerifiedJoseph)
@ -231,6 +261,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [wtuuju](https://github.com/wtuuju)
* [xurxof](https://github.com/xurxof)
* [yardenac](https://github.com/yardenac)
* [ymeister](https://github.com/ymeister)
* [ZeNairolf](https://github.com/ZeNairolf)
Licenses
@ -240,6 +271,7 @@ The source code for RSS-Bridge is [Public Domain](UNLICENSE).
RSS-Bridge uses third party libraries with their own license:
* [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`PHP Simple HTML DOM Parser`](http://simplehtmldom.sourceforge.net/) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT)

View file

@ -131,6 +131,7 @@ class DisplayAction extends ActionAbstract {
try {
$bridge->setDatas($bridge_params);
$bridge->loadConfiguration();
$bridge->collectData();
$items = $bridge->getItems();

View file

@ -0,0 +1,57 @@
<?php
class ASRockNewsBridge extends BridgeAbstract {
const NAME = 'ASRock News Bridge';
const URI = 'https://www.asrock.com';
const DESCRIPTION = 'Returns latest news articles';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array();
const CACHE_TIMEOUT = 3600; // 1 hour
public function collectData() {
$html = getSimpleHTMLDOM(self::URI . '/news/index.asp')
or returnServerError('Could not request: ' . self::URI . '/news/index.asp');
$html = defaultLinkTo($html, self::URI . '/news/');
foreach($html->find('div.inner > a') as $index => $a) {
$item = array();
$articlePath = $a->href;
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT)
or returnServerError('Could not request: ' . $articlePath);
$articlePageHtml = defaultLinkTo($articlePageHtml, self::URI);
$contents = $articlePageHtml->find('div.Contents', 0);
$item['uri'] = $articlePath;
$item['title'] = $contents->find('h5', 0)->innertext;
$contents->find('h5', 0)->outertext = '';
$item['content'] = $contents->innertext;
$item['timestamp'] = $this->extractDate($a->plaintext);
$item['enclosures'][] = $a->find('img', 0)->src;
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
}
private function extractDate($text) {
$dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
$text = trim($text);
if (preg_match($dateRegex, $text, $matches)) {
return $matches[1];
}
return '';
}
}

View file

@ -0,0 +1,54 @@
<?php
class AirBreizhBridge extends BridgeAbstract {
const MAINTAINER = 'fanch317';
const NAME = 'Air Breizh';
const URI = 'https://www.airbreizh.asso.fr/';
const DESCRIPTION = 'Returns newests publications on Air Breizh';
const PARAMETERS = array(
'Publications' => array(
'theme' => array(
'name' => 'Thematique',
'type' => 'list',
'values' => array(
'Tout' => '',
'Rapport d\'activite' => 'rapport-dactivite',
'Etude' => 'etudes',
'Information' => 'information',
'Autres documents' => 'autres-documents',
'Plan Régional de Surveillance de la qualité de lair' => 'prsqa',
'Transport' => 'transport'
)
)
)
);
public function getIcon() {
return 'https://www.airbreizh.asso.fr/voy_content/uploads/2017/11/favicon.png';
}
public function collectData(){
$html = '';
$html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme'))
or returnClientError('No results for this query.');
foreach ($html->find('article') as $article) {
$item = array();
// Title
$item['title'] = $article->find('h2', 0)->plaintext;
// Author
$item['author'] = 'Air Breizh';
// Image
$imagelink = $article->find('.card__image', 0)->find('img', 0)->getAttribute('src');
// Content preview
$item['content'] = '<img src="' . $imagelink . '" />
<br/>'
. $article->find('.card__text', 0)->plaintext;
// URL
$item['uri'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href');
// ID
$item['id'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href');
$this->items[] = $item;
}
}
}

View file

@ -0,0 +1,74 @@
<?php
class AlbionOnlineBridge extends BridgeAbstract {
const NAME = 'Albion Online Changelog';
const MAINTAINER = 'otakuf';
const URI = 'https://albiononline.com';
const DESCRIPTION = 'Returns the changes made to the Albion Online';
const CACHE_TIMEOUT = 3600; // 60min
const PARAMETERS = array( array(
'postcount' => array(
'name' => 'Limit',
'type' => 'number',
'title' => 'Maximum number of items to return',
'defaultValue' => 5,
),
'language' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'English' => 'en',
'Deutsch' => 'de',
'Polski' => 'pl',
'Français' => 'fr',
'Русский' => 'ru',
'Português' => 'pt',
'Español' => 'es',
),
'title' => 'Language of changelog posts',
'defaultValue' => 'en',
),
'full' => array(
'name' => 'Full changelog',
'type' => 'checkbox',
'required' => false,
'title' => 'Enable to receive the full changelog post for each item'
),
));
public function collectData() {
$api = 'https://albiononline.com/';
// Example: https://albiononline.com/en/changelog/1/5
$url = $api . $this->getInput('language') . '/changelog/1/' . $this->getInput('postcount');
$html = getSimpleHTMLDOM($url)
or returnServerError('Unable to get changelog data from "' . $url . '"!');
foreach ($html->find('li') as $data) {
$item = array();
$item['uri'] = self::URI . $data->find('a', 0)->getAttribute('href');
$item['title'] = trim(explode('|', $data->find('span', 0)->plaintext)[0]);
// Time below work only with en lang. Need to think about solution. May be separate request like getFullChangelog, but to english list for all language
//print_r( date_parse_from_format( 'M j, Y' , 'Sep 9, 2020') );
//$item['timestamp'] = $this->extractDate($a->plaintext);
$item['author'] = 'albiononline.com';
if($this->getInput('full')) {
$item['content'] = $this->getFullChangelog($item['uri']);
} else {
//$item['content'] = trim(preg_replace('/\s+/', ' ', $data->find('span', 0)->plaintext));
// Just use title, no info at all or use title and date, see above
$item['content'] = $item['title'];
}
$item['uid'] = hash('sha256', $item['title']);
$this->items[] = $item;
}
}
private function getFullChangelog($url) {
$html = getSimpleHTMLDOMCached($url)
or returnServerError('Unable to load changelog post from "' . $url . '"!');
$html = defaultLinkTo($html, self::URI);
return $html->find('div.small-12.columns', 1)->innertext;
}
}

View file

@ -8,14 +8,25 @@ class AllocineFRBridge extends BridgeAbstract {
const DESCRIPTION = 'Bridge for allocine.fr';
const PARAMETERS = array( array(
'category' => array(
'name' => 'category',
'name' => 'Emission',
'type' => 'list',
'exampleValue' => 'Faux Raccord',
'title' => 'Select your category',
'title' => 'Sélectionner l\'emission',
'values' => array(
'Faux Raccord' => 'faux-raccord',
'Top 5' => 'top-5',
'Tueurs en Séries' => 'tueurs-en-serie'
'Fanzone' => 'fanzone',
'Game In Ciné' => 'game-in-cine',
'Pour la faire courte' => 'pour-la-faire-courte',
'Home Cinéma' => 'home-cinema',
'PILS - Par Ici Les Sorties' => 'pils-par-ici-les-sorties',
'AlloCiné : l\'émission, sur LeStream' => 'allocine-lemission-sur-lestream',
'Give Me Five' => 'give-me-five',
'Aviez-vous remarqué ?' => 'aviez-vous-remarque',
'Et paf, il est mort' => 'et-paf-il-est-mort',
'The Big Fan Theory' => 'the-big-fan-theory',
'Clichés' => 'cliches',
'Complètement...' => 'completement',
'#Fun Facts' => 'fun-facts',
'Origin Story' => 'origin-story',
)
)
));
@ -23,19 +34,30 @@ class AllocineFRBridge extends BridgeAbstract {
public function getURI(){
if(!is_null($this->getInput('category'))) {
switch($this->getInput('category')) {
case 'faux-raccord':
$uri = static::URI . 'video/programme-12284/saison-32180/';
break;
case 'top-5':
$uri = static::URI . 'video/programme-12299/saison-29561/';
break;
case 'tueurs-en-serie':
$uri = static::URI . 'video/programme-12286/saison-22938/';
break;
}
$categories = array(
'faux-raccord' => 'video/programme-12284/saison-37054/',
'fanzone' => 'video/programme-12298/saison-37059/',
'game-in-cine' => 'video/programme-12288/saison-22971/',
'pour-la-faire-courte' => 'video/programme-20960/saison-29678/',
'home-cinema' => 'video/programme-12287/saison-34703/',
'pils-par-ici-les-sorties' => 'video/programme-25789/saison-37253/',
'allocine-lemission-sur-lestream' => 'video/programme-25123/saison-36067/',
'give-me-five' => 'video/programme-21919/saison-34518/',
'aviez-vous-remarque' => 'video/programme-19518/saison-37084/',
'et-paf-il-est-mort' => 'video/programme-25113/saison-36657/',
'the-big-fan-theory' => 'video/programme-20403/saison-37419/',
'cliches' => 'video/programme-24834/saison-35591/',
'completement' => 'video/programme-23859/saison-34102/',
'fun-facts' => 'video/programme-23040/saison-32686/',
'origin-story' => 'video/programme-25667/saison-37041/'
);
return $uri;
$category = $this->getInput('category');
if(array_key_exists($category, $categories)) {
return static::URI . $categories[$category];
} else {
returnClientError('Emission inconnue');
}
}
return parent::getURI();
@ -63,23 +85,23 @@ class AllocineFRBridge extends BridgeAbstract {
self::PARAMETERS[$this->queriedContext]['category']['values']
);
foreach($html->find('.media-meta-list figure.media-meta-fig') as $element) {
foreach($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
$item = array();
$title = $element->find('div.titlebar h3.title a', 0);
$content = trim($element->innertext);
$figCaption = strpos($content, $category);
$title = $element->find('a[class*=meta-title-link]', 0);
$content = trim($element->outertext);
if($figCaption !== false) {
$content = str_replace('src="/', 'src="' . static::URI, $content);
$content = str_replace('href="/', 'href="' . static::URI, $content);
$content = str_replace('src=\'/', 'src=\'' . static::URI, $content);
$content = str_replace('href=\'/', 'href=\'' . static::URI, $content);
$item['content'] = $content;
$item['title'] = trim($title->innertext);
$item['uri'] = static::URI . $title->href;
$this->items[] = $item;
}
// Replace image 'src' with the one in 'data-src'
$content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content);
$content = preg_replace('@data-src=@', 'src=', $content);
// Remove date in the content to prevent content update while the video is getting older
$content = preg_replace('@<div class="meta-sub light">.*<span>[^<]*</span>[^<]*</div>@', '', $content);
$item['content'] = $content;
$item['title'] = trim($title->innertext);
$item['uri'] = static::URI . substr($title->href, 1);
$this->items[] = $item;
}
}
}

View file

@ -32,6 +32,7 @@ class AmazonPriceTrackerBridge extends BridgeAbstract {
'Mexico' => 'com.mx',
'Netherlands' => 'nl',
'Spain' => 'es',
'Sweden' => 'se',
'United Kingdom' => 'co.uk',
'United States' => 'com',
),

View file

@ -3,7 +3,9 @@ class AnidexBridge extends BridgeAbstract {
const MAINTAINER = 'ORelio';
const NAME = 'Anidex';
const URI = 'https://anidex.info/';
const URI = 'http://anidex.info/'; // anidex.info has ddos-guard so we need to use anidex.moe
const ALTERNATE_URI = 'https://anidex.moe/'; // anidex.moe returns 301 unless Host is set to anidex.info
const ALTERNATE_HOST = 'anidex.info'; // Correct host for requesting anidex.moe without 301 redirect
const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
const PARAMETERS = array(
array(
@ -108,7 +110,7 @@ class AnidexBridge extends BridgeAbstract {
public function collectData() {
// Build Search URL from user-provided parameters
$search_url = self::URI . '?s=upload_timestamp&o=desc';
$search_url = self::ALTERNATE_URI . '?s=upload_timestamp&o=desc';
foreach (array('id', 'lang_id', 'group_id') as $param_name) {
$param = $this->getInput($param_name);
if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) {
@ -131,8 +133,16 @@ class AnidexBridge extends BridgeAbstract {
$opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h;
}
// We need to use a different Host HTTP header to reach the correct page on ALTERNATE_URI
$headers = array('Host: ' . self::ALTERNATE_HOST);
// The HTTPS certificate presented by anidex.moe is for anidex.info. We need to ignore this.
// As a consequence, the bridge is intentionally marked as insecure by setting self::URI to http://
$opt[CURLOPT_SSL_VERIFYHOST] = 0;
$opt[CURLOPT_SSL_VERIFYPEER] = 0;
// Retrieve torrent listing from search results, which does not contain torrent description
$html = getSimpleHTMLDOM($search_url, array(), $opt)
$html = getSimpleHTMLDOM($search_url, $headers, $opt)
or returnServerError('Could not request Anidex: ' . $search_url);
$links = $html->find('a');
$results = array();
@ -156,10 +166,11 @@ class AnidexBridge extends BridgeAbstract {
if ($torrent_id != 0 && ctype_digit($torrent_id)) {
//Retrieve data for this torrent ID
$item_uri = self::URI . 'torrent/' . $torrent_id;
$item_browse_uri = self::URI . 'torrent/' . $torrent_id;
$item_fetch_uri = self::ALTERNATE_URI . 'torrent/' . $torrent_id;
//Retrieve full description from torrent page
if ($item_html = getSimpleHTMLDOMCached($item_uri)) {
//Retrieve full description from torrent page (cached for 24 hours: 86400 seconds)
if ($item_html = getSimpleHTMLDOMCached($item_fetch_uri, 86400, $headers, $opt)) {
//Retrieve data from page contents
$item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext);
@ -191,7 +202,7 @@ class AnidexBridge extends BridgeAbstract {
//Build and add final item
$item = array();
$item['uri'] = $item_uri;
$item['uri'] = $item_browse_uri;
$item['title'] = $item_title;
$item['author'] = $item_author;
$item['timestamp'] = $item_date;

View file

@ -102,7 +102,6 @@ class AnimeUltimeBridge extends BridgeAbstract {
$item_description = defaultLinkTo($item_description, self::URI);
$item_description = str_replace("\r", '', $item_description);
$item_description = str_replace("\n", '', $item_description);
$item_description = utf8_encode($item_description);
//Build and add final item
$item = array();

View file

@ -20,6 +20,8 @@ class AppleMusicBridge extends BridgeAbstract {
));
const CACHE_TIMEOUT = 21600; // 6 hours
private $title;
public function collectData() {
$url = $this->getInput('url');
$html = getSimpleHTMLDOM($url)
@ -27,6 +29,8 @@ class AppleMusicBridge extends BridgeAbstract {
$imgSize = $this->getInput('imgSize');
$this->title = $html->find('title', 0)->innertext;
// Grab the json data from the page
$html = $html->find('script[id=shoebox-ember-data-store]', 0);
$html = strstr($html, '{');
@ -59,4 +63,8 @@ class AppleMusicBridge extends BridgeAbstract {
return $a['timestamp'] < $b['timestamp'];
});
}
public function getName() {
return $this->title ?: parent::getName();
}
}

View file

@ -1,7 +1,7 @@
<?php
class Arte7Bridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
// const MAINTAINER = 'mitsukarenai';
const NAME = 'Arte +7';
const URI = 'https://www.arte.tv/';
const CACHE_TIMEOUT = 1800; // 30min

View file

@ -2,8 +2,8 @@
class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
const NAME = 'Atmo Nouvelle Aquitaine';
const URI = 'https://www.atmo-nouvelleaquitaine.org/monair/commune/';
const DESCRIPTION = 'Fetches the latest air polution of Bordeaux from Atmo Nouvelle Aquitaine';
const URI = 'https://www.atmo-nouvelleaquitaine.org';
const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo';
const MAINTAINER = 'floviolleau';
const PARAMETERS = array(array(
'cities' => array(
@ -27,7 +27,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
}
public function collectData() {
$uri = self::URI . $this->getInput('cities');
$uri = self::URI . '/monair/commune/' . $this->getInput('cities');
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request ' . $uri);

View file

@ -0,0 +1,58 @@
<?php
class AtmoOccitanieBridge extends BridgeAbstract {
const NAME = 'Atmo Occitanie';
const URI = 'https://www.atmo-occitanie.org/';
const DESCRIPTION = 'Fetches the latest air polution of cities in Occitanie from Atmo';
const MAINTAINER = 'floviolleau';
const PARAMETERS = array(array(
'city' => array(
'name' => 'Ville',
'required' => true
)
));
const CACHE_TIMEOUT = 7200;
public function collectData() {
$uri = self::URI . $this->getInput('city');
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request ' . $uri);
$generalMessage = $html->find('.landing-ville .city-banner .iqa-avertissement', 0)->innertext;
$recommendationsDom = $html->find('.landing-ville .recommandations', 0);
$recommendationsItemDom = $recommendationsDom->find('.recommandation-item .label');
$recommendationsMessage = '';
$i = 0;
$len = count($recommendationsItemDom);
foreach ($recommendationsItemDom as $key => $value) {
if ($i == 0) {
$recommendationsMessage .= trim($value->innertext) . '.';
} else {
$recommendationsMessage .= ' ' . trim($value->innertext) . '.';
}
$i++;
}
$lastRecommendationsDom = $recommendationsDom->find('.col-md-6', -1);
$informationHeaderMessage = $lastRecommendationsDom->find('.heading', 0)->innertext;
$indice = $lastRecommendationsDom->find('.current-indice .indice div', 0)->innertext;
$informationDescriptionMessage = $lastRecommendationsDom->find('.current-indice .description p', 0)->innertext;
$message = "$generalMessage L'indice est de $indice/10. $informationDescriptionMessage. $recommendationsMessage";
$city = $this->getInput('city');
$item['uri'] = $uri;
$today = date('d/m/Y');
$item['title'] = "Bulletin de l'air du $today pour la ville : $city.";
//$item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-occitanie.org #QualiteAir. ' . $message;
$item['title'] .= ' #QualiteAir. ' . $message;
$item['author'] = 'floviolleau';
$item['content'] = $message;
$item['uid'] = hash('sha256', $item['title']);
$this->items[] = $item;
}
}

View file

@ -77,110 +77,69 @@ class AutoJMBridge extends BridgeAbstract {
$model_url = self::URI . $this->getInput('url');
// Get the session cookies and the form token
$this->getInitialParameters($model_url);
// Build the GET data
$get_data = 'form[energy]=' . $this->getInput('energy') .
'&form[transmission]=' . $this->getInput('transmission') .
'&form[priceMin]=' . $this->getInput('priceMin') .
'&form[priceMin]=' . $this->getInput('priceMin');
// Build the form
$post_data = array(
'form[energy]' => $this->getInput('energy'),
'form[transmission]' => $this->getInput('transmission'),
'form[priceMin]' => $this->getInput('priceMin'),
'form[priceMin]' => $this->getInput('priceMin'),
'form[_token]' => $this->token
);
// Set the Form request content type
// Set the header 'X-Requested-With' like the website does it
$header = array(
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
);
// Set the curl options (POST query and content, and session cookies
$curl_opts = array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($post_data),
CURLOPT_COOKIE => $this->cookies
'X-Requested-With: XMLHttpRequest'
);
// Get the JSON content of the form
$json = getContents($model_url, $header, $curl_opts)
$json = getContents($model_url . '?' . $get_data, $header)
or returnServerError('Could not request AutoJM.');
// Extract the HTML content from the JSON result
$data = json_decode($json);
$html = str_get_html($data->content);
$html = str_get_html($data->results);
// Go through every finisha of the model
$list = $html->find('h3');
foreach ($list as $finish) {
$finish_name = $finish->plaintext;
$motorizations = $finish->next_sibling()->find('li');
foreach ($motorizations as $element) {
$image = $element->find('div[class=block-product-image]', 0)->{'data-ga-banner'};
$serie = $element->find('span[class=model]', 0)->plaintext;
$url = self::URI . substr($element->find('a', 0)->href, 1);
if ($element->find('span[class*=block-product-nbModel]', 0) != null) {
$availability = 'En Stock';
} else {
$availability = 'Sur commande';
}
$discount_html = $element->find('span[class*=tag--promo]', 0);
if ($discount_html != null) {
$discount = $discount_html->plaintext;
} else {
$discount = 'inconnue';
}
$price = $element->find('span[class=price red h1]', 0)->plaintext;
$item = array();
$item['title'] = $finish_name . ' ' . $serie;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
. $finish_name . ' ' . $serie . '</p>';
$item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>';
$item['content'] .= '<li>Série : ' . $serie . '</li>';
$item['content'] .= '<li>Remise : ' . $discount . '</li>';
$item['content'] .= '<li>Prix : ' . $price . '</li></ul>';
// Go through every car of the model
$list = $html->find('div[class=car-card]');
foreach ($list as $car) {
// Add a fictionnal anchor to the RSS element URL, based on the item content ;
// As the URL could be identical even if the price change, some RSS reader will not show those offers as new items
$item['uri'] = $url . '#' . md5($item['content']);
$this->items[] = $item;
// Get the Finish name if this car is the first of a new finish
$prev_tag = $car->prev_sibling();
if($prev_tag->tag == 'div' && $prev_tag->class == 'results-title') {
$finish_name = $prev_tag->plaintext;
}
}
}
/**
* Gets the session cookie and the form token
*
* @param string $pageURL The URL from which to get the values
*/
private function getInitialParameters($pageURL) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $pageURL);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($ch);
// Separate the response header and the content
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($data, 0, $headerSize);
$content = substr($data, $headerSize);
curl_close($ch);
// Extract the cookies from the headers
$cookies = '';
$http_response_header = explode("\r\n", $header);
foreach ($http_response_header as $hdr) {
if (strpos($hdr, 'Set-Cookie') !== false) {
$cLine = explode(':', $hdr)[1];
$cLine = explode(';', $cLine)[0];
$cookies .= ';' . $cLine;
// Get the info about the car offer
$image = $car->find('div[class=car-card__visual]', 0)->find('img', 0)->src;
$serie = $car->find('div[class=car-card__title]', 0)->plaintext;
$url = $car->find('a', 0)->href;
// Check if the car model is in stock or available only on order
if($car->find('span[class*=tag--dispo]', 0) != null) {
$availability = 'En Stock';
} else {
$availability = 'Sur commande';
}
}
$this->cookies = trim(substr($cookies, 1));
$discount_html = $car->find('span[class=promo]', 0);
// Check if there is any discount dsiplayed
if ($discount_html != null) {
$discount = $discount_html->plaintext;
} else {
$discount = 'inconnue';
}
$price = $car->find('span[class=price]', 0)->plaintext;
// Get the token from the content
$html = str_get_html($content);
$token = $html->find('input[type=hidden][id=form__token]', 0);
$this->token = $token->value;
// Construct the new item
$item = array();
$item['title'] = $finish_name . ' ' . $serie;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
. $finish_name . ' ' . $serie . '</p>';
$item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>';
$item['content'] .= '<li>Série : ' . $serie . '</li>';
$item['content'] .= '<li>Remise : ' . $discount . '</li>';
$item['content'] .= '<li>Prix : ' . $price . '</li></ul>';
// Add a fictionnal anchor to the RSS element URL, based on the item content ;
// As the URL could be identical even if the price change, some RSS reader will not show those offers as new items
$item['uri'] = $url . '#' . md5($item['content']);
$this->items[] = $item;
}
}
}

View file

@ -0,0 +1,55 @@
<?php
class AwwwardsBridge extends BridgeAbstract {
const NAME = 'Awwwards';
const URI = 'https://www.awwwards.com/';
const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards';
const MAINTAINER = 'Paroleen';
const CACHE_TIMEOUT = 3600;
const SITESURI = 'https://www.awwwards.com/websites/sites_of_the_day/';
const SITEURI = 'https://www.awwwards.com/sites/';
const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/';
private $sites = array();
public function getIcon() {
return 'https://www.awwwards.com/favicon.ico';
}
private function fetchSites() {
Debug::log('Fetching all sites');
$sites = getSimpleHTMLDOM(self::SITESURI)
or returnServerError('Could not fetch JSON for sites.');
Debug::log('Parsing all JSON data');
foreach($sites->find('li[data-model]') as $site) {
$decode = html_entity_decode($site->attr['data-model'],
ENT_QUOTES, 'utf-8');
$decode = json_decode($decode, true);
$this->sites[] = $decode;
}
}
public function collectData() {
$this->fetchSites();
Debug::log('Building RSS feed');
foreach($this->sites as $site) {
$item = array();
$item['title'] = $site['title'];
$item['timestamp'] = $site['createdAt'];
$item['categories'] = $site['tags'];
$item['content'] = '<img src="'
. self::ASSETSURI
. $site['images']['thumbnail']
. '">';
$item['uri'] = self::SITEURI . $site['slug'];
$this->items[] = $item;
if(count($this->items) >= 10)
break;
}
}
}

View file

@ -19,13 +19,11 @@ class BastaBridge extends BridgeAbstract {
$item['title'] = $element->find('title', 0)->innertext;
$item['uri'] = $element->find('guid', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
// Replaces all relative image URLs by absolute URLs.
// Relative URLs always start with 'local/'!
$item['content'] = preg_replace(
'/src=["\']{1}([^"\']+)/ims',
'src=\'' . self::URI . '$1\'',
getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext
);
$html = getSimpleHTMLDOM($item['uri']);
$html = defaultLinkTo($html, self::URI);
$item['content'] = $html->find('div.texte', 0)->innertext;
$this->items[] = $item;
$limit++;
}

View file

@ -0,0 +1,29 @@
<?php
class BleepingComputerBridge extends FeedExpander {
const MAINTAINER = 'csisoap';
const NAME = 'Bleeping Computer';
const URI = 'https://www.bleepingcomputer.com/';
const DESCRIPTION = 'Returns the newest articles.';
protected function parseItem($item){
$item = parent::parseItem($item);
$article_html = getSimpleHTMLDOMCached($item['uri']);
if(!$article_html) {
$item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
return $item;
}
$article_content = $article_html->find('div.articleBody', 0)->innertext;
$article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="cz-related-article-wrapp');
$item['content'] = trim($article_content);
return $item;
}
public function collectData(){
$feed = static::URI . 'feed/';
$this->collectExpandableDatas($feed);
}
}

View file

@ -0,0 +1,60 @@
<?php
class BlizzardNewsBridge extends XPathAbstract {
const NAME = 'Blizzard News';
const URI = 'https://news.blizzard.com';
const DESCRIPTION = 'Blizzard (game company) newsfeed';
const MAINTAINER = 'Niehztog';
const PARAMETERS = array(
'' => array(
'locale' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'Deutsch' => 'de-de',
'English (EU)' => 'en-gb',
'English (US)' => 'en-us',
'Español (EU)' => 'es-es',
'Español (AL)' => 'es-mx',
'Français' => 'fr-fr',
'Italiano' => 'it-it',
'日本語' => 'ja-jp',
'한국어' => 'ko-kr',
'Polski' => 'pl-pl',
'Português (AL)' => 'pt-br',
'Русский' => 'ru-ru',
'ภาษาไทย' => 'th-th',
'简体中文' => 'zh-cn',
'繁體中文' => 'zh-tw'
),
'defaultValue' => 'en-us',
'title' => 'Select your language'
)
)
);
const CACHE_TIMEOUT = 3600;
const XPATH_EXPRESSION_ITEM = '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article';
const XPATH_EXPRESSION_ITEM_TITLE = './/div/div[2]/h2';
const XPATH_EXPRESSION_ITEM_CONTENT = './/div[@class="ArticleListItem-description"]/div[@class="h6"]';
const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ArticleLink ArticleLink"]/@href';
const XPATH_EXPRESSION_ITEM_AUTHOR = '';
const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp';
const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/div[@class="ArticleListItem-image"]/@style';
const XPATH_EXPRESSION_ITEM_CATEGORIES = './/div[@class="ArticleListItem-label"]';
const SETTING_FIX_ENCODING = true;
/**
* Source Web page URL (should provide either HTML or XML content)
* @return string
*/
protected function getSourceUrl(){
$locale = $this->getInput('locale');
if('zh-cn' === $locale) {
return 'https://cn.news.blizzard.com';
}
return 'https://news.blizzard.com/' . $locale;
}
}

View file

@ -16,6 +16,7 @@ class BrutBridge extends BridgeAbstract {
'Entertainment' => 'entertainment',
'Sports' => 'sport',
'Nature' => 'nature',
'Health' => 'health',
),
'defaultValue' => 'news',
),
@ -26,6 +27,7 @@ class BrutBridge extends BridgeAbstract {
'United States' => 'us',
'United Kingdom' => 'uk',
'France' => 'fr',
'Spain' => 'es',
'India' => 'in',
'Mexico' => 'mx',
),

View file

@ -0,0 +1,84 @@
<?php
class CeskaTelevizeBridge extends BridgeAbstract {
const NAME = 'Česká televize Bridge';
const URI = 'https://www.ceskatelevize.cz';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'Return newest videos';
const MAINTAINER = 'kolarcz';
const PARAMETERS = array(
array(
'url' => array(
'name' => 'url to the show',
'required' => true,
'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/dily/'
)
)
);
private function fixChars($text) {
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
private function getUploadTimeFromString($string) {
if (strpos($string, 'dnes') !== false) {
return strtotime('today');
} elseif (strpos($string, 'včera') !== false) {
return strtotime('yesterday');
} elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) {
returnServerError('Could not get date from Česká televize string');
}
$date = sprintf('%04d-%02d-%02d', isset($match[3]) ? $match[3] : date('Y'), $match[2], $match[1]);
return strtotime($date);
}
public function collectData() {
$url = $this->getInput('url');
$validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(dily\/((nove|vysilani)\/)?)?$/';
if (!preg_match($validUrl, $url, $match)) {
returnServerError('Invalid url');
}
$category = isset($match[4]) ? $match[4] : 'nove';
$fixedUrl = "{$match[1]}dily/{$category}/";
$html = getSimpleHTMLDOM($fixedUrl)
or returnServerError('Could not request Česká televize');
$this->feedUri = $fixedUrl;
$this->feedName = str_replace('Přehled dílů — ', '', $this->fixChars($html->find('title', 0)->plaintext));
if ($category !== 'nove') {
$this->feedName .= " ({$category})";
}
foreach ($html->find('.episodes-broadcast-content a.episode_list_item') as $element) {
$itemTitle = $element->find('.episode_list_item-title', 0);
$itemContent = $element->find('.episode_list_item-desc', 0);
$itemDate = $element->find('.episode_list_item-date', 0);
$itemThumbnail = $element->find('img', 0);
$itemUri = self::URI . $element->getAttribute('href');
$item = array(
'title' => $this->fixChars($itemTitle->plaintext),
'uri' => $itemUri,
'content' => '<img src="https:' . $itemThumbnail->getAttribute('src') . '" /><br />'
. $this->fixChars($itemContent->plaintext),
'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext)
);
$this->items[] = $item;
}
}
public function getURI() {
return isset($this->feedUri) ? $this->feedUri : parent::getURI();
}
public function getName() {
return isset($this->feedName) ? $this->feedName : parent::getName();
}
}

View file

@ -1,28 +0,0 @@
<?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 getIcon() {
return self::URI . 'images/cdrfavicon.png';
}
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;
}
}
}

View file

@ -53,6 +53,8 @@ class DarkReadingBridge extends FeedExpander {
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
if (empty($item['content']))
return null; //ignore dummy articles
$article = getSimpleHTMLDOMCached($item['uri'])
or returnServerError('Could not request Dark Reading: ' . $item['uri']);
$item['content'] = $this->extractArticleContent($article);

View file

@ -0,0 +1,24 @@
<?php
class DaveRamseyBlogBridge extends BridgeAbstract {
const MAINTAINER = 'johnpc';
const NAME = 'Dave Ramsey Blog';
const URI = 'https://www.daveramsey.com/blog';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns blog posts from daveramsey.com';
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request daveramsey.com.');
foreach ($html->find('.Post') as $element) {
$this->items[] = array(
'uri' => 'https://www.daveramsey.com' . $element->find('header > a', 0)->href,
'title' => $element->find('header > h2 > a', 0)->plaintext,
'tags' => $element->find('.Post-topic', 0)->plaintext,
'content' => $element->find('.Post-body', 0)->plaintext,
);
}
}
}

View file

@ -45,29 +45,22 @@ apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png'
}
public function collectData() {
$html = getSimpleHTMLDOMCached($this->getURI())
or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, static::URI);
$articles = $html->find('div[class="single-article"]')
$articles = $html->find('div.crayons-story')
or returnServerError('Could not find articles!');
foreach($articles as $article) {
if($article->find('[class*="cta"]', 0)) { // Skip ads
continue;
}
$item = array();
$item['uri'] = $article->find('a[id*=article-link]', 0)->href;
$item['title'] = $article->find('h3', 0)->plaintext;
$item['title'] = $article->find('h2 > a', 0)->plaintext;
// i.e. "Charlie Harrington・Sep 21"
$item['timestamp'] = strtotime(explode('・', $article->find('h4 a', 0)->plaintext, 2)[1]);
$item['author'] = explode('・', $article->find('h4 a', 0)->plaintext, 2)[0];
$item['timestamp'] = $article->find('time', 0)->datetime;
$item['author'] = $article->find('a.crayons-story__secondary.fw-medium', 0)->plaintext;
// Profile image
$item['enclosures'] = array($article->find('img', 0)->src);
@ -75,7 +68,6 @@ apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png'
if($this->getInput('full')) {
$fullArticle = $this->getFullArticle($item['uri']);
$item['content'] = <<<EOD
<img src="{$item['enclosures'][0]}" alt="{$item['author']}">
<p>{$fullArticle}</p>
EOD;
} else {
@ -85,11 +77,21 @@ EOD;
EOD;
}
$item['categories'] = array_map(function($e){ return $e->plaintext; }, $article->find('div.tags span.tag'));
// categories
foreach ($article->find('a.crayons-tag') as $tag) {
$item['categories'][] = str_replace('#', '', $tag->plaintext);
}
$this->items[] = $item;
}
}
public function getName() {
if (!is_null($this->getInput('tag'))) {
return ucfirst($this->getInput('tag')) . ' - dev.to';
}
return parent::getName();
}
private function getFullArticle($url) {
@ -98,6 +100,10 @@ EOD;
$html = defaultLinkTo($html, static::URI);
if ($html->find('div.crayons-article__cover', 0)) {
return $html->find('div.crayons-article__cover', 0) . $html->find('[id="article-body"]', 0);
}
return $html->find('[id="article-body"]', 0);
}
}

View file

@ -0,0 +1,84 @@
<?php
class DiarioDeNoticiasBridge extends BridgeAbstract {
const NAME = 'Diário de Notícias (PT)';
const URI = 'https://dn.pt';
const DESCRIPTION = 'Diário de Notícias (DN.PT)';
const MAINTAINER = 'somini';
const PARAMETERS = array(
'Tag' => array(
'n' => array(
'name' => 'Tag Name',
'exampleValue' => 'rogerio-casanova',
)
)
);
const MONPT = array(
'jan',
'fev',
'mar',
'abr',
'mai',
'jun',
'jul',
'ago',
'set',
'out',
'nov',
'dez',
);
public function getIcon() {
return 'https://static.globalnoticias.pt/dn/common/images/favicons/favicon-128.png';
}
public function getName() {
switch($this->queriedContext) {
case 'Tag':
$name = self::NAME . ' | Tag | ' . $this->getInput('n');
break;
default:
$name = self::NAME;
}
return $name;
}
public function getURI() {
switch($this->queriedContext) {
case 'Tag':
$url = self::URI . '/tag/' . $this->getInput('n') . '.html';
break;
default:
$url = self::URI;
}
return $url;
}
public function collectData() {
$archives = self::getURI();
$html = getSimpleHTMLDOMCached($archives)
or returnServerError('Could not load content');
foreach($html->find('article') as $element) {
$item = array();
$title = $element->find('.t-am-title', 0);
$link = $element->find('a.t-am-text', 0);
$item['title'] = $title->plaintext;
$item['uri'] = self::URI . $link->href;
$snippet = $element->find('.t-am-lead', 0);
if ($snippet) {
$item['content'] = $snippet->plaintext;
}
preg_match('|edicao-do-dia\\/(?P<day>\d\d)-(?P<monpt>\w\w\w)-(?P<year>\d\d\d\d)|', $link->href, $d);
if ($d) {
$item['timestamp'] = sprintf('%s-%s-%s', $d['year'], array_search($d['monpt'], self::MONPT) + 1, $d['day']);
}
$this->items[] = $item;
}
}
}

123
bridges/DonnonsBridge.php Normal file
View file

@ -0,0 +1,123 @@
<?php
/**
* Retourne les dons d'une recherche filtrée sur le site Donnons.org
* Example: https://donnons.org/Sport/Ile-de-France
*/
class DonnonsBridge extends BridgeAbstract {
const MAINTAINER = 'Binnette';
const NAME = 'Donnons.org';
const URI = 'https://donnons.org';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Retourne les dons depuis le site Donnons.org.';
const PARAMETERS = array(
array(
'q' => array(
'name' => 'Url de recherche',
'required' => true,
'exampleValue' => '/Sport/Ile-de-France',
'pattern' => '\/.*',
'title' => 'Faites une recherche sur le site. Puis copiez ici la fin de lurl. Doit commencer par /',
),
'p' => array(
'name' => 'Nombre de pages à scanner',
'type' => 'number',
'defaultValue' => 5,
'title' => 'Indique le nombre de pages de donnons.org qui seront scannées'
)
)
);
public function collectData() {
$pages = $this->getInput('p');
for($i = 1; $i <= $pages; $i++) {
$this->collectDataByPage($i);
}
}
private function collectDataByPage($page) {
$uri = $this->getPageURI($page);
$html = getSimpleHTMLDOM($uri)
or returnServerError('No results for this query.');
$searchDiv = $html->find('div[id=search]', 0);
if(!is_null($searchDiv)) {
$elements = $searchDiv->find('a.lst-annonce');
foreach($elements as $element) {
$item = array();
// Lien vers le don
$item['uri'] = self::URI . $element->href;
// Id de l'objet
$item['uid'] = $element->getAttribute('data-id');
// Grab info from json
$jsonString = $element->find('script', 0)->innertext;
$json = json_decode($jsonString, true);
$name = $json['name'];
$category = $json['category'];
$date = $json['availabilityStarts'];
$description = $json['description'];
$city = $json['availableAtOrFrom']['address']['addressLocality'];
$region = $json['availableAtOrFrom']['address']['addressRegion'];
// Grab info from HTML
$imageSrc = $element->find('img.ima-center', 0)->getAttribute('data-src');
$image = self::URI . $imageSrc;
$author = $element->find('div.avatar-holder', 0)->plaintext;
$content = '
<img style="margin-right:1em;" src="' . $image . '">
<div>
<h1>' . $name . '</h1>
<p>' . $description . '</p>
<p>Lieu : <b>' . $city . '</b> - ' . $region . '</p>
<p>Par : ' . $author . '</p>
<p>Date : ' . $date . '</p>
</div>
';
// Titre du don
$item['title'] = '[' . $category . '] ' . $name;
$item['timestamp'] = $date;
$item['author'] = $author;
$item['content'] = $content;
$item['enclosures'] = array($image);
$this->items[] = $item;
}
}
}
private function getPageURI($page) {
$uri = $this->getURI();
$haveQueryParams = strpos($uri, '?') !== false;
if($haveQueryParams) {
return $uri . '&page=' . $page;
} else {
return $uri . '?page=' . $page;
}
}
public function getURI() {
if(!is_null($this->getInput('q'))) {
return self::URI . $this->getInput('q');
}
return parent::getURI();
}
public function getName() {
if(!is_null($this->getInput('q'))) {
return 'Donnons.org - ' . $this->getInput('q');
}
return parent::getName();
}
}

View file

@ -6125,9 +6125,16 @@ class DownDetectorBridge extends BridgeAbstract {
$table = $html->find('table.table-striped', 0);
$maxCount = 10;
foreach ($table->find('tr') as $downEvent) {
$downLink = $downEvent->find('td', 1)->find('a', 1);
$item = $this->collectArticleData($downLink->getAttribute('href'));
foreach ($table->find('tr') as $event) {
$td = $event->find('td', 0);
if (is_null($td)) {
continue;
}
$link = $event->find('td', 0)->find('a', 0);
$item = $this->collectArticleData($link->getAttribute('href'));
$this->items[] = $item;
if($maxCount == 0) break;
$maxCount -= 1;

View file

@ -13,7 +13,7 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . '/shots')
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Error while downloading the website content');
$json = $this->loadEmbeddedJsonData($html);
@ -24,19 +24,19 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
$additional_data = $this->findJsonForShot($shot, $json);
if ($additional_data === null) {
$item['uri'] = self::URI . $shot->find('a', 0)->href;
$item['title'] = $shot->find('.dribbble-over strong', 0)->plaintext;
$item['title'] = $shot->find('.shot-title', 0)->plaintext;
} else {
$item['timestamp'] = strtotime($additional_data['published_at']);
$item['uri'] = self::URI . $additional_data['path'];
$item['title'] = $additional_data['title'];
}
$item['author'] = trim($shot->find('.attribution-user a', 0)->plaintext);
$item['author'] = trim($shot->find('.user-information .display-name', 0)->plaintext);
$description = $shot->find('.comment', 0);
$item['content'] = $description === null ? '' : $description->plaintext;
$preview_path = $shot->find('picture source', 0)->attr['srcset'];
$preview_path = $shot->find('figure img', 1)->attr['data-srcset'];
$item['content'] .= $this->getImageTag($preview_path, $item['title']);
$item['enclosures'] = array($this->getFullSizeImagePath($preview_path));
@ -51,10 +51,13 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
foreach($scripts as $script) {
if(strpos($script->innertext, 'newestShots') !== false) {
// fix single quotes
$script->innertext = str_replace('\'', '"', $script->innertext);
$script->innertext = preg_replace('/\'(.*)\'(,?)$/im', '"\1"\2', $script->innertext);
// fix JavaScript JSON (why do they not adhere to the standard?)
$script->innertext = preg_replace('/(\w+):/i', '"\1":', $script->innertext);
$script->innertext = preg_replace('/^(\s*)(\w+):/im', '\1"\2":', $script->innertext);
// fix relative dates, so they are recognized by strtotime
$script->innertext = preg_replace('/"about ([0-9]+ hours? ago)"(,?)$/im', '"\1"\2', $script->innertext);
// find beginning of JSON array
$start = strpos($script->innertext, '[');
@ -83,7 +86,7 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
private function getImageTag($preview_path, $title){
return sprintf(
'<br /> <a href="%s"><img src="%s" alt="%s" /></a>',
'<br /> <a href="%s"><img srcset="%s" alt="%s" /></a>',
$this->getFullSizeImagePath($preview_path),
$preview_path,
$title
@ -91,6 +94,11 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
}
private function getFullSizeImagePath($preview_path){
return str_replace('_1x', '', $preview_path);
// Get last image from srcset
$src_set_urls = explode(',', $preview_path);
$url = end($src_set_urls);
$url = explode(' ', $url)[1];
return htmlspecialchars_decode($url);
}
}

View file

@ -14,17 +14,28 @@ class EconomistBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI . '/latest/')
or returnServerError('Could not fetch latest updates form The Economist.');
foreach($html->find('article') as $element) {
foreach($html->find('div.teaser') as $element) {
$a = $element->find('a.headline-link', 0);
$href = $a->href;
if (substr($href, 0, 4) != 'http')
$href = self::URI . $a->href;
$a = $element->find('a', 0);
$href = self::URI . $a->href;
$full = getSimpleHTMLDOMCached($href);
$article = $full->find('article', 0);
$header = $article->find('span[itemprop="headline"]', 0);
$headerimg = $article->find('div[itemprop="image"]', 0)->find('img', 0);
$author = $article->find('p[itemprop="byline"]', 0);
$time = $article->find('time', 0);
$content = $article->find('div[itemprop="text"]', 0);
$section = array( $article->find('strong[itemprop="articleSection"]', 0)->plaintext );
$header = $article->find('h1', 0);
$author = $article->find('span[itemprop="author"]', 0);
$time = $article->find('time[itemprop="dateCreated"]', 0);
$content = $article->find('div[itemprop="description"]', 0);
// Author
if ($author)
$author = substr($author->innertext, 3, strlen($author));
else
$author = 'The Economist';
// Remove newsletter subscription box
$newsletter = $content->find('div[class="newsletter-form__message"]', 0);
@ -40,19 +51,15 @@ class EconomistBridge extends BridgeAbstract {
if ($nextprev)
$nextprev->outertext = '';
$section = array( $article->find('h3[itemprop="articleSection"]', 0)->plaintext );
$item = array();
$item['title'] = $header->find('span', 0)->innertext . ': '
. $header->find('span', 1)->innertext;
$item['title'] = $header->innertext;
$item['uri'] = $href;
$item['timestamp'] = strtotime($time->datetime);
$item['author'] = $author->innertext;
$item['author'] = $author;
$item['categories'] = $section;
$item['content'] = '<img style="max-width: 100%" src="'
. $a->find('img', 0)->src . '">' . $content->innertext;
. $headerimg->src . '">' . $content->innertext;
$this->items[] = $item;

View file

@ -0,0 +1,93 @@
<?php
class EpicgamesBridge extends BridgeAbstract {
const NAME = 'Epic Games Store News';
const MAINTAINER = 'otakuf';
const URI = 'https://www.epicgames.com';
const DESCRIPTION = 'Returns the latest posts from epicgames.com';
const CACHE_TIMEOUT = 3600; // 60min
const PARAMETERS = array( array(
'postcount' => array(
'name' => 'Limit',
'type' => 'number',
'title' => 'Maximum number of items to return',
'defaultValue' => 10,
),
'language' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'English' => 'en',
'العربية' => 'ar',
'Deutsch' => 'de',
'Español (Spain)' => 'es-ES',
'Español (LA)' => 'es-MX',
'Français' => 'fr',
'Italiano' => 'it',
'日本語' => 'ja',
'한국어' => 'ko',
'Polski' => 'pl',
'Português (Brasil)' => 'pt-BR',
'Русский' => 'ru',
'ไทย' => 'th',
'Türkçe' => 'tr',
'简体中文' => 'zh-CN',
'繁體中文' => 'zh-Hant',
),
'title' => 'Language of blog posts',
'defaultValue' => 'en',
),
));
public function collectData() {
$api = 'https://store-content.ak.epicgames.com/api/';
// Get sticky posts first
// Example: https://store-content.ak.epicgames.com/api/ru/content/blog/sticky?locale=ru
$urlSticky = $api . $this->getInput('language') . '/content/blog/sticky';
// Then get posts
// Example: https://store-content.ak.epicgames.com/api/ru/content/blog?limit=25
$urlBlog = $api . $this->getInput('language') . '/content/blog?limit=' . $this->getInput('postcount');
$dataSticky = getContents($urlSticky)
or returnServerError('Unable to get the sticky posts from epicgames.com!');
$dataBlog = getContents($urlBlog)
or returnServerError('Unable to get the news posts from epicgames.com!');
// Merge data
$decodedData = array_merge(json_decode($dataSticky), json_decode($dataBlog));
foreach($decodedData as $key => $value) {
$item = array();
$item['uri'] = self::URI . $value->url;
$item['title'] = $value->title;
$item['timestamp'] = $value->date;
$item['author'] = 'Epic Games Store';
if(!empty($value->author)) {
$item['author'] = $value->author;
}
if(!empty($value->content)) {
$item['content'] = defaultLinkTo($value->content, self::URI);
}
if(!empty($value->image)) {
$item['enclosures'][] = $value->image;
}
$item['uid'] = $value->_id;
$item['id'] = $value->_id;
$this->items[] = $item;
}
// Sort data
usort($this->items, function ($item1, $item2) {
if ($item2['timestamp'] == $item1['timestamp']) {
return 0;
}
return ($item2['timestamp'] < $item1['timestamp']) ? -1 : 1;
});
// Limit data
$this->items = array_slice($this->items, 0, $this->getInput('postcount'));
}
}

View file

@ -1,7 +1,7 @@
<?php
class ExtremeDownloadBridge extends BridgeAbstract {
const NAME = 'Extreme Download';
const URI = 'https://wvw.extreme-down.xyz/';
const URI = 'https://www.extreme-down.tv/';
const DESCRIPTION = 'Suivi de série sur Extreme Download';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(

67
bridges/FM4Bridge.php Normal file
View file

@ -0,0 +1,67 @@
<?php
class FM4Bridge extends BridgeAbstract
{
const MAINTAINER = 'joni1993';
const NAME = 'FM4 Bridge';
const URI = 'https://fm4.orf.at';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Feed for FM4 articles by tags (authors)';
const PARAMETERS = array(
array(
'tag' => array(
'name' => 'Tag (author, category, ...)',
'title' => 'Tag to retrieve',
'exampleValue' => 'musik'
),
'loadcontent' => array(
'name' => 'Load Full Article Content',
'title' => 'Retrieve full content of articles (may take longer)',
'type' => 'checkbox'
),
'pages' => array(
'name' => 'Pages',
'title' => 'Amount of pages to load',
'type' => 'number',
'defaultValue' => 1
)
)
);
private function getPageData($tag, $page) {
if($tag)
$uri = self::URI . '/tags/' . $tag;
else
$uri = self::URI;
$uri = $uri . '?page=' . $page;
$html = getSimpleHTMLDOM($uri)
or returnServerError('Error while downloading the website content');
$page_items = array();
foreach ($html->find('div[class*=listItem]') as $article) {
$item = array();
$item['uri'] = $article->find('a', 0)->href;
$item['title'] = $article->find('h2', 0)->plaintext;
$item['author'] = $article->find('p[class*=keyword]', 0)->plaintext;
$item['timestamp'] = strtotime($article->find('p[class*=time]', 0)->plaintext);
if ($this->getInput('loadcontent')) {
$item['content'] = getSimpleHTMLDOM($item['uri'])->find('div[class=storyText]', 0)->innertext
or returnServerError('Error while downloading the full article');
}
$page_items[] = $item;
}
return $page_items;
}
public function collectData() {
for ($cur_page = 1; $cur_page <= $this->getInput('pages'); $cur_page++) {
$this->items = array_merge($this->items, $this->getPageData($this->getInput('tag'), $cur_page));
}
}
}

View file

@ -30,7 +30,7 @@ class FacebookBridge extends BridgeAbstract {
'type' => 'checkbox',
'required' => false,
'defaultValue' => false,
'title' => 'Feed includes reviews when checked'
'title' => 'Feed includes reviews when unchecked'
)
),
'Group' => array(
@ -175,7 +175,13 @@ class FacebookBridge extends BridgeAbstract {
$header = array();
}
$html = getSimpleHTMLDOM($this->getURI(), $header)
$touchURI = str_replace(
'https://www.facebook',
'https://touch.facebook',
$this->getURI()
);
$html = getSimpleHTMLDOM($touchURI, $header)
or returnServerError('Failed loading facebook page: ' . $this->getURI());
if(!$this->isPublicGroup($html)) {
@ -186,19 +192,18 @@ class FacebookBridge extends BridgeAbstract {
$this->groupName = $this->extractGroupName($html);
$posts = $html->find('div.userContentWrapper')
$posts = $html->find('div.story_body_container')
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);
$item['uri'] = $this->extractGroupPostURI($post);
$item['title'] = $this->extractGroupPostTitle($post);
$item['author'] = $this->extractGroupPostAuthor($post);
$item['content'] = $this->extractGroupPostContent($post);
$item['enclosures'] = $this->extractGroupPostEnclosures($post);
$this->items[] = $item;
@ -215,16 +220,7 @@ class FacebookBridge extends BridgeAbstract {
$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']
. '"!');
}
$this->validateHost($urlparts['host']);
return explode('/', $urlparts['path'])[2];
@ -236,24 +232,47 @@ class FacebookBridge extends BridgeAbstract {
}
private function validateHost($provided_host) {
// Handle mobile links
if (strpos($provided_host, 'm.') === 0) {
$provided_host = substr($provided_host, strlen('m.'));
}
if (strpos($provided_host, 'touch.') === 0) {
$provided_host = substr($provided_host, strlen('touch.'));
}
$facebook_host = parse_url(self::URI)['host'];
if ($provided_host !== $facebook_host
&& 'www.' . $provided_host !== $facebook_host) {
returnClientError('The host you provided is invalid! Received "'
. $provided_host
. '", expected "'
. $facebook_host
. '"!');
}
}
/**
* @param $html simple_html_dom
* @return bool
*/
private function isPublicGroup($html) {
// Facebook redirects to the groups about page for non-public groups
$about = $html->find('#pagelet_group_about', 0);
return !($about);
// Facebook touch just presents a login page for non-public groups
$title = $html->find('title', 0);
return $title->plaintext !== 'Log in to Facebook | Facebook';
}
private function extractGroupName($html) {
$ogtitle = $html->find('meta[property="og:title"]', 0)
$ogtitle = $html->find('._de1', 0)
or returnServerError('Unable to find group title!');
return html_entity_decode($ogtitle->content, ENT_QUOTES);
return html_entity_decode($ogtitle->plaintext, ENT_QUOTES);
}
private function extractGroupURI($post) {
private function extractGroupPostURI($post) {
$elements = $post->find('a')
or returnServerError('Unable to find URI!');
@ -262,7 +281,8 @@ class FacebookBridge extends BridgeAbstract {
// Find the one that is a permalink
if(strpos($anchor->href, 'permalink') !== false) {
return $anchor->href;
$arr = explode('?', $anchor->href, 2);
return $arr[0];
}
}
@ -271,57 +291,61 @@ class FacebookBridge extends BridgeAbstract {
}
private function extractGroupContent($post) {
private function extractGroupPostContent($post) {
$content = $post->find('div.userContent', 0)
$content = $post->find('div._5rgt', 0)
or returnServerError('Unable to find user content!');
return $content->innertext . $content->next_sibling()->innertext;
$context_text = $content->innertext;
if ($content->next_sibling() !== null) {
$context_text .= $content->next_sibling()->innertext;
}
return $context_text;
}
private function extractGroupTimestamp($post) {
private function extractGroupPostAuthor($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)
$element = $post->find('h3 a', 0)
or returnServerError('Unable to find author information!');
return $element->{'aria-label'};
return $element->plaintext;
}
private function extractGroupEnclosures($post) {
private function extractGroupPostEnclosures($post) {
$elements = $post->find('div.userContent', 0)->next_sibling()->find('img');
$elements = $post->find('span._6qdm');
if ($post->find('div._5rgt', 0)->next_sibling() !== null) {
array_push($elements, ...$post->find('div._5rgt', 0)->next_sibling()->find('i.img'));
}
$enclosures = array();
$background_img_regex = '/background-image: ?url\\((.+?)\\);/';
foreach($elements as $enclosure) {
$enclosures[] = $enclosure->src;
if(preg_match($background_img_regex, $enclosure, $matches) > 0) {
$bg_img_value = trim(html_entity_decode($matches[1], ENT_QUOTES), "'\"");
$bg_img_url = urldecode(preg_replace('/\\\([0-9a-z]{2}) /', '%$1', $bg_img_value));
$enclosures[] = urldecode($bg_img_url);
}
}
return empty($enclosures) ? null : $enclosures;
}
private function extractGroupTitle($post) {
private function extractGroupPostTitle($post) {
$element = $post->find('h5', 0)
$element = $post->find('h3', 0)
or returnServerError('Unable to find title!');
if(strpos($element->plaintext, 'shared') === false) {
$content = strip_tags($this->extractGroupContent($post));
$content = strip_tags($this->extractGroupPostContent($post));
return $this->extractGroupAuthor($post)
return $this->extractGroupPostAuthor($post)
. ' posted: '
. substr(
$content,
@ -348,13 +372,7 @@ class FacebookBridge extends BridgeAbstract {
$urlparts = parse_url($user);
if($urlparts['host'] !== parse_url(self::URI)['host']) {
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
$this->validateHost($urlparts['host']);
if(!array_key_exists('path', $urlparts)
|| $urlparts['path'] === '/') {
@ -555,7 +573,7 @@ EOD;
}
// No captcha? We can carry on retrieving page contents :)
// First, we check wether the page is public or not
// First, we check whether the page is public or not
$loginForm = $html->find('._585r', 0);
if($loginForm != null) {
@ -729,6 +747,7 @@ EOD;
}
}
}
#endregion (User)
}

View file

@ -35,6 +35,8 @@ class FicbookBridge extends BridgeAbstract {
),
);
protected $titleName;
public function getURI() {
switch($this->queriedContext) {
case 'Site News': {
@ -56,6 +58,21 @@ class FicbookBridge extends BridgeAbstract {
}
}
public function getName() {
switch($this->queriedContext) {
case 'Site News': {
return $this->queriedContext . ' | ' . self::NAME;
}
case 'Fiction Updates': {
return $this->titleName . ' | ' . self::NAME;
}
case 'Fiction Comments': {
return $this->titleName . ' | Comments | ' . self::NAME;
}
default: return self::NAME;
}
}
public function collectData() {
$header = array('Accept-Language: en-US');
@ -65,6 +82,10 @@ class FicbookBridge extends BridgeAbstract {
$html = defaultLinkTo($html, self::URI);
if ($this->queriedContext == 'Fiction Updates' or $this->queriedContext == 'Fiction Comments') {
$this->titleName = $html->find('.fanfic-main-info > h1', 0)->innertext;
}
switch($this->queriedContext) {
case 'Site News': return $this->collectSiteNews($html);
case 'Fiction Updates': return $this->collectUpdatesData($html);
@ -84,7 +105,7 @@ class FicbookBridge extends BridgeAbstract {
}
private function collectCommentsData($html) {
foreach($html->find('article.post') as $article) {
foreach($html->find('article.comment-container') as $article) {
$this->items[] = array(
'uri' => $article->find('.comment_link_to_fic > a', 0)->href,
'title' => $article->find('.comment_author', 0)->plaintext,
@ -97,7 +118,7 @@ class FicbookBridge extends BridgeAbstract {
}
private function collectUpdatesData($html) {
foreach($html->find('ul.table-of-contents > li') as $chapter) {
foreach($html->find('ul.list-of-fanfic-parts > li') as $chapter) {
$item = array(
'uri' => $chapter->find('a', 0)->href,
'title' => $chapter->find('a', 0)->plaintext,
@ -130,10 +151,10 @@ class FicbookBridge extends BridgeAbstract {
'июня',
'июля',
'августа',
'Сентября',
'сентября',
'октября',
'Ноября',
'Декабря',
'ноября',
'декабря',
);
$en_month = array(

View file

@ -0,0 +1,50 @@
<?php
class FirstLookMediaTechBridge extends BridgeAbstract {
const NAME = 'First Look Media - Technology';
const URI = 'https://tech.firstlook.media';
const DESCRIPTION = 'First Look Media Technology page';
const MAINTAINER = 'somini';
const PARAMETERS = array(
array(
'projects' => array(
'type' => 'checkbox',
'name' => 'Include Projects?',
)
)
);
public function collectData() {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not load content');
if ($this->getInput('projects')) {
$top_projects = $html->find('.PromoList-ul', 0);
foreach($top_projects->find('li.PromoList-item') as $element) {
$item = array();
$item_uri = $element->find('a', 0);
$item['uri'] = $item_uri->href;
$item['title'] = strip_tags($item_uri->innertext);
$item['content'] = $element->find('div > div', 0);
$this->items[] = $item;
}
}
$top_articles = $html->find('.PromoList-ul', 1);
foreach($top_articles->find('li.PromoList-item') as $element) {
$item = array();
$item_left = $element->find('div > div', 0);
$item_date = $element->find('.PromoList-date', 0);
$item['timestamp'] = strtotime($item_date->innertext);
$item_date->outertext = ''; /* Remove */
$item['author'] = $item_left->innertext;
$item_uri = $element->find('a', 0);
$item['uri'] = self::URI . $item_uri->href;
$item['title'] = strip_tags($item_uri);
$this->items[] = $item;
}
}
}

View file

@ -20,6 +20,27 @@ class FlickrBridge extends BridgeAbstract {
'required' => true,
'title' => 'Insert keyword',
'exampleValue' => 'bird'
),
'media' => array(
'name' => 'Media',
'type' => 'list',
'values' => array(
'All (Photos & videos)' => 'all',
'Photos' => 'photos',
'Videos' => 'videos',
),
'defaultValue' => 'all',
),
'sort' => array(
'name' => 'Sort By',
'type' => 'list',
'values' => array(
'Relevance' => 'relevance',
'Date uploaded' => 'date-posted-desc',
'Date taken' => 'date-taken-desc',
'Interesting' => 'interestingness-desc',
),
'defaultValue' => 'relevance',
)
),
'By username' => array(
@ -29,30 +50,60 @@ class FlickrBridge extends BridgeAbstract {
'required' => true,
'title' => 'Insert username (as shown in the address bar)',
'exampleValue' => 'flickr'
),
'media' => array(
'name' => 'Media',
'type' => 'list',
'values' => array(
'All (Photos & videos)' => 'all',
'Photos' => 'photos',
'Videos' => 'videos',
),
'defaultValue' => 'all',
),
'sort' => array(
'name' => 'Sort By',
'type' => 'list',
'values' => array(
'Relevance' => 'relevance',
'Date uploaded' => 'date-posted-desc',
'Date taken' => 'date-taken-desc',
'Interesting' => 'interestingness-desc',
),
'defaultValue' => 'date-posted-desc',
)
)
);
public function collectData(){
private $username = '';
public function collectData() {
switch($this->queriedContext) {
case 'Explore':
$filter = 'photo-lite-models';
$html = getSimpleHTMLDOM(self::URI . 'explore')
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request Flickr.');
break;
case 'By keyword':
$filter = 'photo-lite-models';
$html = getSimpleHTMLDOM(self::URI . 'search/?q=' . urlencode($this->getInput('q')) . '&s=rec')
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('No results for this query.');
break;
case 'By username':
$filter = 'photo-models';
$html = getSimpleHTMLDOM(self::URI . 'photos/' . urlencode($this->getInput('u')))
//$filter = 'photo-models';
$filter = 'photo-lite-models';
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Requested username can\'t be found.');
$this->username = $this->getInput('u');
if ($html->find('span.search-pill-name', 0)) {
$this->username = $html->find('span.search-pill-name', 0)->plaintext;
}
break;
default:
@ -64,7 +115,6 @@ class FlickrBridge extends BridgeAbstract {
$photo_models = $this->getPhotoModels($model_json, $filter);
foreach($photo_models as $model) {
$item = array();
/* Author name depends on scope. On a keyword search the
@ -72,12 +122,12 @@ class FlickrBridge extends BridgeAbstract {
* the author is part of the owner data.
*/
if(array_key_exists('username', $model)) {
$item['author'] = $model['username'];
$item['author'] = urldecode($model['username']);
} elseif (array_key_exists('owner', reset($model_json)[0])) {
$item['author'] = reset($model_json)[0]['owner']['username'];
$item['author'] = urldecode(reset($model_json)[0]['owner']['username']);
}
$item['title'] = (array_key_exists('title', $model) ? $model['title'] : 'Untitled');
$item['title'] = urldecode((array_key_exists('title', $model) ? $model['title'] : 'Untitled'));
$item['uri'] = self::URI . 'photo.gne?id=' . $model['id'];
$description = (array_key_exists('description', $model) ? $model['description'] : '');
@ -87,7 +137,7 @@ class FlickrBridge extends BridgeAbstract {
. '"><img src="'
. $this->extractContentImage($model)
. '" style="max-width: 640px; max-height: 480px;"/></a><br><p>'
. $description
. urldecode($description)
. '</p>';
$item['enclosures'] = $this->extractEnclosures($model);
@ -98,6 +148,46 @@ class FlickrBridge extends BridgeAbstract {
}
public function getURI() {
switch($this->queriedContext) {
case 'Explore':
return self::URI . 'explore';
break;
case 'By keyword':
return self::URI . 'search/?q=' . urlencode($this->getInput('q'))
. '&sort=' . $this->getInput('sort') . '&media=' . $this->getInput('media');
break;
case 'By username':
return self::URI . 'search/?user_id=' . urlencode($this->getInput('u'))
. '&sort=' . $this->getInput('sort') . '&media=' . $this->getInput('media');
break;
default:
return parent::getURI();
}
}
public function getName() {
switch($this->queriedContext) {
case 'Explore':
return 'Explore - ' . self::NAME;
break;
case 'By keyword':
return $this->getInput('q') . ' - keyword - ' . self::NAME;
break;
case 'By username':
return $this->username . ' - ' . self::NAME;
break;
default:
return parent::getName();
}
return parent::getName();
}
private function extractJsonModel($html) {
// Find SCRIPT containing JSON data

View file

@ -0,0 +1,52 @@
<?php
class FolhaDeSaoPauloBridge extends FeedExpander {
const MAINTAINER = 'somini';
const NAME = 'Folha de São Paulo';
const URI = 'https://www1.folha.uol.com.br';
const DESCRIPTION = 'Returns the newest posts from Folha de São Paulo (full text)';
const PARAMETERS = array(
array(
'feed' => array(
'name' => 'Feed sub-URL',
'type' => 'text',
'title' => 'Select the sub-feed (see https://www1.folha.uol.com.br/feed/)',
'exampleValue' => 'emcimadahora/rss091.xml',
)
)
);
protected function parseItem($item){
$item = parent::parseItem($item);
$articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
if($articleHTMLContent) {
foreach ($articleHTMLContent->find('div.c-news__body .is-hidden') as $toRemove) {
$toRemove->innertext = '';
}
$item_content = $articleHTMLContent->find('div.c-news__body', 0);
if ($item_content) {
$text = $item_content->innertext;
$text = strip_tags($text, '<p><b><a><blockquote><figure><figcaption><img><strong><em>');
$item['content'] = $text;
$item['uri'] = explode('*', $item['uri'])[1];
}
} else {
Debug::log('???: ' . $item['uri']);
}
return $item;
}
public function collectData(){
$feed_input = $this->getInput('feed');
if (substr($feed_input, 0, strlen(self::URI)) === self::URI) {
Debug::log('Input:: ' . $feed_input);
$feed_url = $feed_input;
} else {
/* TODO: prepend `/` if missing */
$feed_url = self::URI . '/' . $this->getInput('feed');
}
Debug::log('URL: ' . $feed_url);
$this->collectExpandableDatas($feed_url);
}
}

View file

@ -96,7 +96,7 @@ class FuturaSciencesBridge extends FeedExpander {
}
private function extractArticleContent($article){
$contents = $article->find('section.article-text-classic', 0)->innertext;
$contents = $article->find('section.article-text', 1)->innertext;
$headline = trim($article->find('p.description', 0)->plaintext);
if(!empty($headline))
$headline = '<p><b>' . $headline . '</b></p>';
@ -129,6 +129,7 @@ class FuturaSciencesBridge extends FeedExpander {
$contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"');
$contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>');
$contents = stripWithDelimiters($contents, '<script ', '</script>');
$contents = stripWithDelimiters($contents, '<script>', '</script>');
return $headline . trim($contents);
}

View file

@ -113,8 +113,8 @@ class GBAtempBridge extends BridgeAbstract {
break;
case 'T':
foreach($html->find('li.portal-tutorial') as $tutorialItem) {
$url = self::URI . $tutorialItem->find('a', 0)->href;
$title = $tutorialItem->find('a', 0)->plaintext;
$url = self::URI . $tutorialItem->find('a', 1)->href;
$title = $tutorialItem->find('a', 1)->plaintext;
$time = $this->findItemDate($tutorialItem);
$author = $tutorialItem->find('a.username', 0)->plaintext;
$content = $this->fetchPostContent($url, self::URI);

View file

@ -0,0 +1,68 @@
<?php
class GenshinImpactBridge extends BridgeAbstract {
const MAINTAINER = 'corenting';
const NAME = 'Genshin Impact';
const URI = 'https://genshin.mihoyo.com/en/news';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'News from the Genshin Impact website';
const PARAMETERS = array(
array(
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'Latest' => 10,
'Info' => 11,
'Updates' => 12,
'Events' => 13
),
'defaultValue' => 10
)
)
);
public function collectData(){
$category = $this->getInput('category');
$url = 'https://genshin.mihoyo.com/content/yuanshen/getContentList';
$url = $url . '?pageSize=3&pageNum=1&channelId=' . $category;
$api_response = getContents($url)
or returnServerError('Error while downloading the website content');
$json_list = json_decode($api_response, true);
foreach($json_list['data']['list'] as $json_item) {
$article_url = 'https://genshin.mihoyo.com/content/yuanshen/getContent';
$article_url = $article_url . '?contentId=' . $json_item['contentId'];
$article_res = getContents($article_url)
or returnServerError('Error while downloading the website content');
$article_json = json_decode($article_res, true);
$item = array();
$item['title'] = $article_json['data']['title'];
$item['timestamp'] = strtotime($article_json['data']['start_time']);
$item['content'] = $article_json['data']['content'];
$item['uri'] = $this->getArticleUri($json_item);
$item['id'] = $json_item['contentId'];
// Picture
foreach($article_json['data']['ext'] as $ext) {
if ($ext['arrtName'] == 'banner' && count($ext['value']) == 1) {
$item['enclosures'] = array($ext['value'][0]['url']);
break;
}
}
$this->items[] = $item;
}
}
public function getIcon() {
return 'https://genshin.mihoyo.com/favicon.ico';
}
private function getArticleUri($json_item) {
return 'https://genshin.mihoyo.com/en/news/detail/' . $json_item['contentId'];
}
}

View file

@ -82,18 +82,21 @@ class GithubIssueBridge extends BridgeAbstract {
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0)->plaintext;
$title .= ' / ' . trim($comment->plaintext);
$content = $title;
if (null !== $comment->nextSibling()) {
$content = $comment->nextSibling()->innertext;
if ($comment->nextSibling()->nodeName() === 'span') {
$content = $comment->nextSibling()->nextSibling()->innertext;
}
$author = $comment->find('.author', 0);
if ($author) {
$author = $author->plaintext;
} else {
$author = '';
}
$title .= ' / '
. trim(str_replace(
array('octicon','-'), array(''),
$comment->find('.octicon', 0)->getAttribute('class')
));
$content = $comment->plaintext;
$item = array();
$item['author'] = $author;
$item['uri'] = $uri;
@ -106,8 +109,7 @@ class GithubIssueBridge extends BridgeAbstract {
}
private function extractIssueComment($issueNbr, $title, $comment){
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->parent->id);
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0)->plaintext;
@ -135,32 +137,20 @@ class GithubIssueBridge extends BridgeAbstract {
substr($issue->find('.gh-header-number', 0)->plaintext, 1)
);
$comments = $issue->find('
[id^="issue-"] > .comment,
[id^="issuecomment-"] > .comment,
[id^="event-"],
[id^="ref-"]
');
$comments = $issue->find(
'.comment, .TimelineItem-badge'
);
foreach($comments as $comment) {
if (!$comment->hasChildNodes()) {
continue;
}
if (!$comment->hasClass('discussion-item-header')) {
if ($comment->hasClass('comment')) {
$comment = $comment->parent;
$item = $this->extractIssueComment($issueNbr, $title, $comment);
$items[] = $item;
continue;
}
while ($comment->hasClass('discussion-item-header')) {
} else {
$comment = $comment->parent;
$item = $this->extractIssueEvent($issueNbr, $title, $comment);
$items[] = $item;
$comment = $comment->nextSibling();
if (null == $comment) {
break;
}
$classes = explode(' ', $comment->getAttribute('class'));
}
}
@ -180,9 +170,9 @@ class GithubIssueBridge extends BridgeAbstract {
case 'Project Issues':
foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
$info = $issue->find('.opened-by', 0);
$issueNbr = substr(
trim($info->plaintext), 1, strpos(trim($info->plaintext), ' ')
);
preg_match('/\/([0-9]+)$/', $issue->find('a', 0)->href, $match);
$issueNbr = $match[1];
$item = array();
$item['content'] = '';

View file

@ -27,16 +27,16 @@ class GithubSearchBridge extends BridgeAbstract {
foreach($html->find('li.repo-list-item') as $element) {
$item = array();
$uri = $element->find('h3 a', 0)->href;
$uri = $element->find('.f4 a', 0)->href;
$uri = substr(self::URI, 0, -1) . $uri;
$item['uri'] = $uri;
$title = $element->find('h3', 0)->plaintext;
$title = $element->find('.f4', 0)->plaintext;
$item['title'] = $title;
// Description
if (count($element->find('p.d-inline-block')) != 0) {
$content = $element->find('p.d-inline-block', 0)->innertext;
if (count($element->find('p.mb-1')) != 0) {
$content = $element->find('p.mb-1', 0)->innertext;
} else{
$content = 'No description';
}

View file

@ -0,0 +1,636 @@
<?php
class GithubTrendingBridge extends BridgeAbstract {
const MAINTAINER = 'liamka';
const NAME = 'Github Trending';
const URI = 'https://github.com/trending';
const URI_ITEM = 'https://github.com';
const CACHE_TIMEOUT = 43200; // 12hr
const DESCRIPTION = 'See what the GitHub community is most excited repos.';
const PARAMETERS = array(
'By language' => array(
'language' => array(
'name' => 'Select language',
'type' => 'list',
'values' => array(
'All languages' => '',
'C++' => 'c++',
'HTML' => 'html',
'Java' => 'java',
'JavaScript' => 'javascript',
'PHP' => 'php',
'Python' => 'python',
'Ruby' => 'ruby',
'Unknown languages' => 'unknown languages',
'1C Enterprise' => '1c enterprise',
'4D' => '4d',
'ABAP' => 'abap',
'ABNF' => 'abnf',
'ActionScript' => 'actionscript',
'Ada' => 'ada',
'Adobe Font Metrics' => 'adobe font metrics',
'Agda' => 'agda',
'AGS Script' => 'ags script',
'Alloy' => 'alloy',
'Alpine Abuild' => 'alpine abuild',
'Altium Designer' => 'altium designer',
'AMPL' => 'ampl',
'AngelScript' => 'angelscript',
'Ant Build System' => 'ant build system',
'ANTLR' => 'antlr',
'ApacheConf' => 'apacheconf',
'Apex' => 'apex',
'API Blueprint' => 'api blueprint',
'APL' => 'apl',
'Apollo Guidance Computer' => 'apollo guidance computer',
'AppleScript' => 'applescript',
'Arc' => 'arc',
'AsciiDoc' => 'asciidoc',
'ASN.1' => 'asn.1',
'ASP' => 'asp',
'AspectJ' => 'aspectj',
'Assembly' => 'assembly',
'Asymptote' => 'asymptote',
'ATS' => 'ats',
'Augeas' => 'augeas',
'AutoHotkey' => 'autohotkey',
'AutoIt' => 'autoit',
'Awk' => 'awk',
'Ballerina' => 'ballerina',
'Batchfile' => 'batchfile',
'Befunge' => 'befunge',
'BibTeX' => 'bibtex',
'Bison' => 'bison',
'BitBake' => 'bitbake',
'Blade' => 'blade',
'BlitzBasic' => 'blitzbasic',
'BlitzMax' => 'blitzmax',
'Bluespec' => 'bluespec',
'Boo' => 'boo',
'Brainfuck' => 'brainfuck',
'Brightscript' => 'brightscript',
'Zeek' => 'zeek',
'C' => 'c',
'C#' => 'c#',
'C++' => 'c++',
'C-ObjDump' => 'c-objdump',
'C2hs Haskell' => 'c2hs haskell',
'Cabal Config' => 'cabal config',
'CartoCSS' => 'cartocss',
'Ceylon' => 'ceylon',
'Chapel' => 'chapel',
'Charity' => 'charity',
'ChucK' => 'chuck',
'Cirru' => 'cirru',
'Clarion' => 'clarion',
'Clean' => 'clean',
'Click' => 'click',
'CLIPS' => 'clips',
'Clojure' => 'clojure',
'Closure Templates' => 'closure templates',
'Cloud Firestore Security Rules' => 'cloud firestore security rules',
'CMake' => 'cmake',
'COBOL' => 'cobol',
'CodeQL' => 'codeql',
'CoffeeScript' => 'coffeescript',
'ColdFusion' => 'coldfusion',
'ColdFusion CFC' => 'coldfusion cfc',
'COLLADA' => 'collada',
'Common Lisp' => 'common lisp',
'Common Workflow Language' => 'common workflow language',
'Component Pascal' => 'component pascal',
'CoNLL-U' => 'conll-u',
'Cool' => 'cool',
'Coq' => 'coq',
'Cpp-ObjDump' => 'cpp-objdump',
'Creole' => 'creole',
'Crystal' => 'crystal',
'CSON' => 'cson',
'Csound' => 'csound',
'Csound Document' => 'csound document',
'Csound Score' => 'csound score',
'CSS' => 'css',
'CSV' => 'csv',
'Cuda' => 'cuda',
'cURL Config' => 'curl config',
'CWeb' => 'cweb',
'Cycript' => 'cycript',
'Cython' => 'cython',
'D' => 'd',
'D-ObjDump' => 'd-objdump',
'Darcs Patch' => 'darcs patch',
'Dart' => 'dart',
'DataWeave' => 'dataweave',
'desktop' => 'desktop',
'Dhall' => 'dhall',
'Diff' => 'diff',
'DIGITAL Command Language' => 'digital command language',
'dircolors' => 'dircolors',
'DirectX 3D File' => 'directx 3d file',
'DM' => 'dm',
'DNS Zone' => 'dns zone',
'Dockerfile' => 'dockerfile',
'Dogescript' => 'dogescript',
'DTrace' => 'dtrace',
'Dylan' => 'dylan',
'E' => 'e',
'Eagle' => 'eagle',
'Easybuild' => 'easybuild',
'EBNF' => 'ebnf',
'eC' => 'ec',
'Ecere Projects' => 'ecere projects',
'ECL' => 'ecl',
'ECLiPSe' => 'eclipse',
'EditorConfig' => 'editorconfig',
'Edje Data Collection' => 'edje data collection',
'edn' => 'edn',
'Eiffel' => 'eiffel',
'EJS' => 'ejs',
'Elixir' => 'elixir',
'Elm' => 'elm',
'Emacs Lisp' => 'emacs lisp',
'EmberScript' => 'emberscript',
'EML' => 'eml',
'EQ' => 'eq',
'Erlang' => 'erlang',
'F#' => 'f#',
'F*' => 'f*',
'Factor' => 'factor',
'Fancy' => 'fancy',
'Fantom' => 'fantom',
'Faust' => 'faust',
'FIGlet Font' => 'figlet font',
'Filebench WML' => 'filebench wml',
'Filterscript' => 'filterscript',
'fish' => 'fish',
'FLUX' => 'flux',
'Formatted' => 'formatted',
'Forth' => 'forth',
'Fortran' => 'fortran',
'FreeMarker' => 'freemarker',
'Frege' => 'frege',
'G-code' => 'g-code',
'Game Maker Language' => 'game maker language',
'GAML' => 'gaml',
'GAMS' => 'gams',
'GAP' => 'gap',
'GCC Machine Description' => 'gcc machine description',
'GDB' => 'gdb',
'GDScript' => 'gdscript',
'Genie' => 'genie',
'Genshi' => 'genshi',
'Gentoo Ebuild' => 'gentoo ebuild',
'Gentoo Eclass' => 'gentoo eclass',
'Gerber Image' => 'gerber image',
'Gettext Catalog' => 'gettext catalog',
'Gherkin' => 'gherkin',
'Git Attributes' => 'git attributes',
'Git Config' => 'git config',
'GLSL' => 'glsl',
'Glyph' => 'glyph',
'Glyph Bitmap Distribution Format' => 'glyph bitmap distribution format',
'GN' => 'gn',
'Gnuplot' => 'gnuplot',
'Go' => 'go',
'Golo' => 'golo',
'Gosu' => 'gosu',
'Grace' => 'grace',
'Gradle' => 'gradle',
'Grammatical Framework' => 'grammatical framework',
'Graph Modeling Language' => 'graph modeling language',
'GraphQL' => 'graphql',
'Graphviz (DOT)' => 'graphviz (dot)',
'Groovy' => 'groovy',
'Groovy Server Pages' => 'groovy server pages',
'Hack' => 'hack',
'Haml' => 'haml',
'Handlebars' => 'handlebars',
'HAProxy' => 'haproxy',
'Harbour' => 'harbour',
'Haskell' => 'haskell',
'Haxe' => 'haxe',
'HCL' => 'hcl',
'HiveQL' => 'hiveql',
'HLSL' => 'hlsl',
'HolyC' => 'holyc',
'HTML' => 'html',
'HTML+Django' => 'html+django',
'HTML+ECR' => 'html+ecr',
'HTML+EEX' => 'html+eex',
'HTML+ERB' => 'html+erb',
'HTML+PHP' => 'html+php',
'HTML+Razor' => 'html+razor',
'HTTP' => 'http',
'HXML' => 'hxml',
'Hy' => 'hy',
'HyPhy' => 'hyphy',
'IDL' => 'idl',
'Idris' => 'idris',
'Ignore List' => 'ignore list',
'IGOR Pro' => 'igor pro',
'Inform 7' => 'inform 7',
'INI' => 'ini',
'Inno Setup' => 'inno setup',
'Io' => 'io',
'Ioke' => 'ioke',
'IRC log' => 'irc log',
'Isabelle' => 'isabelle',
'Isabelle ROOT' => 'isabelle root',
'J' => 'j',
'Jasmin' => 'jasmin',
'Java' => 'java',
'Java Properties' => 'java properties',
'Java Server Pages' => 'java server pages',
'JavaScript' => 'javascript',
'JavaScript+ERB' => 'javascript+erb',
'JFlex' => 'jflex',
'Jison' => 'jison',
'Jison Lex' => 'jison lex',
'Jolie' => 'jolie',
'JSON' => 'json',
'JSON with Comments' => 'json with comments',
'JSON5' => 'json5',
'JSONiq' => 'jsoniq',
'JSONLD' => 'jsonld',
'Jsonnet' => 'jsonnet',
'JSX' => 'jsx',
'Julia' => 'julia',
'Jupyter Notebook' => 'jupyter notebook',
'KiCad Layout' => 'kicad layout',
'KiCad Legacy Layout' => 'kicad legacy layout',
'KiCad Schematic' => 'kicad schematic',
'Kit' => 'kit',
'Kotlin' => 'kotlin',
'KRL' => 'krl',
'LabVIEW' => 'labview',
'Lasso' => 'lasso',
'Latte' => 'latte',
'Lean' => 'lean',
'Less' => 'less',
'Lex' => 'lex',
'LFE' => 'lfe',
'LilyPond' => 'lilypond',
'Limbo' => 'limbo',
'Linker Script' => 'linker script',
'Linux Kernel Module' => 'linux kernel module',
'Liquid' => 'liquid',
'Literate Agda' => 'literate agda',
'Literate CoffeeScript' => 'literate coffeescript',
'Literate Haskell' => 'literate haskell',
'LiveScript' => 'livescript',
'LLVM' => 'llvm',
'Logos' => 'logos',
'Logtalk' => 'logtalk',
'LOLCODE' => 'lolcode',
'LookML' => 'lookml',
'LoomScript' => 'loomscript',
'LSL' => 'lsl',
'LTspice Symbol' => 'ltspice symbol',
'Lua' => 'lua',
'M' => 'm',
'M4' => 'm4',
'M4Sugar' => 'm4sugar',
'Makefile' => 'makefile',
'Mako' => 'mako',
'Markdown' => 'markdown',
'Marko' => 'marko',
'Mask' => 'mask',
'Mathematica' => 'mathematica',
'MATLAB' => 'matlab',
'Maven POM' => 'maven pom',
'Max' => 'max',
'MAXScript' => 'maxscript',
'mcfunction' => 'mcfunction',
'MediaWiki' => 'mediawiki',
'Mercury' => 'mercury',
'Meson' => 'meson',
'Metal' => 'metal',
'Microsoft Developer Studio Project' => 'microsoft developer studio project',
'MiniD' => 'minid',
'Mirah' => 'mirah',
'mIRC Script' => 'mirc script',
'MLIR' => 'mlir',
'Modelica' => 'modelica',
'Modula-2' => 'modula-2',
'Modula-3' => 'modula-3',
'Module Management System' => 'module management system',
'Monkey' => 'monkey',
'Moocode' => 'moocode',
'MoonScript' => 'moonscript',
'Motorola 68K Assembly' => 'motorola 68k assembly',
'MQL4' => 'mql4',
'MQL5' => 'mql5',
'MTML' => 'mtml',
'MUF' => 'muf',
'mupad' => 'mupad',
'Muse' => 'muse',
'Myghty' => 'myghty',
'nanorc' => 'nanorc',
'NASL' => 'nasl',
'NCL' => 'ncl',
'Nearley' => 'nearley',
'Nemerle' => 'nemerle',
'nesC' => 'nesc',
'NetLinx' => 'netlinx',
'NetLinx+ERB' => 'netlinx+erb',
'NetLogo' => 'netlogo',
'NewLisp' => 'newlisp',
'Nextflow' => 'nextflow',
'Nginx' => 'nginx',
'Nim' => 'nim',
'Ninja' => 'ninja',
'Nit' => 'nit',
'Nix' => 'nix',
'NL' => 'nl',
'NPM Config' => 'npm config',
'NSIS' => 'nsis',
'Nu' => 'nu',
'NumPy' => 'numpy',
'ObjDump' => 'objdump',
'Object Data Instance Notation' => 'object data instance notation',
'Objective-C' => 'objective-c',
'Objective-C++' => 'objective-c++',
'Objective-J' => 'objective-j',
'ObjectScript' => 'objectscript',
'OCaml' => 'ocaml',
'Odin' => 'odin',
'Omgrofl' => 'omgrofl',
'ooc' => 'ooc',
'Opa' => 'opa',
'Opal' => 'opal',
'Open Policy Agent' => 'open policy agent',
'OpenCL' => 'opencl',
'OpenEdge ABL' => 'openedge abl',
'OpenQASM' => 'openqasm',
'OpenRC runscript' => 'openrc runscript',
'OpenSCAD' => 'openscad',
'OpenStep Property List' => 'openstep property list',
'OpenType Feature File' => 'opentype feature file',
'Org' => 'org',
'Ox' => 'ox',
'Oxygene' => 'oxygene',
'Oz' => 'oz',
'P4' => 'p4',
'Pan' => 'pan',
'Papyrus' => 'papyrus',
'Parrot' => 'parrot',
'Parrot Assembly' => 'parrot assembly',
'Parrot Internal Representation' => 'parrot internal representation',
'Pascal' => 'pascal',
'Pawn' => 'pawn',
'Pep8' => 'pep8',
'Perl' => 'perl',
'PHP' => 'php',
'Pic' => 'pic',
'Pickle' => 'pickle',
'PicoLisp' => 'picolisp',
'PigLatin' => 'piglatin',
'Pike' => 'pike',
'PLpgSQL' => 'plpgsql',
'PLSQL' => 'plsql',
'Pod' => 'pod',
'Pod 6' => 'pod 6',
'PogoScript' => 'pogoscript',
'Pony' => 'pony',
'PostCSS' => 'postcss',
'PostScript' => 'postscript',
'POV-Ray SDL' => 'pov-ray sdl',
'PowerBuilder' => 'powerbuilder',
'PowerShell' => 'powershell',
'Prisma' => 'prisma',
'Processing' => 'processing',
'Proguard' => 'proguard',
'Prolog' => 'prolog',
'Propeller Spin' => 'propeller spin',
'Protocol Buffer' => 'protocol buffer',
'Public Key' => 'public key',
'Pug' => 'pug',
'Puppet' => 'puppet',
'Pure Data' => 'pure data',
'PureBasic' => 'purebasic',
'PureScript' => 'purescript',
'Python' => 'python',
'Python console' => 'python console',
'Python traceback' => 'python traceback',
'q' => 'q',
'QMake' => 'qmake',
'QML' => 'qml',
'Quake' => 'quake',
'R' => 'r',
'Racket' => 'racket',
'Ragel' => 'ragel',
'Raku' => 'raku',
'RAML' => 'raml',
'Rascal' => 'rascal',
'Raw token data' => 'raw token data',
'RDoc' => 'rdoc',
'Readline Config' => 'readline config',
'REALbasic' => 'realbasic',
'Reason' => 'reason',
'Rebol' => 'rebol',
'Red' => 'red',
'Redcode' => 'redcode',
'Regular Expression' => 'regular expression',
// 'Ren'Py' => 'ren'py',
'RenderScript' => 'renderscript',
'reStructuredText' => 'restructuredtext',
'REXX' => 'rexx',
'RHTML' => 'rhtml',
'Rich Text Format' => 'rich text format',
'Ring' => 'ring',
'Riot' => 'riot',
'RMarkdown' => 'rmarkdown',
'RobotFramework' => 'robotframework',
'Roff' => 'roff',
'Roff Manpage' => 'roff manpage',
'Rouge' => 'rouge',
'RPC' => 'rpc',
'RPM Spec' => 'rpm spec',
'Ruby' => 'ruby',
'RUNOFF' => 'runoff',
'Rust' => 'rust',
'Sage' => 'sage',
'SaltStack' => 'saltstack',
'SAS' => 'sas',
'Sass' => 'sass',
'Scala' => 'scala',
'Scaml' => 'scaml',
'Scheme' => 'scheme',
'Scilab' => 'scilab',
'SCSS' => 'scss',
'sed' => 'sed',
'Self' => 'self',
'ShaderLab' => 'shaderlab',
'Shell' => 'shell',
'ShellSession' => 'shellsession',
'Shen' => 'shen',
'Slash' => 'slash',
'Slice' => 'slice',
'Slim' => 'slim',
'Smali' => 'smali',
'Smalltalk' => 'smalltalk',
'Smarty' => 'smarty',
'SmPL' => 'smpl',
'SMT' => 'smt',
'Solidity' => 'solidity',
'SourcePawn' => 'sourcepawn',
'SPARQL' => 'sparql',
'Spline Font Database' => 'spline font database',
'SQF' => 'sqf',
'SQL' => 'sql',
'SQLPL' => 'sqlpl',
'Squirrel' => 'squirrel',
'SRecode Template' => 'srecode template',
'SSH Config' => 'ssh config',
'Stan' => 'stan',
'Standard ML' => 'standard ml',
'Starlark' => 'starlark',
'Stata' => 'stata',
'STON' => 'ston',
'Stylus' => 'stylus',
'SubRip Text' => 'subrip text',
'SugarSS' => 'sugarss',
'SuperCollider' => 'supercollider',
'Svelte' => 'svelte',
'SVG' => 'svg',
'Swift' => 'swift',
'SWIG' => 'swig',
'SystemVerilog' => 'systemverilog',
'Tcl' => 'tcl',
'Tcsh' => 'tcsh',
'Tea' => 'tea',
'Terra' => 'terra',
'TeX' => 'tex',
'Texinfo' => 'texinfo',
'Text' => 'text',
'Textile' => 'textile',
'Thrift' => 'thrift',
'TI Program' => 'ti program',
'TLA' => 'tla',
'TOML' => 'toml',
'TSQL' => 'tsql',
'TSX' => 'tsx',
'Turing' => 'turing',
'Turtle' => 'turtle',
'Twig' => 'twig',
'TXL' => 'txl',
'Type Language' => 'type language',
'TypeScript' => 'typescript',
'Unified Parallel C' => 'unified parallel c',
'Unity3D Asset' => 'unity3d asset',
'Unix Assembly' => 'unix assembly',
'Uno' => 'uno',
'UnrealScript' => 'unrealscript',
'UrWeb' => 'urweb',
'V' => 'v',
'Vala' => 'vala',
'VBA' => 'vba',
'VBScript' => 'vbscript',
'VCL' => 'vcl',
'Verilog' => 'verilog',
'VHDL' => 'vhdl',
'Vim script' => 'vim script',
'Vim Snippet' => 'vim snippet',
'Visual Basic .NET' => 'visual basic .net',
'Visual Basic .NET' => 'visual basic .net',
'Volt' => 'volt',
'Vue' => 'vue',
'Wavefront Material' => 'wavefront material',
'Wavefront Object' => 'wavefront object',
'wdl' => 'wdl',
'Web Ontology Language' => 'web ontology language',
'WebAssembly' => 'webassembly',
'WebIDL' => 'webidl',
'WebVTT' => 'webvtt',
'Wget Config' => 'wget config',
'Windows Registry Entries' => 'windows registry entries',
'wisp' => 'wisp',
'Wollok' => 'wollok',
'World of Warcraft Addon Data' => 'world of warcraft addon data',
'X BitMap' => 'x bitmap',
'X Font Directory Index' => 'x font directory index',
'X PixMap' => 'x pixmap',
'X10' => 'x10',
'xBase' => 'xbase',
'XC' => 'xc',
'XCompose' => 'xcompose',
'XML' => 'xml',
'XML Property List' => 'xml property list',
'Xojo' => 'xojo',
'XPages' => 'xpages',
'XProc' => 'xproc',
'XQuery' => 'xquery',
'XS' => 'xs',
'XSLT' => 'xslt',
'Xtend' => 'xtend',
'Yacc' => 'yacc',
'YAML' => 'yaml',
'YANG' => 'yang',
'YARA' => 'yara',
'YASnippet' => 'yasnippet',
'ZAP' => 'zap',
'Zeek' => 'zeek',
'ZenScript' => 'zenscript',
'Zephir' => 'zephir',
'Zig' => 'zig',
'ZIL' => 'zil',
'Zimpl' => 'zimpl',
),
'defaultValue' => 'All languages'
)
),
'global' => array(
'date_range' => array(
'name' => 'Date range',
'type' => 'list',
'required' => false,
'values' => array(
'Today' => 'today',
'Weekly' => 'weekly',
'Monthly' => 'monthly',
),
'defaultValue' => 'today'
)
)
);
public function collectData(){
$params = array('since' => urlencode($this->getInput('date_range')));
$url = self::URI . '/' . $this->getInput('language') . '?' . http_build_query($params);
$html = getSimpleHTMLDOM($url)
or returnServerError('Error while downloading the website content');
$this->items = array();
foreach($html->find('.Box-row') as $element) {
$item = array();
// URI
$item['uri'] = self::URI_ITEM . $element->find('h1 a', 0)->href;
// Title
$item['title'] = str_replace(' ', '', trim(strip_tags($element->find('h1 a', 0)->plaintext)));
// Description
$item['content'] = trim(strip_tags($element->find('p.text-gray', 0)->innertext));
// Time
$item['timestamp'] = time();
// TODO: Proxy?
$this->items[] = $item;
}
}
public function getName(){
if($this->getInput('language') == '') {
return self::NAME . ': all';
} elseif (!is_null($this->getInput('language'))) {
return self::NAME . ': ' . $this->getInput('language');
}
return parent::getName();
}
}

View file

@ -3,34 +3,78 @@ class GizmodoBridge extends FeedExpander {
const MAINTAINER = 'polopollo';
const NAME = 'Gizmodo';
const URI = 'http://gizmodo.com/';
const URI = 'https://gizmodo.com';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns the newest posts from Gizmodo (full text).';
const DESCRIPTION = 'Returns the newest posts from Gizmodo.';
protected function parseItem($item){
protected function parseItem($item) {
$item = parent::parseItem($item);
$articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
if(!$articleHTMLContent) {
$text = 'Could not load ' . $item['uri'];
} else {
$text = $articleHTMLContent->find('div.entry-content', 0)->innertext;
foreach($articleHTMLContent->find('pagespeed_iframe') as $element) {
$text .= '<p>link to a iframe (could be a video): <a href="'
. $element->src
. '">'
. $element->src
. '</a></p><br>';
}
$html = getSimpleHTMLDOMCached($item['uri'])
or returnServerError('Could not request: ' . $item['uri']);
$text = strip_tags($text, '<p><b><a><blockquote><img><em>');
}
$html = defaultLinkTo($html, $this->getURI());
$this->stripTags($html);
$this->handleFigureTags($html);
$this->handleIframeTags($html);
// Get header image
$image = $html->find('meta[property="og:image"]', 0)->content;
$item['content'] = $html->find('div.js_post-content', 0)->innertext;
// Get categories
$categories = explode(',', $html->find('meta[name="keywords"]', 0)->content);
$item['categories'] = array_map('trim', $categories);
$item['enclosures'][] = $html->find('meta[property="og:image"]', 0)->content;
$item['content'] = $text;
return $item;
}
public function collectData(){
$this->collectExpandableDatas('http://feeds.gawker.com/gizmodo/full');
public function collectData() {
$this->collectExpandableDatas(self::URI . '/rss', 20);
}
private function stripTags($html) {
foreach ($html->find('aside') as $aside) {
$aside->outertext = '';
}
foreach ($html->find('div.ad-unit') as $div) {
$div->outertext = '';
}
foreach ($html->find('script') as $script) {
$script->outertext = '';
}
}
private function handleFigureTags($html) {
foreach ($html->find('figure') as $index => $figure) {
if (isset($figure->attr['data-id'])) {
$id = $figure->attr['data-id'];
$format = $figure->attr['data-format'];
} else {
$img = $figure->find('img', 0);
$id = $img->attr['data-chomp-id'];
$format = $img->attr['data-format'];
$figure->find('div.img-permalink-sub-wrapper', 0)->style = '';
}
$imageUrl = 'https://i.kinja-img.com/gawker-media/image/upload/' . $id . '.' . $format;
$figure->find('span', 0)->outertext = <<<EOD
<img src="{$imageUrl}">
EOD;
}
}
private function handleIframeTags($html) {
foreach($html->find('iframe') as $iframe) {
$iframe->src = urljoin($this->getURI(), $iframe->src);
}
}
}

View file

@ -28,7 +28,7 @@ class GoComicsBridge extends BridgeAbstract {
$page = getSimpleHTMLDOM($link)
or returnServerError('Could not request GoComics: ' . $link);
$imagelink = $page->find('.img-fluid', 1)->src;
$imagelink = $page->find('.comic.container', 0)->getAttribute('data-image');
$date = explode('/', $link);
$item['id'] = $imagelink;

View file

@ -35,16 +35,10 @@ class GoogleSearchBridge extends BridgeAbstract {
$item = array();
// Extract direct URL from google href (eg. /url?q=...)
$t = $element->find('a[href]', 0)->href;
$item['uri'] = '' . $t;
parse_str(parse_url($t, PHP_URL_QUERY), $parameters);
if(isset($parameters['q'])) {
$item['uri'] = $parameters['q'];
}
$item['uri'] = htmlspecialchars_decode($t);
$item['title'] = $element->find('h3', 0)->plaintext;
$item['content'] = $element->find('span[class=st]', 0)->plaintext;
$item['content'] = $element->find('span[class=aCOpRe]', 0)->plaintext;
$this->items[] = $item;
}

View file

@ -32,7 +32,7 @@ class HDWallpapersBridge extends BridgeAbstract {
$lastpage = 1;
for($page = 1; $page <= $lastpage; $page++) {
$link = self::URI . '/' . $category . '/page/' . $page;
$link = self::URI . $category . '/page/' . $page;
$html = getSimpleHTMLDOM($link)
or returnServerError('No results for this query.');
@ -41,13 +41,16 @@ class HDWallpapersBridge extends BridgeAbstract {
$lastpage = min($matches[1], ceil($max / 14));
}
$html = defaultLinkTo($html, self::URI);
foreach($html->find('.wallpapers .wall a') as $element) {
$thumbnail = $element->find('img', 0);
$search = array(self::URI, 'wallpapers.html');
$replace = array(self::URI . 'download/', $this->getInput('r') . '.jpg');
$item = array();
$item['uri'] = self::URI
. '/download'
. str_replace('wallpapers.html', $this->getInput('r') . '.jpg', $element->href);
$item['uri'] = str_replace($search, $replace, $element->href);
$item['timestamp'] = time();
$item['title'] = $element->find('em1', 0)->text();
@ -55,7 +58,6 @@ class HDWallpapersBridge extends BridgeAbstract {
. '<br><a href="'
. $item['uri']
. '"><img src="'
. self::URI
. $thumbnail->src
. '" /></a>';

View file

@ -40,18 +40,15 @@ class HeiseBridge extends FeedExpander {
protected function parseItem($feedItem) {
$item = parent::parseItem($feedItem);
$uri = $item['uri'];
$uri = $item['uri'] . '&seite=all';
do {
$article = getSimpleHTMLDOMCached($uri)
or returnServerError('Could not open article: ' . $uri);
$article = getSimpleHTMLDOMCached($uri)
or returnServerError('Could not open article: ' . $uri);
if ($article) {
$article = defaultLinkTo($article, $uri);
$item = $this->addArticleToItem($item, $article);
if($next = $article->find('.pagination a[rel="next"]', 0))
$uri = $next->href;
} while ($next);
}
return $item;
}
@ -62,6 +59,9 @@ class HeiseBridge extends FeedExpander {
$content = $article->find('div[class*="article-content"]', 0);
if ($content == null)
$content = $article->find('#article_content', 0);
foreach($content->find('p, h3, ul, table, pre, img') as $element) {
$item['content'] .= $element;
}

View file

@ -19,6 +19,27 @@ class IGNBridge extends FeedExpander {
// $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link);
// List of BS elements
$uselessElements = array(
'.wiki-page-tools',
'.feedback-container',
'.paging-container',
'.dropdown-wrapper',
'.mw-editsection',
'.jsx-4115608983',
'.jsx-4213937408',
'.commerce-container',
'.widget-container',
'.newsletter-signup-button'
);
// Remove useless elements
foreach($uselessElements as $uslElement) {
foreach($articlePage->find($uslElement) as $jsWidget) {
$jsWidget->remove();
}
}
/*
* NOTE: Though articles and wiki/howtos have seperate styles of pages, there is no mechanism
* for handling them seperately as it just ignores the DOM querys which it does not find.
@ -33,19 +54,8 @@ class IGNBridge extends FeedExpander {
}
// 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;
$article = $article . $wikiContents;
}
// Add content to feed

114
bridges/IKWYDBridge.php Normal file
View file

@ -0,0 +1,114 @@
<?php
class IKWYDBridge extends BridgeAbstract {
const MAINTAINER = 'DevonHess';
const NAME = 'I Know What You Download';
const URI = 'https://iknowwhatyoudownload.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns torrent downloads and distributions for an IP address';
const PARAMETERS = array(
array(
'ip' => array(
'name' => 'IP Address',
'required' => true
),
'update' => array(
'name' => 'Update last seen',
'type' => 'checkbox',
'title' => 'Update timestamp every time "last seen" changes'
)
)
);
private $name;
private $uri;
public function detectParameters($url) {
$params = array();
$regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/';
$regex .= '(?:en|ru)\/peer\/\?ip=(\d+\.\d+\.\d+\.\d+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['ip'] = urldecode($matches[2]);
return $params;
}
$regex = '/^(https?:\/\/)?iknowwhatyoudownload\.com\/';
$regex .= '(?:(?:en|ru)\/peer\/)?/';
if(preg_match($regex, $url, $matches) > 0) {
$params['ip'] = $_SERVER['REMOTE_ADDR'];
return $params;
}
return null;
}
public function getName() {
if($this->name) {
return $this->name;
} else {
return self::NAME;
}
}
public function getURI() {
if($this->uri) {
return $this->uri;
} else {
return self::URI;
}
}
public function collectData() {
$ip = $this->getInput('ip');
$root = self::URI . 'en/peer/?ip=' . $ip;
$html = getSimpleHTMLDOM($root)
or returnServerError('Could not request ' . self::URI);
$this->name = 'IKWYD: ' . $ip;
$this->uri = $root;
foreach($html->find('.table > tbody > tr') as $download) {
$download = defaultLinkTo($download, self::URI);
$firstSeen = $download->find('.date-column',
0)->innertext;
$lastSeen = $download->find('.date-column',
1)->innertext;
$category = $download->find('.category-column',
0)->innertext;
$torlink = $download->find('.name-column > div > a',
0);
$tortitle = strip_tags($torlink);
$size = $download->find('td', 4)->innertext;
$title = $tortitle;
$author = $ip;
if($this->getInput('update')) {
$timestamp = strtotime($lastSeen);
} else {
$timestamp = strtotime($firstSeen);
}
$uri = $torlink->href;
$content = 'IP address: <a href="' . $root . '">';
$content .= $ip . '</a><br>';
$content .= 'First seen: ' . $firstSeen . '<br>';
$content .= ($this->getInput('update') ? 'Last seen: ' .
$lastSeen . '<br>' : '');
$content .= ($category ? 'Category: ' .
$category . '<br>' : '');
$content .= 'Title: ' . $torlink . '<br>';
$content .= 'Size: ' . $size;
$item = array();
$item['uri'] = $uri;
$item['title'] = $title;
$item['author'] = $author;
$item['timestamp'] = $timestamp;
$item['content'] = $content;
if($category) {
$item['categories'] = array($category);
}
$this->items[] = $item;
}
}
}

View file

@ -1,7 +1,7 @@
<?php
class InstagramBridge extends BridgeAbstract {
const MAINTAINER = 'pauder';
// const MAINTAINER = 'pauder';
const NAME = 'Instagram Bridge';
const URI = 'https://www.instagram.com/';
const DESCRIPTION = 'Returns the newest images';
@ -47,7 +47,7 @@ class InstagramBridge extends BridgeAbstract {
);
const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951';
const TAG_QUERY_HASH = '174a5243287c5f3a7de741089750ab3b';
const TAG_QUERY_HASH = '9b498c08113f1e09617a1703c22b2f32';
const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a';
protected function getInstagramUserId($username) {
@ -65,7 +65,7 @@ class InstagramBridge extends BridgeAbstract {
$data = getContents(self::URI . 'web/search/topsearch/?query=' . $username);
foreach(json_decode($data)->users as $user) {
if($user->user->username === $username) {
if(strtolower($user->user->username) === strtolower($username)) {
$key = $user->user->pk;
}
}
@ -123,31 +123,33 @@ class InstagramBridge extends BridgeAbstract {
$item['title'] = substr($item['title'], 0, $titleLinePos) . '...';
}
if($directLink) {
$mediaURI = $media->display_url;
} else {
$mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
}
switch($media->__typename) {
case 'GraphSidecar':
$data = $this->getInstagramSidecarData($item['uri'], $item['title']);
$data = $this->getInstagramSidecarData($item['uri'], $item['title'], $media, $textContent);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
break;
case 'GraphImage':
if($directLink) {
$mediaURI = $media->display_url;
} else {
$mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
}
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
$item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
$item['enclosures'] = array($mediaURI);
break;
case 'GraphVideo':
$data = $this->getInstagramVideoData($item['uri']);
$data = $this->getInstagramVideoData($item['uri'], $mediaURI, $media, $textContent);
$item['content'] = $data[0];
if($directLink) {
$item['enclosures'] = $data[1];
} else {
$item['enclosures'] = array(self::URI . 'p/' . $media->shortcode . '/media?size=l');
$item['enclosures'] = array($mediaURI);
}
$item['thumbnail'] = $mediaURI;
break;
default: break;
}
@ -158,11 +160,7 @@ class InstagramBridge extends BridgeAbstract {
}
// returns Sidecar(a post which has multiple media)'s contents and enclosures
protected function getInstagramSidecarData($uri, $postTitle) {
$mediaInfo = $this->getSinglePostData($uri);
$textContent = $this->getTextContent($mediaInfo);
protected function getInstagramSidecarData($uri, $postTitle, $mediaInfo, $textContent) {
$enclosures = array();
$content = '';
foreach($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) {
@ -185,11 +183,11 @@ class InstagramBridge extends BridgeAbstract {
}
// returns Video post's contents and enclosures
protected function getInstagramVideoData($uri) {
$mediaInfo = $this->getSinglePostData($uri);
$textContent = $this->getTextContent($mediaInfo);
$content = '<video controls><source src="' . $mediaInfo->video_url . '" type="video/mp4"></video><br>';
protected function getInstagramVideoData($uri, $mediaURI, $mediaInfo, $textContent) {
$content = '<video controls>';
$content .= '<source src="' . $mediaInfo->video_url . '" poster="' . $mediaURI . '" type="video/mp4">';
$content .= '<img src="' . $mediaURI . '" alt="">';
$content .= '</video><br>';
$content .= '<br>' . nl2br(htmlentities($textContent));
return array($content, array($mediaInfo->video_url));

46
bridges/ItchioBridge.php Normal file
View file

@ -0,0 +1,46 @@
<?php
class ItchioBridge extends BridgeAbstract {
const NAME = 'itch.io';
const URI = 'https://itch.io';
const DESCRIPTION = 'Fetches the file uploads for a product';
const MAINTAINER = 'jacquesh';
const PARAMETERS = array(array(
'url' => array(
'name' => 'Product URL',
'exampleValue' => 'https://remedybg.itch.io/remedybg',
'required' => true,
)
));
const CACHE_TIMEOUT = 21600; // 6 hours
public function collectData() {
$url = $this->getInput('url');
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request: ' . $url);
$title = $html->find('.game_title', 0)->innertext;
$timestampOriginal = $html->find('span.icon-stopwatch', 0)->parent()->title;
$timestampFormatted = str_replace('@', '', $timestampOriginal);
$content = 'The following files are available to download:<br/>';
foreach ($html->find('div.upload') as $element) {
$filename = $element->find('strong.name', 0)->innertext;
$filesize = $element->find('span.file_size', 0)->first_child()->innertext;
$content = $content . $filename . ' (' . $filesize . ')<br/>';
}
// NOTE: At the time of writing it is not clear under which conditions
// itch updates the timestamp. In case they don't always update it,
// we include the file list as well when computing the UID hash.
$uidContent = $timestampFormatted . $content;
$item = array();
$item['uri'] = $url;
$item['uid'] = $uidContent;
$item['title'] = 'New release for ' . $title;
$item['content'] = $content;
$item['timestamp'] = $timestampFormatted;
$this->items[] = $item;
}
}

View file

@ -347,5 +347,6 @@ class JustETFBridge extends BridgeAbstract {
return $element->plaintext;
}
#endregion
}

View file

@ -61,6 +61,8 @@ class KernelBugTrackerBridge extends BridgeAbstract {
if($html === false)
returnServerError('Failed to load page!');
$html = defaultLinkTo($html, self::URI);
// Store header information into private members
$this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext;
$this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext;
@ -93,7 +95,7 @@ class KernelBugTrackerBridge extends BridgeAbstract {
$item['content'] = str_replace("\n", '<br>', $item['content']);
// Fix relative URIs
$item['content'] = $this->replaceRelativeURI($item['content']);
$item['content'] = $item['content'];
$this->items[] = $item;
}
@ -125,17 +127,6 @@ class KernelBugTrackerBridge extends BridgeAbstract {
}
}
/**
* Replaces all relative URIs with absolute ones
*
* @param string $content The source string
* @return string Returns the source string with all relative URIs replaced
* by absolute ones.
*/
private function replaceRelativeURI($content){
return preg_replace('/href="(?!http)/', 'href="' . self::URI . '/', $content);
}
/**
* Adds styles as attributes to tags with known classes
*

View file

@ -3,7 +3,7 @@ class KoreusBridge extends FeedExpander {
const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'Koreus';
const URI = 'http://www.koreus.com/';
const URI = 'https://www.koreus.com/';
const DESCRIPTION = 'Returns the newest posts from Koreus (full text)';
protected function parseItem($item){
@ -17,6 +17,6 @@ class KoreusBridge extends FeedExpander {
}
public function collectData(){
$this->collectExpandableDatas('http://feeds.feedburner.com/Koreus-articles');
$this->collectExpandableDatas('https://feeds.feedburner.com/Koreus-articles');
}
}

View file

@ -26,8 +26,8 @@ class LeMondeInformatiqueBridge extends FeedExpander {
//No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail
$content_node = $article_html->find('div.col-primary, div.col-sm-9', 0);
$item['content'] = utf8_encode($this->cleanArticle($content_node->innertext));
$item['author'] = utf8_encode($article_html->find('div.author-infos', 0)->find('b', 0)->plaintext);
$item['content'] = $this->cleanArticle($content_node->innertext);
$item['author'] = $article_html->find('div.author-infos', 0)->find('b', 0)->plaintext;
return $item;
}

View file

@ -11,7 +11,7 @@ class LesJoiesDuCodeBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request LesJoiesDuCode.');
foreach($html->find('div.blog-post') as $element) {
foreach($html->find('article.blog-post') as $element) {
$item = array();
$temp = $element->find('h1 a', 0);
$titre = html_entity_decode($temp->innertext);

73
bridges/MallTvBridge.php Normal file
View file

@ -0,0 +1,73 @@
<?php
class MallTvBridge extends BridgeAbstract {
const NAME = 'MALL.TV Bridge';
const URI = 'https://www.mall.tv';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'Return newest videos';
const MAINTAINER = 'kolarcz';
const PARAMETERS = array(
array(
'url' => array(
'name' => 'url to the show',
'required' => true,
'exampleValue' => 'https://www.mall.tv/zivot-je-hra'
)
)
);
private function fixChars($text) {
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
private function getUploadTimeFromUrl($url) {
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request MALL.TV detail page');
$scriptLdJson = $html->find('script[type="application/ld+json"]', 0)->innertext;
if (!preg_match('/[\'"]uploadDate[\'"]\s*:\s*[\'"](\d{4}-\d{2}-\d{2})[\'"]/', $scriptLdJson, $match)) {
returnServerError('Could not get date from MALL.TV detail page');
}
return strtotime($match[1]);
}
public function collectData() {
$url = $this->getInput('url');
if (!preg_match('/^https:\/\/www\.mall\.tv\/[a-z0-9-]+(\/[a-z0-9-]+)?\/?$/', $url)) {
returnServerError('Invalid url');
}
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request MALL.TV');
$this->feedUri = $url;
$this->feedName = $this->fixChars($html->find('title', 0)->plaintext);
foreach ($html->find('section.isVideo .video-card') as $element) {
$itemTitle = $element->find('.video-card__details-link', 0);
$itemThumbnail = $element->find('.video-card__thumbnail', 0);
$itemUri = self::URI . $itemTitle->getAttribute('href');
$item = array(
'title' => $this->fixChars($itemTitle->plaintext),
'uri' => $itemUri,
'content' => '<img src="' . $itemThumbnail->getAttribute('data-src') . '" />',
'timestamp' => $this->getUploadTimeFromUrl($itemUri)
);
$this->items[] = $item;
}
}
public function getURI() {
return isset($this->feedUri) ? $this->feedUri : parent::getURI();
}
public function getName() {
return isset($this->feedName) ? $this->feedName : parent::getName();
}
}

View file

@ -0,0 +1,127 @@
<?php
class MarktplaatsBridge extends BridgeAbstract {
const NAME = 'Marktplaats';
const URI = 'https://marktplaats.nl';
const DESCRIPTION = 'Read search queries from marktplaats.nl';
const PARAMETERS = array(
'Search' => array(
'q' => array(
'name' => 'query',
'type' => 'text',
'required' => true,
'title' => 'The search string for marktplaats',
),
'z' => array(
'name' => 'zipcode',
'type' => 'text',
'required' => false,
'title' => 'Zip code for location limited searches',
),
'd' => array(
'name' => 'distance',
'type' => 'number',
'required' => false,
'title' => 'The distance in meters from the zipcode',
),
'f' => array(
'name' => 'priceFrom',
'type' => 'number',
'required' => false,
'title' => 'The minimal price in cents',
),
't' => array(
'name' => 'priceTo',
'type' => 'number',
'required' => false,
'title' => 'The maximal price in cents',
),
's' => array(
'name' => 'showGlobal',
'type' => 'checkbox',
'required' => false,
'title' => 'Include result with negative distance',
),
'i' => array(
'name' => 'includeImage',
'type' => 'checkbox',
'required' => false,
'title' => 'Include the image at the end of the content',
),
'r' => array(
'name' => 'includeRaw',
'type' => 'checkbox',
'required' => false,
'title' => 'Include the raw data behind the content',
)
)
);
const CACHE_TIMEOUT = 900;
public function collectData() {
$query = '';
$excludeGlobal = false;
if(!is_null($this->getInput('z')) && !is_null($this->getInput('d'))) {
$query = '&postcode=' . $this->getInput('z') . '&distanceMeters=' . $this->getInput('d');
}
if(!is_null($this->getInput('f'))) {
$query .= '&PriceCentsFrom=' . $this->getInput('f');
}
if(!is_null($this->getInput('t'))) {
$query .= '&PriceCentsTo=' . $this->getInput('t');
}
if(!is_null($this->getInput('s'))) {
if(!$this->getInput('s')) {
$excludeGlobal = true;
}
}
$url = 'https://www.marktplaats.nl/lrp/api/search?query=' . urlencode($this->getInput('q')) . $query;
$jsonString = getSimpleHTMLDOM($url, 900) or returnServerError('No contents received!');
$jsonObj = json_decode($jsonString);
foreach($jsonObj->listings as $listing) {
if(!$excludeGlobal || $listing->location->distanceMeters >= 0) {
$item = array();
$item['uri'] = 'https://marktplaats.nl' . $listing->vipUrl;
$item['title'] = $listing->title;
$item['timestamp'] = $listing->date;
$item['author'] = $listing->sellerInformation->sellerName;
$item['content'] = $listing->description;
$item['categories'] = $listing->verticals;
$item['uid'] = $listing->itemId;
if(!is_null($this->getInput('i')) && !empty($listing->imageUrls)) {
$item['enclosures'] = $listing->imageUrls;
if(is_array($listing->imageUrls)) {
foreach($listing->imageUrls as $imgurl) {
$item['content'] .= "<br />\n<img src='https:" . $imgurl . "' />";
}
} else {
$item['content'] .= "<br>\n<img src='https:" . $listing->imageUrls . "' />";
}
}
if(!is_null($this->getInput('r'))) {
if($this->getInput('r')) {
$item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing);
}
}
$item['content'] .= "<br>\n<br>\nPrice: " . $listing->priceInfo->priceCents / 100;
$item['content'] .= '&nbsp;&nbsp;(' . $listing->priceInfo->priceType . ')';
if(!empty($listing->location->cityName)) {
$item['content'] .= "<br><br>\n" . $listing->location->cityName;
}
if(!is_null($this->getInput('r'))) {
if($this->getInput('r')) {
$item['content'] .= "<br />\n<br />\n<br />\n" . json_encode($listing);
}
}
$this->items[] = $item;
}
}
}
public function getName(){
if(!is_null($this->getInput('q'))) {
return $this->getInput('q') . ' - Marktplaats';
}
return parent::getName();
}
}

View file

@ -78,7 +78,7 @@ class MastodonBridge extends FeedExpander {
public function getURI(){
if($this->getInput('canusername'))
return 'https://' . $this->getInstance() . '/users/' . $this->getUsername() . '.atom';
return 'https://' . $this->getInstance() . '/@' . $this->getUsername() . '.rss';
return parent::getURI();
}

View file

@ -0,0 +1,48 @@
<?php
class MediapartBlogsBridge extends BridgeAbstract {
const NAME = 'Mediapart Blogs';
const BASE_URI = 'https://blogs.mediapart.fr';
const URI = self::BASE_URI . '/blogs';
const MAINTAINER = 'somini';
const PARAMETERS = array(
array(
'slug' => array(
'name' => 'Blog Slug',
'type' => 'text',
'title' => 'Blog user name',
'exampleValue' => 'jean-vincot',
)
)
);
public function getIcon() {
return 'https://static.mediapart.fr/favicon/favicon-club.ico?v=2';
}
public function collectData() {
$html = getSimpleHTMLDOM(self::BASE_URI . '/' . $this->getInput('slug') . '/blog')
or returnServerError('Could not load content');
foreach($html->find('ul.post-list li') as $element) {
$item = array();
$item_title = $element->find('h3.title a', 0);
$item_divs = $element->find('div');
$item['title'] = $item_title->innertext;
$item['uri'] = self::BASE_URI . trim($item_title->href);
$item['author'] = $element->find('.author .subscriber', 0)->innertext;
$item['content'] = $item_divs[count($item_divs) - 2] . $item_divs[count($item_divs) - 1];
$item['timestamp'] = strtotime($element->find('.author time', 0)->datetime);
$this->items[] = $item;
}
}
public function getName() {
if ($this->getInput('slug')) {
return self::NAME . ' | ' . $this->getInput('slug');
}
return parent::getName();
}
}

View file

@ -3,22 +3,26 @@ class MondeDiploBridge extends BridgeAbstract {
const MAINTAINER = 'Pitchoule';
const NAME = 'Monde Diplomatique';
const URI = 'http://www.monde-diplomatique.fr/';
const URI = 'https://www.monde-diplomatique.fr';
const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'Returns most recent results from MondeDiplo.';
private function cleanText($text) {
return trim(str_replace(array('&nbsp;', '&nbsp'), ' ', $text));
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request MondeDiplo. for : ' . self::URI);
foreach($html->find('div.unarticle') as $article) {
$element = $article->parent();
$title = $element->find('h3', 0)->plaintext;
$datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext;
$item = array();
$item['uri'] = self::URI . $element->href;
$item['title'] = $element->find('h3', 0)->plaintext;
$item['content'] = $element->find('div.dates_auteurs', 0)->plaintext
. '<br>'
. strstr($element->find('div', 0)->plaintext, $element->find('div.dates_auteurs', 0)->plaintext, true);
$item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs);
$item['content'] = $this->cleanText(str_replace(array($title, $datesAuteurs), '', $element->plaintext));
$this->items[] = $item;
}

View file

@ -61,43 +61,44 @@ class MozillaBugTrackerBridge extends BridgeAbstract {
if($html === false)
returnServerError('Failed to load page!');
// Fix relative URLs
defaultLinkTo($html, self::URI);
// Store header information into private members
$this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext;
$this->bugdesc = $html->find('table.bugfields', 0)->find('tr', 0)->find('td', 0)->innertext;
$this->bugid = $html->find('#field-value-bug_id', 0)->plaintext;
$this->bugdesc = $html->find('h1#field-value-short_desc', 0)->plaintext;
// Get and limit comments
$comments = $html->find('.bz_comment_table div.bz_comment');
$comments = $html->find('div.change-set');
if($limit > 0 && count($comments) > $limit) {
$comments = array_slice($comments, count($comments) - $limit, $limit);
}
// Order comments
switch($sorting) {
case 'lf': $comments = array_reverse($comments, true);
case 'of':
default: // Nothing to do, keep original order
if ($sorting === 'lf') {
$comments = array_reverse($comments, true);
}
foreach($comments as $comment) {
$comment = $this->inlineStyles($comment);
$item = array();
$item['uri'] = $this->getURI() . '#' . $comment->id;
$item['author'] = $comment->find('span.bz_comment_user', 0)->innertext;
$item['title'] = $comment->find('span.bz_comment_number', 0)->find('a', 0)->innertext;
$item['timestamp'] = strtotime($comment->find('span.bz_comment_time', 0)->innertext);
$item['content'] = $comment->find('pre.bz_comment_text', 0)->innertext;
$item['uri'] = $comment->find('h3.change-name', 0)->find('a', 0)->href;
$item['author'] = $comment->find('td.change-author', 0)->plaintext;
$item['title'] = $comment->find('h3.change-name', 0)->plaintext;
$item['timestamp'] = strtotime($comment->find('span.rel-time', 0)->title);
$item['content'] = '';
// Fix line breaks (they use LF)
$item['content'] = str_replace("\n", '<br>', $item['content']);
if ($comment->find('.comment-text', 0)) {
$item['content'] = $comment->find('.comment-text', 0)->outertext;
}
// Fix relative URIs
$item['content'] = $this->replaceRelativeURI($item['content']);
if ($comment->find('div.activity', 0)) {
$item['content'] .= $comment->find('div.activity', 0)->innertext;
}
$this->items[] = $item;
}
}
public function getURI(){
@ -114,9 +115,8 @@ class MozillaBugTrackerBridge extends BridgeAbstract {
public function getName(){
switch($this->queriedContext) {
case 'Bug comments':
return 'Bug '
. $this->bugid
. ' tracker for '
return $this->bugid
. ' - '
. $this->bugdesc
. ' - '
. parent::getName();
@ -125,17 +125,6 @@ class MozillaBugTrackerBridge extends BridgeAbstract {
}
}
/**
* Replaces all relative URIs with absolute ones
*
* @param string $content The source string
* @return string Returns the source string with all relative URIs replaced
* by absolute ones.
*/
private function replaceRelativeURI($content){
return preg_replace('/href="(?!http)/', 'href="' . self::URI . '/', $content);
}
/**
* Adds styles as attributes to tags with known classes
*
@ -144,10 +133,14 @@ class MozillaBugTrackerBridge extends BridgeAbstract {
* attributes.
*/
private function inlineStyles($html){
foreach($html->find('.bz_obsolete') as $element) {
foreach($html->find('.bz_closed') as $element) {
$element->style = 'text-decoration:line-through;';
}
foreach($html->find('pre') as $element) {
$element->style = 'white-space: pre-wrap;';
}
return $html;
}
}

View file

@ -15,7 +15,7 @@ class MozillaSecurityBridge extends BridgeAbstract {
$html = defaultLinkTo($html, self::WEBROOT);
$item = array();
$articles = $html->find('div[itemprop="articleBody"] h2');
$articles = $html->find('div[id="main-content"] h2');
foreach ($articles as $element) {
$item['title'] = $element->innertext;

View file

@ -12,10 +12,8 @@ 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);
for($i = 0; $i < 3; $i++) {
$line = $list[$i];
// Start at 1 to skip the "APOD Full Archive" on top of the page
for($i = 1; $i < 4; $i++) {
$item = array();
$uri_page = $html->find('a', $i + 3)->href;
@ -26,9 +24,14 @@ class NasaApodBridge extends BridgeAbstract {
$picture_html_string = $picture_html->innertext;
//Extract image and explanation
$media = $picture_html->find('p', 1)->innertext;
$media = strstr($media, '<br>');
$media = preg_replace('/<br>/', '', $media, 1);
$image_wrapper = $picture_html->find('a', 1);
$image_path = $image_wrapper->href;
$img_placeholder = $image_wrapper->find('img', 0);
$img_alt = $img_placeholder->alt;
$img_style = $img_placeholder->style;
$image_uri = self::URI . $image_path;
$new_img_placeholder = "<img src=\"$image_uri\" alt=\"$img_alt\" style=\"$img_style\">";
$media = "<a href=\"$image_uri\">$new_img_placeholder</a>";
$explanation = $picture_html->find('p', 2)->innertext;
//Extract date from the picture page

View file

@ -0,0 +1,59 @@
<?php
class NewOnNetflixBridge extends BridgeAbstract {
const NAME = 'NewOnNetflix removals bridge';
const URI = 'https://www.newonnetflix.info';
const DESCRIPTION = 'Upcoming removals from Netflix (NewOnNetflix already provides additions as RSS)';
const MAINTAINER = 'jdesgats';
const PARAMETERS = array(array(
'country' => array(
'name' => 'Country',
'type' => 'list',
'values' => array(
'Australia/New Zealand' => 'anz',
'Canada' => 'can',
'United Kingdom' => 'uk',
'United States' => 'usa',
),
'defaultValue' => 'uk',
)
));
const CACHE_TIMEOUT = 3600 * 24;
public function collectData() {
$baseURI = 'https://' . $this->getInput('country') . '.newonnetflix.info';
$html = getSimpleHTMLDOMCached($baseURI . '/lastchance', self::CACHE_TIMEOUT)
or returnServerError('Could not request NewOnNetflix (U FAILED LOL).');
foreach($html->find('article.oldpost') as $element) {
$title = $element->find('a.infopop[title]', 0);
$img = $element->find('img[lazy_src]', 0);
$date = $element->find('span[title]', 0);
// format sholud be 'dd/mm/yy - dd/mm/yy'
// (the added date might be "unknown")
$fromTo = array();
if (preg_match('/^\s*(.*?)\s*-\s*(.*?)\s*$/', $date->title, $fromTo)) {
$from = $fromTo[1];
$to = $fromTo[2];
} else {
$from = 'unknown';
$to = 'unknown';
}
$summary = <<<EOD
<img src="{$img->lazy_src}" loading="lazy">
<div>{$title->title}</div>
<div><strong>Added on:</strong>$from</div>
<div><strong>Removed on:</strong>$to</div>
EOD;
$item = array();
$item['uri'] = $baseURI . $title->href;
$item['title'] = $to . ' - ' . $title->plaintext;
$item['content'] = $summary;
// some movies are added and removed multiple times
$item['uid'] = $title->href . '-' . $to;
$this->items[] = $item;
}
}
}

View file

@ -1,9 +1,10 @@
<?php
class NextInpactBridge extends FeedExpander {
const MAINTAINER = 'qwertygc';
const MAINTAINER = 'qwertygc and ORelio';
const NAME = 'NextInpact Bridge';
const URI = 'https://www.nextinpact.com/';
const URI_HARDWARE = 'https://www.inpact-hardware.com/';
const DESCRIPTION = 'Returns the newest articles.';
const PARAMETERS = array( array(
@ -11,10 +12,30 @@ class NextInpactBridge extends FeedExpander {
'name' => 'Feed',
'type' => 'list',
'values' => array(
'Tous nos articles' => 'news',
'Nos contenus en accès libre' => 'acces-libre',
'Blog' => 'blog',
'Bons plans' => 'bonsplans'
'Nos actualités' => array(
'Toutes nos publications' => 'news',
'Toutes nos publications sauf #LeBrief' => 'nobrief',
'Toutes nos publications sauf INpact Hardware' => 'noih',
'Seulement les publications INpact Hardware' => 'hardware:news',
'Seulement les publications Next INpact' => 'nobrief-noih',
'Seulement les publications #LeBrief' => 'lebrief',
),
'Flux spécifiques' => array(
'Le blog' => 'blog',
'Les bons plans' => 'bonsplans',
'Publications INpact Hardware en accès libre' => 'hardware:acces-libre',
'Publications Next INpact en accès libre' => 'acces-libre',
),
'Flux thématiques' => array(
'Tech' => 'category:1',
'Logiciel' => 'category:2',
'Internet' => 'category:3',
'Mobilité' => 'category:4',
'Droit' => 'category:5',
'Économie' => 'category:6',
'Culture numérique' => 'category:7',
'Next INpact' => 'category:8',
)
)
),
'filter_premium' => array(
@ -39,9 +60,27 @@ class NextInpactBridge extends FeedExpander {
public function collectData(){
$feed = $this->getInput('feed');
if (empty($feed))
$base_uri = self::URI;
$args = '';
if (empty($feed)) {
// Default to All articles
$feed = 'news';
$this->collectExpandableDatas(self::URI . 'rss/' . $feed . '.xml');
}
if (strpos($feed, 'hardware:') === 0) {
// Feed hosted on Hardware domain
$base_uri = self::URI_HARDWARE;
$feed = str_replace('hardware:', '', $feed);
}
if (strpos($feed, 'category:') === 0) {
// Feed with specific category parameter
$args = '?CategoryIds=' . str_replace('category:', '', $feed);
$feed = 'params';
}
$this->collectExpandableDatas($base_uri . 'rss/' . $feed . '.xml' . $args);
}
protected function parseItem($newsItem){
@ -57,9 +96,11 @@ class NextInpactBridge extends FeedExpander {
if (!is_object($html))
return 'Failed to request NextInpact: ' . $url;
// Filter premium and brief articles?
$brief_selector = 'div.brief-container';
foreach(array(
'filter_premium' => 'h2.title_reserve_article',
'filter_brief' => 'div.brief-inner-content'
'filter_premium' => 'p.red-msg',
'filter_brief' => $brief_selector
) as $param_name => $selector) {
$param_val = intval($this->getInput($param_name));
if ($param_val != 0) {
@ -71,38 +112,71 @@ class NextInpactBridge extends FeedExpander {
}
}
if (is_object($html->find('div[itemprop=articleBody], div.brief-inner-content', 0))) {
$article_content = $html->find('div.article-content', 0);
if (!is_object($article_content)) {
$article_content = $html->find('div.content', 0);
}
if (is_object($article_content)) {
$subtitle = trim($html->find('span.sub_title, div.brief-head', 0));
if(is_object($subtitle) && $subtitle->plaintext !== $item['title']) {
$subtitle = '<p><em>' . $subtitle->plaintext . '</em></p>';
// Subtitle
$subtitle = $html->find('small.subtitle', 0);
if(!is_object($subtitle) && !is_object($html->find($brief_selector, 0))) {
$subtitle = $html->find('small', 0);
}
if(!is_object($subtitle)) {
$content_wrapper = $html->find('div.content-wrapper', 0);
if (is_object($content_wrapper)) {
$subtitle = $content_wrapper->find('h2.title', 0);
}
}
if(is_object($subtitle) && (!isset($item['title']) || $subtitle->plaintext != $item['title'])) {
$subtitle = '<p><em>' . trim($subtitle->plaintext) . '</em></p>';
} else {
$subtitle = '';
}
$postimg = $html->find(
'div.container_main_image_article, div.image-brief-container, div.image-brief-side-container', 0
);
// Image
$postimg = $html->find('div.article-image, div.image-container', 0);
if(is_object($postimg)) {
$postimg = '<p><img src="'
. $postimg->find('img.dedicated', 0)->src
. '" alt="-" /></p>';
$postimg = $postimg->find('img', 0);
if (!empty($postimg->src)) {
$postimg = $postimg->src;
} else {
$postimg = $postimg->srcset; //"url 355w, url 1003w, url 748w"
$postimg = explode(', ', $postimg); //split by ', ' to get each url separately
$postimg = end($postimg); //Get last item: "url 748w" which is of largest size
$postimg = explode(' ', $postimg); //split by ' ' to separate url from res
$postimg = array_reverse($postimg); //reverse array content to have url last
$postimg = end($postimg); //Get last item of array: "url"
}
$postimg = '<p><img src="' . $postimg . '" alt="-" /></p>';
} else {
$postimg = '';
}
// Paywall
$paywall = $html->find('div.paywall-restriction', 0);
if (is_object($paywall) && is_object($paywall->find('p.red-msg', 0))) {
$paywall = '<p><em>' . $paywall->find('span.head-mention', 0)->innertext . '</em></p>';
} else {
$paywall = '';
}
// Content
$article_content = $article_content->outertext;
$article_content = str_replace('>Signaler une erreur</span>', '></span>', $article_content);
// Result
$text = $subtitle
. $postimg
. $html->find('div[itemprop=articleBody], div.brief-inner-content', 0)->outertext;
. $article_content
. $paywall;
} else {
$text = $item['content']
. '<p><em>Failed retrieve full article content</em></p>';
}
$premium_article = $html->find('h2.title_reserve_article', 0);
if (is_object($premium_article)) {
$text .= '<p><em>' . $premium_article->innertext . '</em></p>';
$text = '<p><em>Failed to retrieve full article content</em></p>';
if (isset($item['content'])) {
$text = $item['content'] . $text;
}
}
return $text;

View file

@ -148,7 +148,7 @@ class NineGagBridge extends BridgeAbstract {
}
if (!$AvoidElement) {
$item['uri'] = $post['url'];
$item['uri'] = preg_replace('/^http:/i', 'https:', $post['url']);
$item['title'] = $post['title'];
$item['content'] = self::getContent($post);
$item['categories'] = self::getCategories($post);

View file

@ -0,0 +1,131 @@
<?php
ini_set('max_execution_time', '300');
class NordbayernBridge extends BridgeAbstract {
const MAINTAINER = 'schabi.org';
const NAME = 'Nordbayern Bridge';
const CACHE_TIMEOUT = 3600;
const URI = 'https://www.nordbayern.de';
const DESCRIPTION = 'Bridge for Bavarian reginoal news site nordbayern.de';
const PARAMETERS = array( array(
'region' => array(
'name' => 'region',
'type' => 'list',
'exampleValue' => 'Nürnberg',
'title' => 'Select a region',
'values' => array(
'Nürnberg' => 'nuernberg',
'Fürth' => 'fuerth',
'Altdorf' => 'altdorf',
'Ansbach' => 'ansbach',
'Bad Windsheim' => 'bad-windsheim',
'Bamberg' => 'bamberg',
'Dinkelsbühl/Feuchtwangen' => 'dinkelsbuehl-feuchtwangen',
'Feucht' => 'feucht',
'Forchheim' => 'forchheim',
'Gunzenhausen' => 'gunzenhausen',
'Hersbruck' => 'hersbruck',
'Herzogenaurach' => 'herzogenaurach',
'Hilpolstein' => 'holpolstein',
'Höchstadt' => 'hoechstadt',
'Lauf' => 'lauf',
'Neumarkt' => 'neumarkt',
'Neustadt/Aisch' => 'neustadt-aisch',
'Pegnitz' => 'pegnitz',
'Roth' => 'roth',
'Rothenburg o.d.T.' => 'rothenburg-o-d-t',
'Schwabach' => 'schwabach',
'Treuchtlingen' => 'treuchtlingen',
'Weißenburg' => 'weissenburg'
)
),
'policeReports' => array(
'name' => 'Police Reports',
'type' => 'checkbox',
'exampleValue' => 'checked',
'title' => 'Read Police Reports',
)
));
private function getImageUrlFromScript($script) {
preg_match(
"#src=\\\\'(https:[-:\\.\\\\/a-zA-Z0-9%_]*\\.(jpg|JPG))#",
$script->innertext,
$matches,
PREG_OFFSET_CAPTURE
);
if(isset($matches[1][0])) {
return stripcslashes($matches[1][0]) . '?w=800';
}
return null;
}
private function handleArticle($link) {
$item = array();
$article = getSimpleHTMLDOM($link);
$content = $article->find('div[class*=article-content]', 0);
$item['uri'] = $link;
$item['title'] = $article->find('h1', 0)->innertext;
$item['content'] = '';
//first get image from block/modul
$figure = $article->find('figure[class*=panorama]', 0);
if($figure !== null) {
$imgUrl = self::getImageUrlFromScript($figure->find('script', 0));
if($imgUrl === null) {
$imgUrl = self::getImageUrlFromScript($figure->find('script', 1));
}
$item['content'] .= '<img src="' . $imgUrl . '">';
}
// get regular paragraphs
foreach($content->children() as $child) {
if($child->tag === 'p') {
$item['content'] .= $child;
}
}
//get image divs
foreach($content->find('div[class*=article-slideshow]') as $slides) {
foreach($slides->children() as $child) {
switch($child->tag) {
case 'p':
$item['content'] .= $child;
break;
case 'h5':
$item['content'] .= '<h5><a href="'
. self::URI . $child->find('a', 0)->href . '">' . $child->plaintext . '</a></h5>';
break;
case 'a':
$url = self::getImageUrlFromScript($child->find('script', 0));
$item['content'] .= '<img src="' . $url . '">';
break;
}
}
}
$this->items[] = $item;
$article->clear();
}
private function handleNewsblock($listSite, $readPoliceReports) {
$newsBlocks = $listSite->find('section[class*=newsblock]');
$regionalNewsBlock = $newsBlocks[0];
$policeBlock = $newsBlocks[1];
foreach($regionalNewsBlock->find('h2') as $headline) {
self::handleArticle(self::URI . $headline->find('a', 0)->href);
}
if($readPoliceReports === true) {
foreach($policeBlock->find('h2') as $headline) {
self::handleArticle(self::URI . $headline->find('a', 0)->href);
}
}
}
public function collectData() {
$item = array();
$region = $this->getInput('region');
$listSite = getSimpleHTMLDOM(self::URI . '/region/' . $region);
self::handleNewsblock($listSite, $this->getInput('policeReports'));
}
}

View file

@ -100,7 +100,9 @@ class NyaaTorrentsBridge extends BridgeAbstract {
//Retrieve data from page contents
$item_title = str_replace(' :: Nyaa', '', $item_html->find('title', 0)->plaintext);
$item_desc = str_get_html(markdownToHtml($item_html->find('#torrent-description', 0)->innertext));
$item_desc = str_get_html(
markdownToHtml(html_entity_decode($item_html->find('#torrent-description', 0)->innertext))
);
$item_author = extractFromDelimiters($item_html->outertext, 'href="/user/', '"');
$item_date = intval(extractFromDelimiters($item_html->outertext, 'data-timestamp="', '"'));

View file

@ -0,0 +1,37 @@
<?php
class OpenwrtSecurityBridge extends BridgeAbstract {
const NAME = 'OpenWrt Security Advisories';
const URI = 'https://openwrt.org/advisory/start';
const DESCRIPTION = 'Security Advisories published by openwrt.org';
const MAINTAINER = 'mschwld';
const CACHE_TIMEOUT = 3600;
const WEBROOT = 'https://openwrt.org';
public function collectData() {
$item = array();
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request entries');
$advisories = $html->find('div[class=plugin_nspages]', 0);
foreach($advisories->find('a[class=wikilink1]') as $element) {
$item = array();
$row = $element->innertext;
$item['title'] = substr($row, 0, strpos($row, ' - '));
$item['timestamp'] = $this->getDate($element->href);
$item['uri'] = self::WEBROOT . $element->href;
$item['uid'] = self::WEBROOT . $element->href;
$item['content'] = substr($row, strpos($row, ' - ') + 3);
$item['author'] = 'OpenWrt Project';
$this->items[] = $item;
}
}
private function getDate($href) {
$date = substr($href, -12);
return $date;
}
}

View file

@ -0,0 +1,175 @@
<?php
class OtrkeyFinderBridge extends BridgeAbstract {
const MAINTAINER = 'mibe';
const NAME = 'OtrkeyFinder';
const URI = 'https://otrkeyfinder.com';
const URI_TEMPLATE = 'https://otrkeyfinder.com/en/?search=%s&order=&page=%d';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns the newest .otrkey files matching the search criteria.';
const PARAMETERS = array(
array(
'searchterm' => array(
'name' => 'Search term',
'exampleValue' => 'Terminator',
'title' => 'The search term is case-insensitive',
),
'station' => array(
'name' => 'Station name',
'exampleValue' => 'ARD',
),
'type' => array(
'name' => 'Media type',
'type' => 'list',
'values' => array(
'any' => '',
'Detail' => array(
'HD' => 'HD.avi',
'AC3' => 'HD.ac3',
'HD &amp; AC3' => 'HD.',
'HQ' => 'HQ.avi',
'AVI' => 'g.avi', // 'g.' to exclude HD.avi and HQ.avi (filename always contains 'mpg.')
'MP4' => '.mp4',
),
),
),
'minTime' => array(
'name' => 'Min. running time',
'type' => 'number',
'title' => 'The minimum running time in minutes. The resolution is 5 minutes.',
'exampleValue' => '90',
'defaultValue' => '0',
),
'maxTime' => array(
'name' => 'Max. running time',
'type' => 'number',
'title' => 'The maximum running time in minutes. The resolution is 5 minutes.',
'exampleValue' => '120',
'defaultValue' => '0',
),
'pages' => array(
'name' => 'Number of pages',
'type' => 'number',
'title' => 'Specifies the number of pages to fetch. Increase this value if you get an empty feed.',
'exampleValue' => '5',
'defaultValue' => '5',
),
)
);
// Example: Terminator_20.04.13_02-25_sf2_100_TVOON_DE.mpg.avi.otrkey
// The first group is the running time in minutes
const FILENAME_REGEX = '/_(\d+)_TVOON_DE\.mpg\..+\.otrkey/';
// year.month.day_hour-minute with leading zeros
const TIME_REGEX = '/\d{2}\.\d{2}\.\d{2}_\d{2}-\d{2}/';
const CONTENT_TEMPLATE = '<ul>%s</ul>';
const MIRROR_TEMPLATE = '<li><a href="https://otrkeyfinder.com%s">%s</a></li>';
public function collectData() {
$pages = $this->getInput('pages');
for($page = 1; $page <= $pages; $page++) {
$uri = $this->buildUri($page);
$html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT)
or returnServerError('Could not request ' . $uri);
$keys = $html->find('div.otrkey');
foreach($keys as $key) {
$temp = $this->buildItem($key);
if ($temp != null)
$this->items[] = $temp;
}
// Sleep for 0.5 seconds to don't hammer the server.
usleep(500000);
}
}
private function buildUri($page) {
$searchterm = $this->getInput('searchterm');
$station = $this->getInput('station');
$type = $this->getInput('type');
// Combine all three parts to a search query by separating them with white space
$search = implode(' ', array($searchterm, $station, $type));
$search = trim($search);
$search = urlencode($search);
return sprintf(self::URI_TEMPLATE, $search, $page);
}
private function buildItem(simple_html_dom_node $node) {
$file = $this->getFilename($node);
if ($file == null)
return null;
$minTime = $this->getInput('minTime');
$maxTime = $this->getInput('maxTime');
// Do we need to check the running time?
if ($minTime != 0 || $maxTime != 0) {
if ($maxTime > 0 && $maxTime < $minTime)
returnClientError('The minimum running time must be less than the maximum running time.');
preg_match(self::FILENAME_REGEX, $file, $matches);
if (!isset($matches[1]))
return null;
$time = (integer)$matches[1];
// Check for minimum running time
if ($minTime > 0 && $minTime > $time)
return null;
// Check for maximum running time
if ($maxTime > 0 && $maxTime < $time)
return null;
}
$item = array();
$item['title'] = $file;
// The URI_TEMPLATE for querying the site can be reused here
$item['uri'] = sprintf(self::URI_TEMPLATE, $file, 1);
$content = $this->buildContent($node);
if ($content != null)
$item['content'] = $content;
if (preg_match(self::TIME_REGEX, $file, $matches) === 1) {
$item['timestamp'] = DateTime::createFromFormat(
'y.m.d_H-i',
$matches[0],
new DateTimeZone('Europe/Berlin')
)->getTimestamp();
}
return $item;
}
private function getFilename(simple_html_dom_node $node) {
$file = $node->find('.file', 0);
if ($file == null)
return null;
else
return trim($file->innertext);
}
private function buildContent(simple_html_dom_node $node) {
$mirrors = $node->find('div.mirror');
$list = '';
// Build list of available mirrors
foreach($mirrors as $mirror) {
$anchor = $mirror->find('a', 0);
$list .= sprintf(self::MIRROR_TEMPLATE, $anchor->href, $anchor->innertext);
}
return sprintf(self::CONTENT_TEMPLATE, $list);
}
}

View file

@ -2,22 +2,43 @@
class PcGamerBridge extends BridgeAbstract
{
const NAME = 'PC Gamer';
const URI = 'https://www.pcgamer.com/';
const URI = 'https://www.pcgamer.com/archive/';
const DESCRIPTION = 'PC Gamer Most Read Stories';
const MAINTAINER = 'mdemoss';
const CACHE_TIMEOUT = 3600;
const MAINTAINER = 'IceWreck, mdemoss';
public function collectData()
{
$html = getSimpleHTMLDOMCached($this->getURI(), 300);
$stories = $html->find('div#popularcontent li.most-popular-item');
$stories = $html->find('ul.basic-list li.day-article');
$i = 0;
// Find induvidual stories in the archive page
foreach ($stories as $element) {
if($i == 15) break;
$item['uri'] = $element->find('a', 0)->href;
// error_log(print_r($item['uri'], TRUE));
$articleHtml = getSimpleHTMLDOMCached($item['uri']);
$item['title'] = $element->find('h4 a', 0)->plaintext;
$item['title'] = $element->find('a', 0)->plaintext;
$item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content);
$item['content'] = $articleHtml->find('meta[name=description]', 0)->content;
$item['author'] = $articleHtml->find('a[itemprop=author]', 0)->plaintext;
$item['author'] = $articleHtml->find('span.by-author a', 0)->plaintext;
// Get the article content
$articleContents = $articleHtml->find('#article-body', 0);
/*
By default the img src has a link to an error image and then the actual image
is added in by JS. So we replace the error image with the actual full size image
whoose link is in one of the attributes of the img tag
*/
foreach($articleContents->find('img') as $img) {
$imgsrc = $img->getAttribute('data-original-mos');
// error_log($imgsrc);
$img->src = $imgsrc;
}
$item['content'] = $articleContents;
$this->items[] = $item;
$i++;
}
}
}

View file

@ -0,0 +1,22 @@
<?php
class PhoronixBridge extends FeedExpander {
const MAINTAINER = 'IceWreck';
const NAME = 'Phoronix Bridge';
const URI = 'https://www.phoronix.com';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'RSS feed for Linux news website Phoronix';
public function collectData(){
$this->collectExpandableDatas('https://www.phoronix.com/rss.php', 15);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
// $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link);
$article = $articlePage->find('.content', 0);
$item['content'] = $article;
return $item;
}
}

99
bridges/PornhubBridge.php Normal file
View file

@ -0,0 +1,99 @@
<?php
class PornhubBridge extends BridgeAbstract {
const MAINTAINER = 'Mitsukarenai';
const NAME = 'Pornhub';
const URI = 'https://www.pornhub.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns videos from specified user,model,pornstar';
const PARAMETERS = array(array(
'q' => array(
'name' => 'User name',
'required' => true,
),
'type' => array(
'name' => 'User type',
'type' => 'list',
'values' => array(
'user' => 'users',
'model' => 'model',
'pornstar' => 'pornstar',
),
'defaultValue' => 'users',
),
'sort' => array(
'name' => 'Sort by',
'type' => 'list',
'values' => array(
'Most recent' => '?',
'Most views' => '?o=mv',
'Top rated' => '?o=tr',
'Longest' => '?o=lg',
),
'defaultValue' => '?',
),
'show_images' => array(
'name' => 'Show thumbnails',
'type' => 'checkbox',
),
));
public function getName(){
if(!is_null($this->getInput('type')) && !is_null($this->getInput('q'))) {
return 'PornHub ' . $this->getInput('type') . ':' . $this->getInput('q');
}
return parent::getName();
}
public function collectData() {
$uri = 'https://www.pornhub.com/' . $this->getInput('type') . '/';
switch($this->getInput('type')) { // select proper permalink format per user type...
case 'model':
$uri .= urlencode($this->getInput('q')) . '/videos' . $this->getInput('sort'); break;
case 'users':
$uri .= urlencode($this->getInput('q')) . '/videos/public' . $this->getInput('sort'); break;
case 'pornstar':
$uri .= urlencode($this->getInput('q')) . '/videos/upload' . $this->getInput('sort'); break;
}
$show_images = $this->getInput('show_images');
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request PornHub.');
foreach($html->find('div.videoUList ul.videos li.videoblock') as $element) {
$item = array();
$item['author'] = $this->getInput('q');
// Title
$title = $element->find('a', 0)->getAttribute('title');
if (is_null($title)) {
continue;
}
$item['title'] = $title;
// Url
$url = $element->find('a', 0)->href;
$item['uri'] = 'https://www.pornhub.com' . $url;
// Content
$image = $element->find('img', 0)->getAttribute('data-src');
if($show_images === true) {
$item['content'] = '<a href="' . $item['uri'] . '"><img src="' . $image . '"></a>';
}
// date hack, guess upload YYYYMMDD from thumbnail URL (format: https://ci.phncdn.com/videos/201907/25/--- )
$uploaded = explode('/', $image);
$uploaded = strtotime($uploaded[4] . $uploaded[5]);
$item['timestamp'] = $uploaded;
$this->items[] = $item;
}
}
}

View file

@ -0,0 +1,56 @@
<?php
class RaceDepartmentBridge extends FeedExpander {
const NAME = 'RaceDepartment News';
const URI = 'https://racedepartment.com/';
const DESCRIPTION = 'Get the latest (sim)racing news from RaceDepartment.';
const MAINTAINER = 't0stiman';
public function collectData() {
$this->collectExpandableDatas('https://www.racedepartment.com/news/archive.rss', 10);
}
protected function parseItem($feedItem) {
$item = parent::parseItem($feedItem);
//fetch page
$articlePage = getSimpleHTMLDOMCached($feedItem->link)
or returnServerError('Could not retrieve ' . $feedItem->link);
//extract article
$item['content'] = $articlePage->find('div.thfeature_firstPost', 0);
//convert iframes to links. meant for embedded videos.
foreach($item['content']->find('iframe') as $found) {
$iframeUrl = $found->getAttribute('src');
if ($iframeUrl) {
$found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>';
}
}
//get rid of some elements we don't need
$to_remove_selectors = array(
'div.p-title', //title
'ul.listInline', //Thread starter, Start date
'div.rd_news_article_share_buttons',
'div.thfeature_firstPost-author',
'div.reactionsBar',
'footer',
'div.message-lastEdit',
'section.message-attachments'
);
foreach($to_remove_selectors as $selector) {
foreach($item['content']->find($selector) as $found) {
$found->outertext = '';
}
}
//category
$forumPath = $articlePage->find('div.breadcrumb', 0);
$pathElements = $forumPath->find('span');
$item['categories'] = array(end($pathElements)->innertext);
return $item;
}
}

View file

@ -2,19 +2,18 @@
class RainbowSixSiegeBridge extends BridgeAbstract {
const MAINTAINER = 'corenting';
const NAME = 'Rainbow Six Siege Blog';
const URI = 'https://rainbow6.ubisoft.com/siege/en-us/news/';
const NAME = 'Rainbow Six Siege News';
const URI = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege/news-updates';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Latest articles from the Rainbow Six Siege blog';
const DESCRIPTION = 'Latest news about Rainbow Six Siege';
public function getIcon() {
return 'https://ubistatic19-a.akamaihd.net/resource/en-us/game/rainbow6/siege-v3/r6s-favicon_316592.ico';
return 'https://static-dm.akamaized.net/siege/prod/favicon-144x144.png';
}
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%3A150-194572-64';
$dlUrl .= '&keywordList=233416%2C316144%2C233418%2C233417&siteId=undefined&useSeoFriendlyUrl=true';
$dlUrl = 'https://www.ubisoft.com/api/updates/items?locale=en-us&categoriesFilter=all';
$dlUrl = $dlUrl . '&limit=6&mediaFilter=news&skip=0&startIndex=undefined&tags=BR-rainbow-six%20GA-siege';
$jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
$json = json_decode($jsonString, true);
@ -22,17 +21,20 @@ class RainbowSixSiegeBridge extends BridgeAbstract {
// Start at index 2 to remove highlighted articles
for($i = 0; $i < count($json); $i++) {
$jsonItem = $json[$i]['Content'];
$article = str_get_html($jsonItem);
$jsonItem = $json[$i];
$uri = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege';
$uri = $uri . $jsonItem['button']['buttonUrl'];
$thumbnail = '<img src="' . $jsonItem['thumbnail']['url'] . '" alt="Thumbnail">';
$content = $thumbnail . '<br />' . markdownToHtml($jsonItem['content']);
$item = array();
$uri = $article->find('h3 a', 0)->href;
$uri = 'https://rainbow6.ubisoft.com' . $uri;
$item['uri'] = $uri;
$item['title'] = $article->find('h3', 0)->plaintext;
$item['content'] = $article->find('img', 0)->outertext . '<br />' . $article->find('strong', 0)->plaintext;
$item['timestamp'] = strtotime($article->find('p.news_date', 0)->plaintext);
$item['id'] = $jsonItem['id'];
$item['title'] = $jsonItem['title'];
$item['content'] = $content;
$item['timestamp'] = strtotime($jsonItem['date']);
$this->items[] = $item;
}

View file

@ -1,10 +1,11 @@
<?php
class RedditBridge extends FeedExpander {
const MAINTAINER = 'leomaradan';
class RedditBridge extends BridgeAbstract {
const MAINTAINER = 'dawidsowa';
const NAME = 'Reddit Bridge';
const URI = 'https://www.reddit.com/';
const DESCRIPTION = 'Reddit RSS Feed fixer';
const URI = 'https://www.reddit.com';
const DESCRIPTION = 'Return hot submissions from Reddit';
const PARAMETERS = array(
'single' => array(
@ -25,16 +26,153 @@ class RedditBridge extends FeedExpander {
)
);
public function collectData(){
switch($this->queriedContext) {
case 'single': $subreddits[] = $this->getInput('r'); break;
case 'multi': $subreddits = explode(',', $this->getInput('rs')); break;
public function getIcon() {
return 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png';
}
public function getName() {
if ($this->queriedContext == 'single') {
return 'Reddit r/' . $this->getInput('r');
} else {
return self::NAME;
}
}
public function collectData() {
switch ($this->queriedContext) {
case 'single':
$subreddits[] = $this->getInput('r');
break;
case 'multi':
$subreddits = explode(',', $this->getInput('rs'));
break;
}
foreach ($subreddits as $subreddit) {
$name = trim($subreddit);
$this->collectExpandableDatas("https://www.reddit.com/r/$name/.rss");
$values = getContents(self::URI . '/r/' . $name . '.json')
or returnServerError('Unable to fetch posts!');
$decodedValues = json_decode($values);
foreach ($decodedValues->data->children as $post) {
$data = $post->data;
$item = array();
$item['author'] = $data->author;
$item['title'] = $data->title;
$item['uid'] = $data->id;
$item['timestamp'] = $data->created_utc;
$item['uri'] = $this->encodePermalink($data->permalink);
$item['categories'] = array();
$item['categories'][] = $data->link_flair_text;
$item['categories'][] = $data->pinned ? 'Pinned' : null;
$item['categories'][] = $data->over_18 ? 'NSFW' : null;
$item['categories'][] = $data->spoiler ? 'Spoiler' : null;
$item['categories'] = array_filter($item['categories']);
if ($data->is_self) {
// Text post
$item['content']
= htmlspecialchars_decode($data->selftext_html);
} elseif (isset($data->post_hint) ? $data->post_hint == 'link' : false) {
// Link with preview
if (isset($data->media)) {
// Reddit embeds content for some sites (e.g. Twitter)
$embed = htmlspecialchars_decode(
$data->media->oembed->html
);
} else {
$embed = '';
}
$item['content'] = $this->template(
$data->url,
$data->thumbnail,
$data->domain
) . $embed;
} elseif (isset($data->post_hint) ? $data->post_hint == 'image' : false) {
// Single image
$item['content'] = $this->link(
$this->encodePermalink($data->permalink),
'<img src="' . $data->url . '" />'
);
} elseif (isset($data->is_gallery) ? $data->is_gallery : false) {
// Multiple images
$images = array();
foreach ($data->gallery_data->items as $media) {
$id = $media->media_id;
$type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u';
$src = $data->media_metadata->$id->s->$type;
$images[] = '<img src="' . $src . '"/>';
}
$item['content'] = implode('', $images);
} elseif ($data->is_video) {
// Video
// Higher index -> Higher resolution
end($data->preview->images[0]->resolutions);
$index = key($data->preview->images[0]->resolutions);
$item['content'] = $this->template(
$data->url,
$data->preview->images[0]->resolutions[$index]->url,
'Video'
);
} elseif (isset($data->media) ? $data->media->type == 'youtube.com' : false) {
// Youtube link
$item['content'] = $this->template(
$data->url,
$data->media->oembed->thumbnail_url,
'YouTube');
} elseif (explode('.', $data->domain)[0] == 'self') {
// Crossposted text post
// TODO (optionally?) Fetch content of the original post.
$item['content'] = $this->link(
$this->encodePermalink($data->permalink),
'Crossposted from r/'
. explode('.', $data->domain)[1]
);
} else {
// Link WITHOUT preview
$item['content'] = $this->link($data->url, $data->domain);
}
$this->items[] = $item;
}
}
}
private function encodePermalink($link) {
return self::URI . implode(
'/',
array_map('urlencode', explode('/', $link))
);
}
private function template($href, $src, $caption) {
return '<a href="' . $href . '"><figure><figcaption>'
. $caption . '</figcaption><img src="'
. $src . '"/></figure></a>';
}
private function link($href, $text) {
return '<a href="' . $href . '">' . $text . '</a>';
}
}

View file

@ -5,13 +5,16 @@ class Releases3DSBridge extends BridgeAbstract {
const NAME = '3DS Scene Releases';
const URI = 'http://www.3dsdb.com/';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the newest scene releases.';
const DESCRIPTION = 'Returns the newest scene releases for Nintendo 3DS.';
public function collectData(){
$this->collectDataUrl(self::URI . 'xml.php');
}
protected function collectDataUrl($dataUrl){
$dataUrl = self::URI . 'xml.php';
$xml = getContents($dataUrl)
or returnServerError('Could not request 3dsdb: ' . $dataUrl);
or returnServerError('Could not request URL: ' . $dataUrl);
$limit = 0;
foreach(array_reverse(explode('<release>', $xml)) as $element) {
@ -52,17 +55,25 @@ class Releases3DSBridge extends BridgeAbstract {
$ignSearchUrl = 'https://www.ign.com/search?q=' . urlencode($name);
if($ignResult = getSimpleHTMLDOMCached($ignSearchUrl)) {
$ignCoverArt = $ignResult->find('div.search-item-media', 0)->find('img', 0)->src;
$ignDesc = $ignResult->find('div.search-item-description', 0)->plaintext;
$ignLink = $ignResult->find('div.search-item-sub-title', 0)->find('a', 1)->href;
$ignDate = strtotime(trim($ignResult->find('span.publish-date', 0)->plaintext));
$ignDescription = '<div><img src="'
. $ignCoverArt
. '" /></div><div>'
. $ignDesc
. ' <a href="'
. $ignLink
. '">More at IGN</a></div>';
$ignCoverArt = $ignResult->find('div.search-item-media', 0);
$ignDesc = $ignResult->find('div.search-item-description', 0);
$ignLink = $ignResult->find('div.search-item-sub-title', 0);
$ignDate = $ignResult->find('span.publish-date', 0);
if (is_object($ignCoverArt))
$ignCoverArt = $ignCoverArt->find('img', 0);
if (is_object($ignLink))
$ignLink = $ignLink->find('a', 1);
if (is_object($ignDate))
$ignDate = strtotime(trim($ignDate->plaintext));
if (is_object($ignCoverArt) && is_object($ignDesc) && is_object($ignLink)) {
$ignDescription = '<div><img src="'
. $ignCoverArt->src
. '" /></div><div>'
. $ignDesc->plaintext
. ' <a href="'
. $ignLink->href
. '">More at IGN</a></div>';
}
}
//Main section : Release description from 3DS database
@ -111,7 +122,7 @@ class Releases3DSBridge extends BridgeAbstract {
private function typeToString($type){
switch($type) {
case 1: return '3DS Game';
case 1: return 'Card Game';
case 4: return 'eShop';
default: return '??? (' . $type . ')';
}

View file

@ -0,0 +1,17 @@
<?php
// This bridge depends on Releases3DSBridge
if (!class_exists('Releases3DSBridge')) {
include('Releases3DSBridge.php');
}
class ReleasesSwitchBridge extends Releases3DSBridge {
const NAME = 'Switch Scene Releases';
const URI = 'http://www.nswdb.com/';
const DESCRIPTION = 'Returns the newest scene releases for Nintendo Switch.';
public function collectData(){
$this->collectDataUrl(self::URI . 'xml.php');
}
}

View file

@ -8,6 +8,7 @@ class ReporterreBridge extends BridgeAbstract {
private function extractContent($url){
$html2 = getSimpleHTMLDOM($url);
$html2 = defaultLinkTo($html2, self::URI);
foreach($html2->find('div[style=text-align:justify]') as $e) {
$text = $e->outertext;
@ -16,13 +17,6 @@ class ReporterreBridge extends BridgeAbstract {
$html2->clear();
unset($html2);
// Replace all relative urls with absolute ones
$text = preg_replace(
'/(href|src)(\=[\"\'])(?!http)([^"\']+)/ims',
'$1$2' . self::URI . '$3',
$text
);
$text = strip_tags($text, '<p><br><a><img>');
return $text;
}

View file

@ -0,0 +1,27 @@
<?php
class RobinhoodSnacksBridge extends BridgeAbstract {
const MAINTAINER = 'johnpc';
const NAME = 'Robinhood Snacks Newsletter';
const URI = 'https://snacks.robinhood.com/newsletters/';
const CACHE_TIMEOUT = 86400; // 24h
const DESCRIPTION = 'Returns newsletters from Robinhood Snacks';
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request snacks.robinhood.com.');
foreach ($html->find('#root > div > div > div > div > div > a') as $element) {
if ($element->href === 'https://snacks.robinhood.com/newsletters/page/2/') {
continue;
}
$this->items[] = array(
'uri' => $element->href,
'title' => $element->find('div > div', 3)->plaintext,
'content' => $element->find('div > div', 4)->plaintext,
);
}
}
}

View file

@ -0,0 +1,107 @@
<?php
class RoosterTeethBridge extends BridgeAbstract {
const MAINTAINER = 'tgkenney';
const NAME = 'Rooster Teeth';
const URI = 'https://roosterteeth.com';
const DESCRIPTION = 'Gets the latest channel videos from the Rooster Teeth website';
const API = 'https://svod-be.roosterteeth.com/';
const PARAMETERS = array(
'Options' => array(
'channel' => array(
'type' => 'list',
'name' => 'Channel',
'title' => 'Select a channel to filter by',
'values' => array(
'All channels' => 'all',
'Achievement Hunter' => 'achievement-hunter',
'Cow Chop' => 'cow-chop',
'Death Battle' => 'death-battle',
'Funhaus' => 'funhaus',
'Inside Gaming' => 'inside-gaming',
'JT Music' => 'jt-music',
'Kinda Funny' => 'kinda-funny',
'Rooster Teeth' => 'rooster-teeth',
'Sugar Pine 7' => 'sugar-pine-7'
)
),
'sort' => array(
'type' => 'list',
'name' => 'Sort',
'title' => 'Select a sort order',
'values' => array(
'Newest -> Oldest' => 'desc',
'Oldest -> Newest' => 'asc'
),
'defaultValue' => 'desc'
),
'first' => array(
'type' => 'list',
'name' => 'RoosterTeeth First',
'title' => 'Select whether to include "First" videos before they are public',
'values' => array(
'True' => true,
'False' => false
)
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'required' => false,
'title' => 'Maximum number of items to return',
'defaultValue' => 10
)
)
);
public function collectData() {
if ($this->getInput('channel') !== 'all') {
$uri = self::API
. 'api/v1/episodes?per_page='
. $this->getInput('limit')
. '&channel_id='
. $this->getInput('channel')
. '&order=' . $this->getInput('sort')
. '&page=1';
$htmlJSON = getSimpleHTMLDOM($uri)
or returnServerError('Could not contact Rooster Teeth: ' . $uri);
} else {
$uri = self::API
. '/api/v1/episodes?per_page='
. $this->getInput('limit')
. '&filter=all&order='
. $this->getInput('sort')
. '&page=1';
$htmlJSON = getSimpleHTMLDOM($uri)
or returnServerError('Could not contact Rooster Teeth: ' . $uri);
}
$htmlArray = json_decode($htmlJSON, true);
foreach($htmlArray['data'] as $key => $value) {
$item = array();
if (!$this->getInput('first') && $value['attributes']['is_sponsors_only']) {
continue;
}
$publicDate = date_create($value['attributes']['member_golive_at']);
$dateDiff = date_diff($publicDate, date_create(), false);
if (!$this->getInput('first') && $dateDiff->invert == 1) {
continue;
}
$item['uri'] = self::URI . $value['canonical_links']['self'];
$item['title'] = $value['attributes']['title'];
$item['timestamp'] = $value['attributes']['member_golive_at'];
$item['author'] = $value['attributes']['show_title'];
$this->items[] = $item;
}
}
}

View file

@ -8,12 +8,14 @@ class Rule34pahealBridge extends Shimmie2Bridge {
const URI = 'https://rule34.paheal.net/';
const DESCRIPTION = 'Returns images from given page';
const PATHTODATA = '.shm-thumb';
protected function getItemFromElement($element){
$item = array();
$item['uri'] = $this->getURI() . $element->href;
$item['uri'] = rtrim($this->getURI(), '/') . $element->find('.shm-thumb-link', 0)->href;
$item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src;
$thumbnailUri = $element->find('a', 1)->href;
$item['tags'] = $element->getAttribute('data-tags');
$item['title'] = $this->getName() . ' | ' . $item['id'];
$item['content'] = '<a href="'

83
bridges/ScribdBridge.php Normal file
View file

@ -0,0 +1,83 @@
<?php
class ScribdBridge extends BridgeAbstract {
const NAME = 'Scribd Bridge';
const URI = 'https://www.scribd.com';
const DESCRIPTION = 'Returns documents uploaded by a user.';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'profile' => array(
'name' => 'Profile URL',
'type' => 'text',
'required' => true,
'title' => 'Profile URL. Example: https://www.scribd.com/user/489040929/number10leaks-com',
'exampleValue' => 'https://www.scribd.com/user/'
),
));
const CACHE_TIMEOUT = 3600;
private $profileUrlRegex = '/scribd\.com\/(user\/[0-9]+\/[\w-]+)\/?/';
private $feedName = '';
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
$header = $html->find('div.header', 0);
$this->feedName = $header->find('a', 0)->plaintext;
foreach($html->find('div.content ul li') as $index => $li) {
$item = array();
$item['title'] = $li->find('div.under_title', 0)->plaintext;
$item['uri'] = $li->find('a', 0)->href;
$item['author'] = $li->find('span.uploader', 0)->plaintext;
//$item['timestamp'] =
$item['uid'] = $li->find('a', 0)->href;
$pageHtml = getSimpleHTMLDOMCached($item['uri'], 3600)
or returnServerError('Could not request: ' . $item['uri']);
$image = $pageHtml->find('meta[property="og:image"]', 0)->content;
$description = $pageHtml->find('meta[property="og:description"]', 0)->content;
foreach ($pageHtml->find('ul.interest_pills li') as $pills) {
$item['categories'][] = $pills->plaintext;
}
$item['content'] = <<<EOD
<p>{$description}<p><p><img src="{$image}"></p>
EOD;
$item['enclosures'][] = $image;
$this->items[] = $item;
if (count($this->items) >= 15) {
break;
}
}
}
public function getName() {
if ($this->feedName) {
return $this->feedName . ' - Scribd';
}
return parent::getName();
}
public function getURI() {
if (!is_null($this->getInput('profile'))) {
preg_match($this->profileUrlRegex, $this->getInput('profile'), $user)
or returnServerError('Could not extract user ID and name from given profile URL.');
return self::URI . '/' . $user[1] . '/uploads';
}
return parent::getURI();
}
}

View file

@ -3,15 +3,11 @@ class SensCritiqueBridge extends BridgeAbstract {
const MAINTAINER = 'kranack';
const NAME = 'Sens Critique';
const URI = 'http://www.senscritique.com/';
const URI = 'https://www.senscritique.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Sens Critique news';
const PARAMETERS = array( array(
'm' => array(
'name' => 'Movies',
'type' => 'checkbox'
),
's' => array(
'name' => 'Series',
'type' => 'checkbox'
@ -40,8 +36,6 @@ class SensCritiqueBridge extends BridgeAbstract {
if($this->getInput($category)) {
$uri = self::URI;
switch($category) {
case 'm': $uri .= 'films/cette-semaine';
break;
case 's': $uri .= 'series/actualite';
break;
case 'g': $uri .= 'jeuxvideo/actualite';
@ -77,20 +71,25 @@ class SensCritiqueBridge extends BridgeAbstract {
. ' '
. $movie->find('.elco-date', 0)->plaintext;
$item['content'] = '<em>'
. $movie->find('.elco-original-title', 0)->plaintext
. '</em><br><br>'
. $movie->find('.elco-baseline', 0)->plaintext
$item['content'] = '';
$originalTitle = $movie->find('.elco-original-title', 0);
$description = $movie->find('.elco-description', 0);
if ($originalTitle) {
$item['content'] = '<em>' . $originalTitle->plaintext . '</em><br><br>';
}
$item['content'] .= $movie->find('.elco-baseline', 0)->plaintext
. '<br>'
. $movie->find('.elco-baseline', 1)->plaintext
. '<br><br>'
. $movie->find('.elco-description', 0)->plaintext
. ($description ? $description->plaintext : '')
. '<br><br>'
. trim($movie->find('.erra-ratings .erra-global', 0)->plaintext)
. ' / 10';
$item['id'] = $this->getURI() . $movie->find('.elco-title a', 0)->href;
$item['uri'] = $this->getURI() . $movie->find('.elco-title a', 0)->href;
$item['id'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/');
$item['uri'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/');
$this->items[] = $item;
}
}

View file

@ -0,0 +1,91 @@
<?php
class SeznamZpravyBridge extends BridgeAbstract {
const NAME = 'Seznam Zprávy Bridge';
const URI = 'https://seznamzpravy.cz';
const DESCRIPTION = 'Returns newest stories from Seznam Zprávy';
const MAINTAINER = 'thezeroalpha';
const PARAMETERS = array(
'By Author' => array(
'author' => array(
'name' => 'Author String',
'type' => 'text',
'required' => true,
'title' => 'The dash-separated author string, as shown in the URL bar.',
'pattern' => '[a-z]+-[a-z]+-[0-9]+',
'exampleValue' => 'janek-rubes-506'
),
)
);
private $feedName;
public function getName() {
if (isset($this->feedName)) {
return $this->feedName;
}
return parent::getName();
}
public function collectData() {
$ONE_DAY = 86500;
switch($this->queriedContext) {
case 'By Author':
$url = 'https://www.seznamzpravy.cz/autor/';
$selectors = array(
'breadcrumbs' => 'div[data-dot=ogm-breadcrumb-navigation]',
'article_list' => 'ul.ogm-document-timeline-page.atm-list-ul li article[data-dot=mol-timeline-item]',
'article_title' => 'a[data-dot=mol-article-card-title]',
'article_dm' => 'span.mol-formatted-date__date',
'article_time' => 'span.mol-formatted-date__time',
'article_content' => 'div[data-dot=ogm-article-content]'
);
$html = getSimpleHTMLDOMCached($url . $this->getInput('author'), $ONE_DAY);
$main_breadcrumbs = $html->find($selectors['breadcrumbs'], 0);
$author = $main_breadcrumbs->last_child()->plaintext
or returnServerError('Could not get author on: ' . $this->getURI());
$this->feedName = $author . ' - Seznam Zprávy';
$articles = $html->find($selectors['article_list'])
or returnServerError('Could not find articles on: ' . $this->getURI());
foreach ($articles as $article) {
$title_link = $article->find($selectors['article_title'], 0)
or returnServerError('Could not find title on: ' . $this->getURI());
$article_url = $title_link->href;
$article_content_html = getSimpleHTMLDOMCached($article_url, $ONE_DAY);
$content_e = $article_content_html->find($selectors['article_content'], 0);
$content_text = $content_e->innertext
or returnServerError('Could not get article content for: ' . $article_url);
$breadcrumbs_e = $article_content_html->find($selectors['breadcrumbs'], 0);
$breadcrumbs = $breadcrumbs_e->children();
$num_breadcrumbs = count($breadcrumbs);
$categories = array();
foreach ($breadcrumbs as $cat) {
if (--$num_breadcrumbs <= 0) {
break;
}
$categories[] = trim($cat->plaintext);
}
$article_dm_e = $article->find($selectors['article_dm'], 0);
$article_dm_text = $article_dm_e->plaintext;
$article_dmy = preg_replace('/[^0-9\.]/', '', $article_dm_text) . date('Y');
$article_time = $article->find($selectors['article_time'], 0)->plaintext;
$item = array(
'title' => $title_link->plaintext,
'uri' => $title_link->href,
'timestamp' => strtotime($article_dmy . ' ' . $article_time),
'author' => $author,
'content' => $content_text,
'categories' => $categories
);
$this->items[] = $item;
}
break;
}
$this->items[] = $item;
}
}

View file

@ -455,6 +455,35 @@ class SkimfeedBridge extends BridgeAbstract {
}
public function detectParameters($url) {
if (0 !== strpos($url, static::URI)) {
return null;
}
foreach(self::PARAMETERS as $channels) {
foreach($channels as $box_name => $box) {
foreach($box['values'] as $name => $channel_url) {
if (static::URI . $channel_url === $url) {
return array(
$box_name => $name,
);
}
}
}
}
return null;
}
public function getName() {
switch($this->queriedContext) {

View file

@ -11,44 +11,57 @@ class SoundCloudBridge extends BridgeAbstract {
'u' => array(
'name' => 'username',
'required' => true
),
't' => array(
'name' => 'type',
'type' => 'list',
'defaultValue' => 'tracks',
'values' => array(
'Tracks' => 'tracks',
'Playlists' => 'playlists'
)
)
));
private $feedTitle = null;
private $feedIcon = null;
private $clientIDCache = null;
public function collectData(){
$res = $this->apiGet('resolve', array(
'url' => 'http://www.soundcloud.com/' . $this->getInput('u')
'url' => 'https://soundcloud.com/' . $this->getInput('u')
)) or returnServerError('No results for this query');
$this->feedTitle = $res->username;
$this->feedIcon = $res->avatar_url;
$tracks = $this->apiGet('users/' . urlencode($res->id) . '/tracks')
or returnServerError('No results for this user');
$tracks = $this->apiGet(
'users/' . urlencode($res->id) . '/' . $this->getInput('t'),
array('limit' => 31)
) or returnServerError('No results for this user/playlist');
$numTracks = min(count($tracks), 10);
for($i = 0; $i < $numTracks; $i++) {
foreach ($tracks->collection as $index => $track) {
$item = array();
$item['author'] = $tracks[$i]->user->username;
$item['title'] = $tracks[$i]->user->username . ' - ' . $tracks[$i]->title;
$item['timestamp'] = strtotime($tracks[$i]->created_at);
$item['content'] = $tracks[$i]->description;
$item['enclosures'] = array($tracks[$i]->uri
. '/stream?client_id='
. $this->getClientID());
$item['author'] = $track->user->username;
$item['title'] = $track->user->username . ' - ' . $track->title;
$item['timestamp'] = strtotime($track->created_at);
$item['content'] = nl2br($track->description);
$item['enclosures'][] = $track->artwork_url;
$item['id'] = self::URI
. urlencode($this->getInput('u'))
. '/'
. urlencode($tracks[$i]->permalink);
. urlencode($track->permalink);
$item['uri'] = self::URI
. urlencode($this->getInput('u'))
. '/'
. urlencode($tracks[$i]->permalink);
. urlencode($track->permalink);
$this->items[] = $item;
}
if (count($this->items) >= 10) {
break;
}
}
}
public function getIcon(){
@ -59,9 +72,13 @@ class SoundCloudBridge extends BridgeAbstract {
return parent::getIcon();
}
public function getURI(){
return 'https://soundcloud.com/' . $this->getInput('u');
}
public function getName(){
if(!is_null($this->getInput('u'))) {
return self::NAME . ' - ' . $this->getInput('u');
if($this->feedTitle) {
return $this->feedTitle . ' - ' . self::NAME;
}
return parent::getName();
@ -95,14 +112,14 @@ class SoundCloudBridge extends BridgeAbstract {
// Without url=http, this returns a 404
$playerHTML = getContents('https://w.soundcloud.com/player/?url=http')
or returnServerError('Unable to get player page.');
or returnServerError('Unable to get player page.');
$regex = '/widget-.+?\.js/';
if(preg_match($regex, $playerHTML, $matches) == false)
returnServerError('Unable to find widget JS URL.');
$widgetURL = 'https://widget.sndcdn.com/' . $matches[0];
$widgetJS = getContents($widgetURL)
or returnServerError('Unable to get widget JS page.');
or returnServerError('Unable to get widget JS page.');
$regex = '/client_id.*?"(.+?)"/';
if(preg_match($regex, $widgetJS, $matches) == false)
returnServerError('Unable to find client ID.');
@ -113,13 +130,13 @@ class SoundCloudBridge extends BridgeAbstract {
}
private function buildAPIURL($endpoint, $parameters){
return 'https://api.soundcloud.com/'
return 'https://api-v2.soundcloud.com/'
. $endpoint
. '?'
. http_build_query($parameters);
}
private function apiGet($endpoint, $parameters = array()){
private function apiGet($endpoint, $parameters = array()) {
$parameters['client_id'] = $this->getClientID();
try {

View file

@ -39,8 +39,8 @@ class TelegramBridge extends BridgeAbstract {
$item = array();
$item['uri'] = $this->processUri($messageDiv);
$item['content'] = html_entity_decode($this->processContent($messageDiv), ENT_QUOTES);
$item['title'] = html_entity_decode($this->itemTitle, ENT_QUOTES);
$item['content'] = $this->processContent($messageDiv);
$item['title'] = $this->itemTitle;
$item['timestamp'] = $this->processDate($messageDiv);
$item['enclosures'] = $this->enclosures;
$author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext);

View file

@ -11,14 +11,14 @@ class TheCodingLoveBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request The Coding Love.');
foreach($html->find('div.post') as $element) {
foreach($html->find('article.blog-post') as $element) {
$item = array();
$temp = $element->find('h3 a', 0);
$temp = $element->find('h1 a', 0);
$titre = $temp->innertext;
$title = $temp->innertext;
$url = $temp->href;
$temp = $element->find('div.bodytype', 0);
$temp = $element->find('div.blog-post-content', 0);
// retrieve .gif instead of static .jpg
$images = $temp->find('p.e img');
@ -28,17 +28,13 @@ class TheCodingLoveBridge extends BridgeAbstract {
}
$content = $temp->innertext;
$auteur = $temp->find('i', 0);
$pos = strpos($auteur->innertext, 'by');
if($pos > 0) {
$auteur = trim(str_replace('*/', '', substr($auteur->innertext, ($pos + 2))));
$item['author'] = $auteur;
}
$temp = $element->find('div.post-meta-info', 0);
$author = $temp->find('span', 0);
$item['author'] = $author->innertext;
$item['content'] .= trim($content);
$item['uri'] = $url;
$item['title'] = trim($titre);
$item['title'] = trim($title);
$this->items[] = $item;
}

View file

@ -17,6 +17,7 @@ class TheHackerNewsBridge extends BridgeAbstract {
$article_url = $element->find('a.story-link', 0)->href;
$article_author = trim($element->find('i.icon-user', 0)->parent()->plaintext);
$article_author = str_replace('&#59396;', '', $article_author);
$article_title = $element->find('h2.home-title', 0)->plaintext;
//Date without time

View file

@ -0,0 +1,54 @@
<?php
class TinyLetterBridge extends BridgeAbstract {
const NAME = 'Tiny Letter';
const URI = 'https://tinyletter.com/';
const DESCRIPTION = 'Tiny Letter is a mailing list service';
const MAINTAINER = 'somini';
const PARAMETERS = array(
array(
'username' => array(
'name' => 'User Name',
'exampleValue' => 'forwards',
)
)
);
public function getName() {
$username = $this->getInput('username');
if (!is_null($username)) {
return static::NAME . ' | ' . $username;
}
return parent::getName();
}
public function getURI() {
$username = $this->getInput('username');
if (!is_null($username)) {
return static::URI . urlencode($username);
}
return parent::getURI();
}
public function collectData() {
$archives = self::getURI() . '/archive';
$html = getSimpleHTMLDOMCached($archives)
or returnServerError('Could not load content');
foreach($html->find('.message-list li') as $element) {
$item = array();
$snippet = $element->find('p.message-snippet', 0);
$link = $element->find('.message-link', 0);
$item['title'] = $link->plaintext;
$item['content'] = $snippet->innertext;
$item['uri'] = $link->href;
$item['timestamp'] = strtotime($element->find('.message-date', 0)->plaintext);
$this->items[] = $item;
}
}
}

View file

@ -20,7 +20,9 @@ class TwitchBridge extends BridgeAbstract {
'All' => 'all',
'Archive' => 'archive',
'Highlights' => 'highlight',
'Uploads' => 'upload'
'Uploads' => 'upload',
'Past Premieres' => 'past_premiere',
'Premiere Uploads' => 'premiere_upload'
),
'defaultValue' => 'archive'
)
@ -32,43 +34,90 @@ class TwitchBridge extends BridgeAbstract {
*/
const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
const API_ENDPOINT = 'https://gql.twitch.tv/gql';
const BROADCAST_TYPES = array(
'all' => array(
'ARCHIVE',
'HIGHLIGHT',
'UPLOAD',
'PAST_PREMIERE',
'PREMIERE_UPLOAD'
),
'archive' => 'ARCHIVE',
'highlight' => 'HIGHLIGHT',
'upload' => 'UPLOAD',
'past_premiere' => 'PAST_PREMIERE',
'premiere_upload' => 'PREMIERE_UPLOAD'
);
public function collectData(){
// get channel user
$query_data = array(
'login' => $this->getInput('channel')
$query = <<<'EOD'
query VODList($channel: String!, $types: [BroadcastType!]) {
user(login: $channel) {
displayName
videos(types: $types, sort: TIME) {
edges {
node {
id
title
publishedAt
lengthSeconds
viewCount
thumbnailURLs(width: 640, height: 360)
previewThumbnailURL(width: 640, height: 360)
description
tags
contentTags {
isLanguageTag
localizedName
}
game {
displayName
}
moments(momentRequestType: VIDEO_CHAPTER_MARKERS) {
edges {
node {
description
positionMilliseconds
}
}
}
}
}
}
}
}
EOD;
$variables = array(
'channel' => $this->getInput('channel'),
'types' => self::BROADCAST_TYPES[$this->getInput('type')]
);
$users = $this->apiGet('users', $query_data)->users;
if(count($users) === 0)
returnClientError('User "'
. $this->getInput('channel')
. '" could not be found');
$user = $users[0];
$data = $this->apiRequest($query, $variables);
// 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;
$user = $data->user;
foreach($user->videos->edges as $edge) {
$video = $edge->node;
$url = 'https://www.twitch.tv/videos/' . $video->id;
foreach($videos as $video) {
$item = array(
'uri' => $video->url,
'uri' => $url,
'title' => $video->title,
'timestamp' => $video->published_at,
'author' => $video->channel->display_name,
'timestamp' => $video->publishedAt,
'author' => $user->displayName,
);
// Add categories for tags and played game
$item['categories'] = array_filter(explode(' ', $video->tag_list));
if(!empty($video->game))
$item['categories'][] = $video->game;
$item['categories'] = $video->tags;
if(!is_null($video->game))
$item['categories'][] = $video->game->displayName;
foreach($video->contentTags as $tag)
if(!$tag->isLanguageTag)
$item['categories'][] = $tag->localizedName;
// Add enclosures for thumbnails from a few points in the video
$item['enclosures'] = array();
foreach($video->thumbnails->large as $thumbnail)
$item['enclosures'][] = $thumbnail->url;
// Thumbnail list has duplicate entries sometimes so remove those
$item['enclosures'] = array_unique($video->thumbnailURLs);
/*
* Content format example:
@ -86,44 +135,45 @@ class TwitchBridge extends BridgeAbstract {
*
*/
$item['content'] = '<p><a href="'
. $video->url
. $url
. '"><img src="'
. $video->preview->large
. $video->previewThumbnailURL
. '" /></a></p><p>'
. $video->description_html
. $video->description // in markdown format
. '</p><p><b>Duration:</b> '
. $this->formatTimestampTime($video->length)
. $this->formatTimestampTime($video->lengthSeconds)
. '<br/><b>Views:</b> '
. $video->views
. $video->viewCount
. '</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'] .= '<p><b>Played games:</b><ul>';
if(count($video->moments->edges) > 0) {
foreach($video->moments->edges as $edge) {
$moment = $edge->node;
$item['categories'][] = $moment->description;
$item['content'] .= '<li><a href="'
. $video->url
. $url
. '?t='
. $this->formatQueryTime($game_change->time)
. $this->formatQueryTime($moment->positionMilliseconds / 1000)
. '">'
. $this->formatTimestampTime($game_change->time)
. $this->formatTimestampTime($moment->positionMilliseconds / 1000)
. '</a> - '
. $game_change->label
. $moment->description
. '</li>';
}
} else {
$item['content'] .= '<li><a href="'
. $url
. '">00:00:00</a> - '
. ($video->game ? $video->game->displayName : 'No Game')
. '</li>';
}
$item['content'] .= '</ul></p>';
$item['categories'] = array_unique($item['categories']);
$this->items[] = $item;
}
}
@ -144,25 +194,37 @@ class TwitchBridge extends BridgeAbstract {
$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);
// GraphQL: https://graphql.org/
// Tool for developing/testing queries: https://github.com/skevy/graphiql-app
private function apiRequest($query, $variables) {
$request = array(
'query' => $query,
'variables' => $variables
);
$header = array(
'Client-ID: ' . self::CLIENT_ID
);
$opts = array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => json_encode($request)
);
$data = json_decode(getContents($url, $header))
or returnServerError('API request to "' . $url . '" failed.');
Debug::log("Sending GraphQL query:\n" . $query);
Debug::log("Sending GraphQL variables:\n"
. json_encode($variables, JSON_PRETTY_PRINT));
return $data;
$response = json_decode(getContents(self::API_ENDPOINT, $header, $opts))
or returnServerError('API request to "' . self::API_ENDPOINT . '" failed.');
Debug::log("Got GraphQL response:\n"
. json_encode($response, JSON_PRETTY_PRINT));
if(isset($response->errors)) {
$messages = array_column($response->errors, 'message');
returnServerError('API error(s): ' . implode("\n", $messages));
}
return $response->data;
}
public function getName(){

View file

@ -2,6 +2,9 @@
class TwitterBridge extends BridgeAbstract {
const NAME = 'Twitter Bridge';
const URI = 'https://twitter.com/';
const API_URI = 'https://api.twitter.com';
const GUEST_TOKEN_USES = 100;
const GUEST_TOKEN_EXPIRY = 300; // 5min
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'returns tweets';
const MAINTAINER = 'pmaziere';
@ -72,6 +75,12 @@ EOD
'required' => false,
'type' => 'checkbox',
'title' => 'Hide retweets'
),
'nopinned' => array(
'name' => 'Without pinned tweet',
'required' => false,
'type' => 'checkbox',
'title' => 'Hide pinned tweet'
)
),
'By list' => array(
@ -92,6 +101,20 @@ EOD
'required' => false,
'title' => 'Specify term to search for'
)
),
'By list ID' => array(
'listid' => array(
'name' => 'List ID',
'exampleValue' => '31748',
'required' => true,
'title' => 'Insert the list id'
),
'filter' => array(
'name' => 'Filter',
'exampleValue' => '#rss-bridge',
'required' => false,
'title' => 'Specify term to search for'
)
)
);
@ -142,6 +165,8 @@ EOD
break;
case 'By list':
return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
case 'By list ID':
return 'Twitter List #' . $this->getInput('listid');
default: return parent::getName();
}
return 'Twitter ' . $specific . $this->getInput($param);
@ -164,22 +189,46 @@ EOD
. urlencode($this->getInput('user'))
. '/lists/'
. str_replace(' ', '-', strtolower($this->getInput('list')));
case 'By list ID':
return self::URI
. 'i/lists/'
. urlencode($this->getInput('listid'));
default: return parent::getURI();
}
}
private function getApiURI() {
switch($this->queriedContext) {
case 'By keyword or hashtag':
return self::API_URI
. '/2/search/adaptive.json?q='
. urlencode($this->getInput('q'))
. '&tweet_mode=extended&tweet_search_mode=live';
case 'By username':
return self::API_URI
. '/2/timeline/profile/'
. $this->getRestId($this->getInput('u'))
. '.json?tweet_mode=extended';
case 'By list':
return self::API_URI
. '/2/timeline/list.json?list_id='
. $this->getListId($this->getInput('user'), $this->getInput('list'))
. '&tweet_mode=extended';
case 'By list ID':
return self::API_URI
. '/2/timeline/list.json?list_id='
. $this->getInput('listid')
. '&tweet_mode=extended';
default: returnServerError('Invalid query context !');
}
}
public function collectData(){
$html = '';
$page = $this->getURI();
$data = json_decode($this->getApiContents($this->getApiURI()));
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 => ''));
}
if(!$html) {
if(!$data) {
switch($this->queriedContext) {
case 'By keyword or hashtag':
returnServerError('No results for this query.');
@ -192,75 +241,93 @@ EOD
$hidePictures = $this->getInput('nopic');
foreach($html->find('div.js-stream-tweet') as $tweet) {
$promotedTweetIds = array_reduce($data->timeline->instructions[0]->addEntries->entries, function($carry, $entry) {
if (!isset($entry->content->item)) {
return $carry;
}
$tweet = $entry->content->item->content->tweet;
if (isset($tweet->promotedMetadata)) {
$carry[] = $tweet->id;
}
return $carry;
}, array());
// Skip retweets?
if($this->getInput('noretweet')
&& $tweet->find('div.context span.js-retweet-text a', 0)) {
$hidePinned = $this->getInput('nopinned');
if ($hidePinned) {
$pinnedTweetId = null;
if (isset($data->timeline->instructions[1]) && isset($data->timeline->instructions[1]->pinEntry)) {
$pinnedTweetId = $data->timeline->instructions[1]->pinEntry->entry->content->item->content->tweet->id;
}
}
foreach($data->globalObjects->tweets as $tweet) {
/* Debug::log('>>> ' . json_encode($tweet)); */
// Skip spurious retweets
if (isset($tweet->retweeted_status_id_str) && substr($tweet->full_text, 0, 4) === 'RT @') {
continue;
}
// remove 'invisible' content
foreach($tweet->find('.invisible') as $invisible) {
$invisible->outertext = '';
// Skip promoted tweets
if (in_array($tweet->id_str, $promotedTweetIds)) {
continue;
}
// Skip protmoted tweets
$heading = $tweet->previousSibling();
if(!is_null($heading) &&
$heading->getAttribute('class') === 'promoted-tweet-heading'
) {
// Skip pinned tweet
if ($hidePinned && $tweet->id_str === $pinnedTweetId) {
continue;
}
$item = array();
// extract username and sanitize
$item['username'] = htmlspecialchars_decode($tweet->getAttribute('data-screen-name'), ENT_QUOTES);
// extract fullname (pseudonym)
$item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES);
// get author
$user_info = $this->getUserInformation($tweet->user_id_str, $data->globalObjects);
$item['username'] = $user_info->screen_name;
$item['fullname'] = $user_info->name;
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
if($rt = $tweet->find('div.context span.js-retweet-text a', 0)) {
$item['author'] .= ' RT: @' . $rt->plaintext;
if (null !== $this->getInput('u') && strtolower($item['username']) != strtolower($this->getInput('u'))) {
$item['author'] .= ' RT: @' . $this->getInput('u');
}
// get avatar link
$item['avatar'] = $tweet->find('img', 0)->src;
// get TweetID
$item['id'] = $tweet->getAttribute('data-tweet-id');
// get tweet link
$item['uri'] = self::URI . substr($tweet->find('a.js-permalink', 0)->getAttribute('href'), 1);
$item['avatar'] = $user_info->profile_image_url_https;
$item['id'] = $tweet->id_str;
$item['uri'] = self::URI . $item['username'] . '/status/' . $item['id'];
// extract tweet timestamp
$item['timestamp'] = $tweet->find('span.js-short-timestamp', 0)->getAttribute('data-time');
// generate the title
$item['title'] = strip_tags($this->fixAnchorSpacing(htmlspecialchars_decode(
$tweet->find('p.js-tweet-text', 0), ENT_QUOTES), '<a>'));
$item['timestamp'] = $tweet->created_at;
switch($this->queriedContext) {
case 'By list':
// Check if filter applies to list (using raw content)
if($this->getInput('filter')) {
if(stripos($tweet->find('p.js-tweet-text', 0)->plaintext, $this->getInput('filter')) === false) {
continue 2; // switch + for-loop!
}
}
break;
default:
// Convert plain text URLs into HTML hyperlinks
$cleanedTweet = $tweet->full_text;
$foundUrls = false;
if (isset($tweet->entities->media)) {
foreach($tweet->entities->media as $media) {
$cleanedTweet = str_replace($media->url,
'<a href="' . $media->expanded_url . '">' . $media->display_url . '</a>',
$cleanedTweet);
$foundUrls = true;
}
}
if (isset($tweet->entities->urls)) {
foreach($tweet->entities->urls as $url) {
$cleanedTweet = str_replace($url->url,
'<a href="' . $url->expanded_url . '">' . $url->display_url . '</a>',
$cleanedTweet);
$foundUrls = true;
}
}
if ($foundUrls === false) {
// fallback to regex'es
$reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
if(preg_match($reg_ex, $tweet->full_text, $url)) {
$cleanedTweet = preg_replace($reg_ex,
"<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
$cleanedTweet);
}
}
// generate the title
$item['title'] = strip_tags($cleanedTweet);
$this->processContentLinks($tweet);
$this->processEmojis($tweet);
// get tweet text
$cleanedTweet = str_replace(
'href="/',
'href="' . self::URI,
$tweet->find('p.js-tweet-text', 0)->innertext
);
// fix anchors missing spaces in-between
$cleanedTweet = $this->fixAnchorSpacing($cleanedTweet);
// Add picture to content
// Add avatar
$picture_html = '';
if(!$hidePictures) {
$picture_html = <<<EOD
@ -274,31 +341,79 @@ EOD
EOD;
}
// Add embeded image to content
$image_html = '';
$images = $this->getImageURI($tweet);
if(!$this->getInput('noimg') && !is_null($images)) {
// Get images
$media_html = '';
if(isset($tweet->extended_entities->media) && !$this->getInput('noimg')) {
foreach($tweet->extended_entities->media as $media) {
switch($media->type) {
case 'photo':
$image = $media->media_url_https . '?name=orig';
$display_image = $media->media_url_https;
// add enclosures
$item['enclosures'][] = $image;
foreach ($images as $image) {
// Set image scaling
$image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
$image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
// add enclosures
$item['enclosures'][] = $image_orig;
$image_html .= <<<EOD
<a href="{$image_orig}">
$media_html .= <<<EOD
<a href="{$image}">
<img
style="align:top; max-width:558px; border:1px solid black;"
src="{$image_thumb}" />
referrerpolicy="no-referrer"
src="{$display_image}" />
</a>
EOD;
break;
case 'video':
case 'animated_gif':
if(isset($media->video_info)) {
$link = $media->expanded_url;
$poster = $media->media_url_https;
$video = null;
$maxBitrate = -1;
foreach($media->video_info->variants as $variant) {
$bitRate = isset($variant->bitrate) ? $variant->bitrate : -100;
if ($bitRate > $maxBitrate) {
$maxBitrate = $bitRate;
$video = $variant->url;
}
}
if(!is_null($video)) {
// add enclosures
$item['enclosures'][] = $video;
$item['enclosures'][] = $poster;
$media_html .= <<<EOD
<a href="{$link}">Video</a>
<video
style="align:top; max-width:558px; border:1px solid black;"
referrerpolicy="no-referrer"
src="{$video}" poster="{$poster}" />
EOD;
}
}
break;
default:
Debug::log('Missing support for media type: ' . $media->type);
}
}
}
// add content
switch($this->queriedContext) {
case 'By list':
case 'By list ID':
// Check if filter applies to list (using raw content)
if($this->getInput('filter')) {
if(stripos($cleanedTweet, $this->getInput('filter')) === false) {
continue 2; // switch + for-loop!
}
}
break;
case 'By username':
if ($this->getInput('noretweet') && strtolower($item['username']) != strtolower($this->getInput('u'))) {
continue 2; // switch + for-loop!
}
break;
default:
}
$item['content'] = <<<EOD
<div style="display: inline-block; vertical-align: top;">
{$picture_html}
@ -307,155 +422,149 @@ EOD;
<blockquote>{$cleanedTweet}</blockquote>
</div>
<div style="display: block; vertical-align: top;">
<blockquote>{$image_html}</blockquote>
<blockquote>{$media_html}</blockquote>
</div>
EOD;
// add quoted tweet
$quotedTweet = $tweet->find('div.QuoteTweet', 0);
if($quotedTweet) {
// get tweet text
$cleanedQuotedTweet = str_replace(
'href="/',
'href="' . self::URI,
$quotedTweet->find('div.tweet-text', 0)->innertext
);
$this->processContentLinks($quotedTweet);
$this->processEmojis($quotedTweet);
// Add embeded image to content
$quotedImage_html = '';
$quotedImages = $this->getQuotedImageURI($tweet);
if(!$this->getInput('noimg') && !is_null($quotedImages)) {
foreach ($quotedImages as $image) {
// Set image scaling
$image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
$image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
// add enclosures
$item['enclosures'][] = $image_orig;
$quotedImage_html .= <<<EOD
<a href="{$image_orig}">
<img
style="align:top; max-width:558px; border:1px solid black;"
src="{$image_thumb}" />
</a>
EOD;
}
}
$item['content'] = <<<EOD
{$item['content']}
<hr>
<div style="display: inline-block; vertical-align: top;">
<blockquote>{$cleanedQuotedTweet}</blockquote>
</div>
<div style="display: block; vertical-align: top;">
<blockquote>{$quotedImage_html}</blockquote>
</div>
EOD;
}
$item['content'] = htmlspecialchars_decode($item['content'], ENT_QUOTES);
// put out
$this->items[] = $item;
}
usort($this->items, array('TwitterBridge', 'compareTweetId'));
}
private function processEmojis($tweet){
// process emojis (reduce size)
foreach($tweet->find('img.Emoji') as $img) {
$img->style .= ' height: 1em;';
private static function compareTweetId($tweet1, $tweet2) {
return (intval($tweet1['id']) < intval($tweet2['id']) ? 1 : -1);
}
//The aim of this function is to get an API key and a guest token
//This function takes 2 requests, and therefore is cached
private function getApiKey() {
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$r_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$r_cache->setScope(get_called_class());
$r_cache->setKey(array('refresh'));
$data = $r_cache->loadData();
$refresh = null;
if($data === null) {
$refresh = time();
$r_cache->saveData($refresh);
} else {
$refresh = $data;
}
}
private function processContentLinks($tweet){
// processing content links
foreach($tweet->find('a') as $link) {
if($link->hasAttribute('data-expanded-url')) {
$link->href = $link->getAttribute('data-expanded-url');
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope(get_called_class());
$cache->setKey(array('api_key'));
$data = $cache->loadData();
$apiKey = null;
if($data === null || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) {
$twitterPage = getContents('https://twitter.com');
$jsLink = false;
$jsMainRegexArray = array(
'/(https:\/\/abs\.twimg\.com\/responsive-web\/web\/main\.[^\.]+\.js)/m',
'/(https:\/\/abs\.twimg\.com\/responsive-web\/web_legacy\/main\.[^\.]+\.js)/m',
'/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web\/main\.[^\.]+\.js)/m',
'/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web-legacy\/main\.[^\.]+\.js)/m',
);
foreach ($jsMainRegexArray as $jsMainRegex) {
if (preg_match_all($jsMainRegex, $twitterPage, $jsMainMatches, PREG_SET_ORDER, 0)) {
$jsLink = $jsMainMatches[0][0];
break;
}
}
$link->removeAttribute('data-expanded-url');
$link->removeAttribute('data-query-source');
$link->removeAttribute('rel');
$link->removeAttribute('class');
$link->removeAttribute('target');
$link->removeAttribute('title');
if (!$jsLink) {
returnServerError('Could not locate main.js link');
}
$jsContent = getContents($jsLink);
$apiKeyRegex = '/([a-zA-Z0-9]{59}%[a-zA-Z0-9]{44})/m';
preg_match_all($apiKeyRegex, $jsContent, $apiKeyMatches, PREG_SET_ORDER, 0);
$apiKey = $apiKeyMatches[0][0];
$cache->saveData($apiKey);
} else {
$apiKey = $data;
}
$cacheFac2 = new CacheFactory();
$cacheFac2->setWorkingDir(PATH_LIB_CACHES);
$gt_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$gt_cache->setScope(get_called_class());
$gt_cache->setKey(array('guest_token'));
$guestTokenUses = $gt_cache->loadData();
$guestToken = null;
if($guestTokenUses === null || !is_array($guestTokenUses) || count($guestTokenUses) != 2
|| $guestTokenUses[0] <= 0 || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) {
$guestToken = $this->getGuestToken();
$gt_cache->saveData(array(self::GUEST_TOKEN_USES, $guestToken));
$r_cache->saveData(time());
} else {
$guestTokenUses[0] -= 1;
$gt_cache->saveData($guestTokenUses);
$guestToken = $guestTokenUses[1];
}
return array($apiKey, $guestToken);
}
private function fixAnchorSpacing($content){
// fix anchors missing spaces in-between
return str_replace(
'<a',
' <a',
$content
);
// Get a guest token. This is different to an API key,
// and it seems to change more regularly than the API key.
private function getGuestToken() {
$pageContent = getContents('https://twitter.com', array(), array(), true);
$guestTokenRegex = '/gt=([0-9]*)/m';
preg_match_all($guestTokenRegex, $pageContent['header'], $guestTokenMatches, PREG_SET_ORDER, 0);
if (!$guestTokenMatches)
preg_match_all($guestTokenRegex, $pageContent['content'], $guestTokenMatches, PREG_SET_ORDER, 0);
if (!$guestTokenMatches) returnServerError('Could not parse guest token');
$guestToken = $guestTokenMatches[0][1];
return $guestToken;
}
private function getImageURI($tweet){
// Find media in tweet
$images = array();
private function getApiContents($uri) {
$apiKeys = $this->getApiKey();
$headers = array('authorization: Bearer ' . $apiKeys[0],
'x-guest-token: ' . $apiKeys[1],
);
return getContents($uri, $headers);
}
$container = $tweet->find('div.AdaptiveMedia-container', 0);
private function getRestId($username) {
$searchparams = urlencode('{"screen_name":"' . strtolower($username) . '", "withHighlightedLabel":true}');
$searchURL = self::API_URI . '/graphql/-xfUfZsnR_zqjFd-IfrN5A/UserByScreenName?variables=' . $searchparams;
$searchResult = $this->getApiContents($searchURL);
$searchResult = json_decode($searchResult);
return $searchResult->data->user->rest_id;
}
if($container && $container->find('img', 0)) {
foreach ($container->find('img') as $img) {
$images[] = $img->src;
private function getListId($username, $listName) {
$searchparams = urlencode('{"screenName":"'
. strtolower($username)
. '", "listSlug": "'
. $listName
. '", "withHighlightedLabel":false}');
$searchURL = self::API_URI . '/graphql/ErWsz9cObLel1BF-HjuBlA/ListBySlug?variables=' . $searchparams;
$searchResult = $this->getApiContents($searchURL);
$searchResult = json_decode($searchResult);
return $searchResult->data->user_by_screen_name->list->id_str;
}
private function getUserInformation($userId, $apiData) {
foreach($apiData->users as $user) {
if($user->id_str == $userId) {
return $user;
}
}
if (!empty($images)) {
return $images;
}
return null;
}
private function getQuotedImageURI($tweet){
// Find media in tweet
$images = array();
$container = $tweet->find('div.QuoteMedia-container', 0);
if($container && $container->find('img', 0)) {
foreach ($container->find('img') as $img) {
$images[] = $img->src;
}
}
if (!empty($images)) {
return $images;
}
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

@ -0,0 +1,71 @@
<?php
class UnraidCommunityApplicationsBridge extends BridgeAbstract {
const NAME = 'Unraid Community Applications';
const URI = 'https://forums.unraid.net/topic/38582-plug-in-community-applications/';
const DESCRIPTION = 'Fetches the latest fifteen new apps/plugins from Unraid Community Applications';
const MAINTAINER = 'Paroleen';
const CACHE_TIMEOUT = 3600;
const APPSURI = 'https://raw.githubusercontent.com/Squidly271/AppFeed/master/applicationFeed.json';
private $apps = array();
private function fetchApps() {
Debug::log('Fetching all applications/plugins');
$this->apps = getContents(self::APPSURI)
or returnServerError('Could not fetch JSON for apps.');
$this->apps = json_decode($this->apps, true)['applist'];
}
private function sortApps() {
Debug::log('Sorting applications/plugins');
usort($this->apps, function($app1, $app2) {
return $app1['FirstSeen'] < $app2['FirstSeen'] ? 1 : -1;
});
}
public function collectData() {
$this->fetchApps();
$this->sortApps();
Debug::log('Building RSS feed');
foreach($this->apps as $app) {
if(!array_key_exists('Language', $app)) {
$item = array();
$item['title'] = $app['Name'];
$item['timestamp'] = $app['FirstSeen'];
$item['author'] = explode('\'', $app['Repo'])[0];
$item['categories'] = explode(' ', $app['Category']);
$item['content'] = '';
if(array_key_exists('Icon', $app))
$item['content'] .= '<img style="width: 64px" src="'
. $app['Icon']
. '">';
if(array_key_exists('Overview', $app))
$item['content'] .= '<p>'
. $app['Overview']
. '</p>';
if(array_key_exists('Project', $app))
$item['uri'] = $app['Project'];
if(array_key_exists('Registry', $app))
$item['content'] .= '<br><a href="'
. $app['Registry']
. '">Docker Hub</a>';
if(array_key_exists('Support', $app))
$item['content'] .= '<br><a href="'
. $app['Support']
. '">Support</a>';
$this->items[] = $item;
if(count($this->items) >= 15)
break;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more