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 ENV APACHE_DOCUMENT_ROOT=/app
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
&& apt-get --yes update && apt-get --yes install libxml2-dev \ && apt-get --yes update \
&& docker-php-ext-install -j$(nproc) simplexml \ && 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/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!/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 \ && 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) - [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
- [`curl`](https://secure.php.net/manual/en/book.curl.php) - [`curl`](https://secure.php.net/manual/en/book.curl.php)
- [`json`](https://secure.php.net/manual/en/book.json.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) - [`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) 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) * [alexAubin](https://github.com/alexAubin)
* [AmauryCarrade](https://github.com/AmauryCarrade) * [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel) * [AntoineTurmel](https://github.com/AntoineTurmel)
* [arnd-s](https://github.com/arnd-s)
* [ArthurHoaro](https://github.com/ArthurHoaro) * [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven) * [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42) * [Astyan-42](https://github.com/Astyan-42)
* [AxorPL](https://github.com/AxorPL)
* [ayacoo](https://github.com/ayacoo)
* [az5he6ch](https://github.com/az5he6ch) * [az5he6ch](https://github.com/az5he6ch)
* [azdkj532](https://github.com/azdkj532) * [azdkj532](https://github.com/azdkj532)
* [b1nj](https://github.com/b1nj) * [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse) * [benasse](https://github.com/benasse)
* [Binnette](https://github.com/Binnette)
* [captn3m0](https://github.com/captn3m0) * [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel) * [chemel](https://github.com/chemel)
* [ckiw](https://github.com/ckiw) * [ckiw](https://github.com/ckiw)
* [cnlpete](https://github.com/cnlpete) * [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting) * [corenting](https://github.com/corenting)
* [couraudt](https://github.com/couraudt) * [couraudt](https://github.com/couraudt)
* [csisoap](https://github.com/csisoap)
* [cyberjacob](https://github.com/cyberjacob) * [cyberjacob](https://github.com/cyberjacob)
* [da2x](https://github.com/da2x) * [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei) * [Daiyousei](https://github.com/Daiyousei)
@ -146,27 +152,36 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [em92](https://github.com/em92) * [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh) * [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM) * [EtienneM](https://github.com/EtienneM)
* [fanch317](https://github.com/fanch317)
* [floviolleau](https://github.com/floviolleau) * [floviolleau](https://github.com/floviolleau)
* [fluffy-critter](https://github.com/fluffy-critter) * [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie) * [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek) * [fulmeek](https://github.com/fulmeek)
* [ggiessen](https://github.com/ggiessen)
* [Ginko-Aloe](https://github.com/Ginko-Aloe) * [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos) * [Glandos](https://github.com/Glandos)
* [gloony](https://github.com/gloony) * [gloony](https://github.com/gloony)
* [GregThib](https://github.com/GregThib) * [GregThib](https://github.com/GregThib)
* [griffaurel](https://github.com/griffaurel) * [griffaurel](https://github.com/griffaurel)
* [Grummfy](https://github.com/Grummfy) * [Grummfy](https://github.com/Grummfy)
* [gsantner](https://github.com/gsantner)
* [hunhejj](https://github.com/hunhejj) * [hunhejj](https://github.com/hunhejj)
* [husim0](https://github.com/husim0) * [husim0](https://github.com/husim0)
* [IceWreck](https://github.com/IceWreck) * [IceWreck](https://github.com/IceWreck)
* [j0k3r](https://github.com/j0k3r) * [j0k3r](https://github.com/j0k3r)
* [JackNUMBER](https://github.com/JackNUMBER) * [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) * [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand) * [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z) * [Jocker666z](https://github.com/Jocker666z)
* [johnnygroovy](https://github.com/johnnygroovy) * [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) * [klimplant](https://github.com/klimplant)
* [kolarcz](https://github.com/kolarcz)
* [kranack](https://github.com/kranack) * [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc) * [kraoc](https://github.com/kraoc)
* [l1n](https://github.com/l1n) * [l1n](https://github.com/l1n)
@ -175,6 +190,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [lalannev](https://github.com/lalannev) * [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry) * [ldidry](https://github.com/ldidry)
* [Leomaradan](https://github.com/Leomaradan) * [Leomaradan](https://github.com/Leomaradan)
* [liamka](https://github.com/liamka)
* [Limero](https://github.com/Limero) * [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal) * [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos) * [lorenzos](https://github.com/lorenzos)
@ -185,18 +201,25 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [mdemoss](https://github.com/mdemoss) * [mdemoss](https://github.com/mdemoss)
* [melangue](https://github.com/melangue) * [melangue](https://github.com/melangue)
* [metaMMA](https://github.com/metaMMA) * [metaMMA](https://github.com/metaMMA)
* [mibe](https://github.com/mibe)
* [mightymt](https://github.com/mightymt)
* [mitsukarenai](https://github.com/mitsukarenai) * [mitsukarenai](https://github.com/mitsukarenai)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours) * [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble) * [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro) * [mro](https://github.com/mro)
* [mschwld](https://github.com/mschwld)
* [mxmehl](https://github.com/mxmehl) * [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n) * [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag) * [niawag](https://github.com/niawag)
* [Niehztog](https://github.com/Niehztog)
* [Nono-m0le](https://github.com/Nono-m0le) * [Nono-m0le](https://github.com/Nono-m0le)
* [ObsidianWitch](https://github.com/ObsidianWitch) * [ObsidianWitch](https://github.com/ObsidianWitch)
* [OliverParoczai](https://github.com/OliverParoczai) * [OliverParoczai](https://github.com/OliverParoczai)
* [oratosquilla-oratoria](https://github.com/oratosquilla-oratoria) * [Ololbu](https://github.com/Ololbu)
* [ORelio](https://github.com/ORelio) * [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) * [PaulVayssiere](https://github.com/PaulVayssiere)
* [pellaeon](https://github.com/pellaeon) * [pellaeon](https://github.com/pellaeon)
* [Piranhaplant](https://github.com/Piranhaplant) * [Piranhaplant](https://github.com/Piranhaplant)
@ -206,24 +229,31 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [Pofilo](https://github.com/Pofilo) * [Pofilo](https://github.com/Pofilo)
* [prysme01](https://github.com/prysme01) * [prysme01](https://github.com/prysme01)
* [quentinus95](https://github.com/quentinus95) * [quentinus95](https://github.com/quentinus95)
* [RawkBob](https://github.com/RawkBob)
* [regisenguehard](https://github.com/regisenguehard) * [regisenguehard](https://github.com/regisenguehard)
* [Riduidel](https://github.com/Riduidel) * [Riduidel](https://github.com/Riduidel)
* [rogerdc](https://github.com/rogerdc) * [rogerdc](https://github.com/rogerdc)
* [Roliga](https://github.com/Roliga) * [Roliga](https://github.com/Roliga)
* [ronansalmon](https://github.com/ronansalmon)
* [rremizov](https://github.com/rremizov)
* [sebsauvage](https://github.com/sebsauvage) * [sebsauvage](https://github.com/sebsauvage)
* [shutosg](https://github.com/shutosg) * [shutosg](https://github.com/shutosg)
* [Simounet](https://github.com/Simounet)
* [somini](https://github.com/somini) * [somini](https://github.com/somini)
* [squeek502](https://github.com/squeek502) * [squeek502](https://github.com/squeek502)
* [stjohnjohnson](https://github.com/stjohnjohnson) * [stjohnjohnson](https://github.com/stjohnjohnson)
* [Strubbl](https://github.com/Strubbl) * [Strubbl](https://github.com/Strubbl)
* [sublimz](https://github.com/sublimz) * [sublimz](https://github.com/sublimz)
* [sunchaserinfo](https://github.com/sunchaserinfo) * [sunchaserinfo](https://github.com/sunchaserinfo)
* [SuperSandro2000](https://github.com/SuperSandro2000)
* [sysadminstory](https://github.com/sysadminstory) * [sysadminstory](https://github.com/sysadminstory)
* [tameroski](https://github.com/tameroski) * [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene) * [teromene](https://github.com/teromene)
* [tgkenney](https://github.com/tgkenney)
* [thefranke](https://github.com/thefranke) * [thefranke](https://github.com/thefranke)
* [ThePadawan](https://github.com/ThePadawan) * [ThePadawan](https://github.com/ThePadawan)
* [TheRadialActive](https://github.com/TheRadialActive) * [TheRadialActive](https://github.com/TheRadialActive)
* [theScrabi](https://github.com/theScrabi)
* [TitiTestScalingo](https://github.com/TitiTestScalingo) * [TitiTestScalingo](https://github.com/TitiTestScalingo)
* [triatic](https://github.com/triatic) * [triatic](https://github.com/triatic)
* [VerifiedJoseph](https://github.com/VerifiedJoseph) * [VerifiedJoseph](https://github.com/VerifiedJoseph)
@ -231,6 +261,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [wtuuju](https://github.com/wtuuju) * [wtuuju](https://github.com/wtuuju)
* [xurxof](https://github.com/xurxof) * [xurxof](https://github.com/xurxof)
* [yardenac](https://github.com/yardenac) * [yardenac](https://github.com/yardenac)
* [ymeister](https://github.com/ymeister)
* [ZeNairolf](https://github.com/ZeNairolf) * [ZeNairolf](https://github.com/ZeNairolf)
Licenses 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: 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 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) * [`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 { try {
$bridge->setDatas($bridge_params); $bridge->setDatas($bridge_params);
$bridge->loadConfiguration();
$bridge->collectData(); $bridge->collectData();
$items = $bridge->getItems(); $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 DESCRIPTION = 'Bridge for allocine.fr';
const PARAMETERS = array( array( const PARAMETERS = array( array(
'category' => array( 'category' => array(
'name' => 'category', 'name' => 'Emission',
'type' => 'list', 'type' => 'list',
'exampleValue' => 'Faux Raccord', 'title' => 'Sélectionner l\'emission',
'title' => 'Select your category',
'values' => array( 'values' => array(
'Faux Raccord' => 'faux-raccord', 'Faux Raccord' => 'faux-raccord',
'Top 5' => 'top-5', 'Fanzone' => 'fanzone',
'Tueurs en Séries' => 'tueurs-en-serie' '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(){ public function getURI(){
if(!is_null($this->getInput('category'))) { if(!is_null($this->getInput('category'))) {
switch($this->getInput('category')) { $categories = array(
case 'faux-raccord': 'faux-raccord' => 'video/programme-12284/saison-37054/',
$uri = static::URI . 'video/programme-12284/saison-32180/'; 'fanzone' => 'video/programme-12298/saison-37059/',
break; 'game-in-cine' => 'video/programme-12288/saison-22971/',
case 'top-5': 'pour-la-faire-courte' => 'video/programme-20960/saison-29678/',
$uri = static::URI . 'video/programme-12299/saison-29561/'; 'home-cinema' => 'video/programme-12287/saison-34703/',
break; 'pils-par-ici-les-sorties' => 'video/programme-25789/saison-37253/',
case 'tueurs-en-serie': 'allocine-lemission-sur-lestream' => 'video/programme-25123/saison-36067/',
$uri = static::URI . 'video/programme-12286/saison-22938/'; 'give-me-five' => 'video/programme-21919/saison-34518/',
break; '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(); return parent::getURI();
@ -63,23 +85,23 @@ class AllocineFRBridge extends BridgeAbstract {
self::PARAMETERS[$this->queriedContext]['category']['values'] 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(); $item = array();
$title = $element->find('div.titlebar h3.title a', 0); $title = $element->find('a[class*=meta-title-link]', 0);
$content = trim($element->innertext); $content = trim($element->outertext);
$figCaption = strpos($content, $category);
// 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);
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['content'] = $content;
$item['title'] = trim($title->innertext); $item['title'] = trim($title->innertext);
$item['uri'] = static::URI . $title->href; $item['uri'] = static::URI . substr($title->href, 1);
$this->items[] = $item; $this->items[] = $item;
} }
} }
}
} }

View file

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

View file

@ -3,7 +3,9 @@ class AnidexBridge extends BridgeAbstract {
const MAINTAINER = 'ORelio'; const MAINTAINER = 'ORelio';
const NAME = 'Anidex'; 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 DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
const PARAMETERS = array( const PARAMETERS = array(
array( array(
@ -108,7 +110,7 @@ class AnidexBridge extends BridgeAbstract {
public function collectData() { public function collectData() {
// Build Search URL from user-provided parameters // 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) { foreach (array('id', 'lang_id', 'group_id') as $param_name) {
$param = $this->getInput($param_name); $param = $this->getInput($param_name);
if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) { 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; $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 // 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); or returnServerError('Could not request Anidex: ' . $search_url);
$links = $html->find('a'); $links = $html->find('a');
$results = array(); $results = array();
@ -156,10 +166,11 @@ class AnidexBridge extends BridgeAbstract {
if ($torrent_id != 0 && ctype_digit($torrent_id)) { if ($torrent_id != 0 && ctype_digit($torrent_id)) {
//Retrieve data for this 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 //Retrieve full description from torrent page (cached for 24 hours: 86400 seconds)
if ($item_html = getSimpleHTMLDOMCached($item_uri)) { if ($item_html = getSimpleHTMLDOMCached($item_fetch_uri, 86400, $headers, $opt)) {
//Retrieve data from page contents //Retrieve data from page contents
$item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext); $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 //Build and add final item
$item = array(); $item = array();
$item['uri'] = $item_uri; $item['uri'] = $item_browse_uri;
$item['title'] = $item_title; $item['title'] = $item_title;
$item['author'] = $item_author; $item['author'] = $item_author;
$item['timestamp'] = $item_date; $item['timestamp'] = $item_date;

View file

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

View file

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

View file

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

View file

@ -2,8 +2,8 @@
class AtmoNouvelleAquitaineBridge extends BridgeAbstract { class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
const NAME = 'Atmo Nouvelle Aquitaine'; const NAME = 'Atmo Nouvelle Aquitaine';
const URI = 'https://www.atmo-nouvelleaquitaine.org/monair/commune/'; const URI = 'https://www.atmo-nouvelleaquitaine.org';
const DESCRIPTION = 'Fetches the latest air polution of Bordeaux from Atmo Nouvelle Aquitaine'; const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo';
const MAINTAINER = 'floviolleau'; const MAINTAINER = 'floviolleau';
const PARAMETERS = array(array( const PARAMETERS = array(array(
'cities' => array( 'cities' => array(
@ -27,7 +27,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
} }
public function collectData() { public function collectData() {
$uri = self::URI . $this->getInput('cities'); $uri = self::URI . '/monair/commune/' . $this->getInput('cities');
$html = getSimpleHTMLDOM($uri) $html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request ' . $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,59 +77,55 @@ class AutoJMBridge extends BridgeAbstract {
$model_url = self::URI . $this->getInput('url'); $model_url = self::URI . $this->getInput('url');
// Get the session cookies and the form token // Build the GET data
$this->getInitialParameters($model_url); $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 // Set the header 'X-Requested-With' like the website does it
$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
$header = array( $header = array(
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With: XMLHttpRequest'
);
// 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
); );
// Get the JSON content of the form // 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.'); or returnServerError('Could not request AutoJM.');
// Extract the HTML content from the JSON result // Extract the HTML content from the JSON result
$data = json_decode($json); $data = json_decode($json);
$html = str_get_html($data->content); $html = str_get_html($data->results);
// Go through every finisha of the model // Go through every car of the model
$list = $html->find('h3'); $list = $html->find('div[class=car-card]');
foreach ($list as $finish) { foreach ($list as $car) {
$finish_name = $finish->plaintext;
$motorizations = $finish->next_sibling()->find('li'); // Get the Finish name if this car is the first of a new finish
foreach ($motorizations as $element) { $prev_tag = $car->prev_sibling();
$image = $element->find('div[class=block-product-image]', 0)->{'data-ga-banner'}; if($prev_tag->tag == 'div' && $prev_tag->class == 'results-title') {
$serie = $element->find('span[class=model]', 0)->plaintext; $finish_name = $prev_tag->plaintext;
$url = self::URI . substr($element->find('a', 0)->href, 1); }
if ($element->find('span[class*=block-product-nbModel]', 0) != null) {
// 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'; $availability = 'En Stock';
} else { } else {
$availability = 'Sur commande'; $availability = 'Sur commande';
} }
$discount_html = $element->find('span[class*=tag--promo]', 0); $discount_html = $car->find('span[class=promo]', 0);
// Check if there is any discount dsiplayed
if ($discount_html != null) { if ($discount_html != null) {
$discount = $discount_html->plaintext; $discount = $discount_html->plaintext;
} else { } else {
$discount = 'inconnue'; $discount = 'inconnue';
} }
$price = $element->find('span[class=price red h1]', 0)->plaintext; $price = $car->find('span[class=price]', 0)->plaintext;
// Construct the new item
$item = array(); $item = array();
$item['title'] = $finish_name . ' ' . $serie; $item['title'] = $finish_name . ' ' . $serie;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' $item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
@ -146,41 +142,4 @@ class AutoJMBridge extends BridgeAbstract {
$this->items[] = $item; $this->items[] = $item;
} }
} }
}
/**
* 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;
}
}
$this->cookies = trim(substr($cookies, 1));
// 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;
}
} }

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['title'] = $element->find('title', 0)->innertext;
$item['uri'] = $element->find('guid', 0)->plaintext; $item['uri'] = $element->find('guid', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('dc:date', 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/'! $html = getSimpleHTMLDOM($item['uri']);
$item['content'] = preg_replace( $html = defaultLinkTo($html, self::URI);
'/src=["\']{1}([^"\']+)/ims',
'src=\'' . self::URI . '$1\'', $item['content'] = $html->find('div.texte', 0)->innertext;
getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext
);
$this->items[] = $item; $this->items[] = $item;
$limit++; $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', 'Entertainment' => 'entertainment',
'Sports' => 'sport', 'Sports' => 'sport',
'Nature' => 'nature', 'Nature' => 'nature',
'Health' => 'health',
), ),
'defaultValue' => 'news', 'defaultValue' => 'news',
), ),
@ -26,6 +27,7 @@ class BrutBridge extends BridgeAbstract {
'United States' => 'us', 'United States' => 'us',
'United Kingdom' => 'uk', 'United Kingdom' => 'uk',
'France' => 'fr', 'France' => 'fr',
'Spain' => 'es',
'India' => 'in', 'India' => 'in',
'Mexico' => 'mx', '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){ protected function parseItem($newsItem){
$item = parent::parseItem($newsItem); $item = parent::parseItem($newsItem);
if (empty($item['content']))
return null; //ignore dummy articles
$article = getSimpleHTMLDOMCached($item['uri']) $article = getSimpleHTMLDOMCached($item['uri'])
or returnServerError('Could not request Dark Reading: ' . $item['uri']); or returnServerError('Could not request Dark Reading: ' . $item['uri']);
$item['content'] = $this->extractArticleContent($article); $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() { public function collectData() {
$html = getSimpleHTMLDOMCached($this->getURI()) $html = getSimpleHTMLDOMCached($this->getURI())
or returnServerError('Could not request ' . $this->getURI()); or returnServerError('Could not request ' . $this->getURI());
$html = defaultLinkTo($html, static::URI); $html = defaultLinkTo($html, static::URI);
$articles = $html->find('div[class="single-article"]') $articles = $html->find('div.crayons-story')
or returnServerError('Could not find articles!'); or returnServerError('Could not find articles!');
foreach($articles as $article) { foreach($articles as $article) {
if($article->find('[class*="cta"]', 0)) { // Skip ads
continue;
}
$item = array(); $item = array();
$item['uri'] = $article->find('a[id*=article-link]', 0)->href; $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'] = $article->find('time', 0)->datetime;
$item['timestamp'] = strtotime(explode('・', $article->find('h4 a', 0)->plaintext, 2)[1]); $item['author'] = $article->find('a.crayons-story__secondary.fw-medium', 0)->plaintext;
$item['author'] = explode('・', $article->find('h4 a', 0)->plaintext, 2)[0];
// Profile image // Profile image
$item['enclosures'] = array($article->find('img', 0)->src); $item['enclosures'] = array($article->find('img', 0)->src);
@ -75,7 +68,6 @@ apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png'
if($this->getInput('full')) { if($this->getInput('full')) {
$fullArticle = $this->getFullArticle($item['uri']); $fullArticle = $this->getFullArticle($item['uri']);
$item['content'] = <<<EOD $item['content'] = <<<EOD
<img src="{$item['enclosures'][0]}" alt="{$item['author']}">
<p>{$fullArticle}</p> <p>{$fullArticle}</p>
EOD; EOD;
} else { } else {
@ -85,11 +77,21 @@ EOD;
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; $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) { private function getFullArticle($url) {
@ -98,6 +100,10 @@ EOD;
$html = defaultLinkTo($html, static::URI); $html = defaultLinkTo($html, static::URI);
if ($html->find('div.crayons-article__cover', 0)) {
return $html->find('div.crayons-article__cover', 0) . $html->find('[id="article-body"]', 0);
}
return $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); $table = $html->find('table.table-striped', 0);
$maxCount = 10; $maxCount = 10;
foreach ($table->find('tr') as $downEvent) { foreach ($table->find('tr') as $event) {
$downLink = $downEvent->find('td', 1)->find('a', 1); $td = $event->find('td', 0);
$item = $this->collectArticleData($downLink->getAttribute('href'));
if (is_null($td)) {
continue;
}
$link = $event->find('td', 0)->find('a', 0);
$item = $this->collectArticleData($link->getAttribute('href'));
$this->items[] = $item; $this->items[] = $item;
if($maxCount == 0) break; if($maxCount == 0) break;
$maxCount -= 1; $maxCount -= 1;

View file

@ -13,7 +13,7 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
} }
public function collectData(){ public function collectData(){
$html = getSimpleHTMLDOM(self::URI . '/shots') $html = getSimpleHTMLDOM(self::URI)
or returnServerError('Error while downloading the website content'); or returnServerError('Error while downloading the website content');
$json = $this->loadEmbeddedJsonData($html); $json = $this->loadEmbeddedJsonData($html);
@ -24,19 +24,19 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
$additional_data = $this->findJsonForShot($shot, $json); $additional_data = $this->findJsonForShot($shot, $json);
if ($additional_data === null) { if ($additional_data === null) {
$item['uri'] = self::URI . $shot->find('a', 0)->href; $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 { } else {
$item['timestamp'] = strtotime($additional_data['published_at']); $item['timestamp'] = strtotime($additional_data['published_at']);
$item['uri'] = self::URI . $additional_data['path']; $item['uri'] = self::URI . $additional_data['path'];
$item['title'] = $additional_data['title']; $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); $description = $shot->find('.comment', 0);
$item['content'] = $description === null ? '' : $description->plaintext; $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['content'] .= $this->getImageTag($preview_path, $item['title']);
$item['enclosures'] = array($this->getFullSizeImagePath($preview_path)); $item['enclosures'] = array($this->getFullSizeImagePath($preview_path));
@ -51,10 +51,13 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
foreach($scripts as $script) { foreach($scripts as $script) {
if(strpos($script->innertext, 'newestShots') !== false) { if(strpos($script->innertext, 'newestShots') !== false) {
// fix single quotes // 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?) // 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 // find beginning of JSON array
$start = strpos($script->innertext, '['); $start = strpos($script->innertext, '[');
@ -83,7 +86,7 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
private function getImageTag($preview_path, $title){ private function getImageTag($preview_path, $title){
return sprintf( 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), $this->getFullSizeImagePath($preview_path),
$preview_path, $preview_path,
$title $title
@ -91,6 +94,11 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
} }
private function getFullSizeImagePath($preview_path){ 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/') $html = getSimpleHTMLDOM(self::URI . '/latest/')
or returnServerError('Could not fetch latest updates form The Economist.'); 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', 0); $a = $element->find('a.headline-link', 0);
$href = $a->href;
if (substr($href, 0, 4) != 'http')
$href = self::URI . $a->href; $href = self::URI . $a->href;
$full = getSimpleHTMLDOMCached($href); $full = getSimpleHTMLDOMCached($href);
$article = $full->find('article', 0); $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
$author = $article->find('span[itemprop="author"]', 0); if ($author)
$time = $article->find('time[itemprop="dateCreated"]', 0); $author = substr($author->innertext, 3, strlen($author));
$content = $article->find('div[itemprop="description"]', 0); else
$author = 'The Economist';
// Remove newsletter subscription box // Remove newsletter subscription box
$newsletter = $content->find('div[class="newsletter-form__message"]', 0); $newsletter = $content->find('div[class="newsletter-form__message"]', 0);
@ -40,19 +51,15 @@ class EconomistBridge extends BridgeAbstract {
if ($nextprev) if ($nextprev)
$nextprev->outertext = ''; $nextprev->outertext = '';
$section = array( $article->find('h3[itemprop="articleSection"]', 0)->plaintext );
$item = array(); $item = array();
$item['title'] = $header->find('span', 0)->innertext . ': ' $item['title'] = $header->innertext;
. $header->find('span', 1)->innertext;
$item['uri'] = $href; $item['uri'] = $href;
$item['timestamp'] = strtotime($time->datetime); $item['timestamp'] = strtotime($time->datetime);
$item['author'] = $author->innertext; $item['author'] = $author;
$item['categories'] = $section; $item['categories'] = $section;
$item['content'] = '<img style="max-width: 100%" src="' $item['content'] = '<img style="max-width: 100%" src="'
. $a->find('img', 0)->src . '">' . $content->innertext; . $headerimg->src . '">' . $content->innertext;
$this->items[] = $item; $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 <?php
class ExtremeDownloadBridge extends BridgeAbstract { class ExtremeDownloadBridge extends BridgeAbstract {
const NAME = 'Extreme Download'; 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 DESCRIPTION = 'Suivi de série sur Extreme Download';
const MAINTAINER = 'sysadminstory'; const MAINTAINER = 'sysadminstory';
const PARAMETERS = array( 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', 'type' => 'checkbox',
'required' => false, 'required' => false,
'defaultValue' => false, 'defaultValue' => false,
'title' => 'Feed includes reviews when checked' 'title' => 'Feed includes reviews when unchecked'
) )
), ),
'Group' => array( 'Group' => array(
@ -175,7 +175,13 @@ class FacebookBridge extends BridgeAbstract {
$header = array(); $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()); or returnServerError('Failed loading facebook page: ' . $this->getURI());
if(!$this->isPublicGroup($html)) { if(!$this->isPublicGroup($html)) {
@ -186,19 +192,18 @@ class FacebookBridge extends BridgeAbstract {
$this->groupName = $this->extractGroupName($html); $this->groupName = $this->extractGroupName($html);
$posts = $html->find('div.userContentWrapper') $posts = $html->find('div.story_body_container')
or returnServerError('Failed finding posts!'); or returnServerError('Failed finding posts!');
foreach($posts as $post) { foreach($posts as $post) {
$item = array(); $item = array();
$item['uri'] = $this->extractGroupURI($post); $item['uri'] = $this->extractGroupPostURI($post);
$item['title'] = $this->extractGroupTitle($post); $item['title'] = $this->extractGroupPostTitle($post);
$item['author'] = $this->extractGroupAuthor($post); $item['author'] = $this->extractGroupPostAuthor($post);
$item['content'] = $this->extractGroupContent($post); $item['content'] = $this->extractGroupPostContent($post);
$item['timestamp'] = $this->extractGroupTimestamp($post); $item['enclosures'] = $this->extractGroupPostEnclosures($post);
$item['enclosures'] = $this->extractGroupEnclosures($post);
$this->items[] = $item; $this->items[] = $item;
@ -215,16 +220,7 @@ class FacebookBridge extends BridgeAbstract {
$urlparts = parse_url($group); $urlparts = parse_url($group);
if($urlparts['host'] !== parse_url(self::URI)['host'] $this->validateHost($urlparts['host']);
&& 'www.' . $urlparts['host'] !== parse_url(self::URI)['host']) {
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
return explode('/', $urlparts['path'])[2]; 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) { private function isPublicGroup($html) {
// Facebook redirects to the groups about page for non-public groups // Facebook touch just presents a login page for non-public groups
$about = $html->find('#pagelet_group_about', 0); $title = $html->find('title', 0);
return $title->plaintext !== 'Log in to Facebook | Facebook';
return !($about);
} }
private function extractGroupName($html) { private function extractGroupName($html) {
$ogtitle = $html->find('meta[property="og:title"]', 0) $ogtitle = $html->find('._de1', 0)
or returnServerError('Unable to find group title!'); 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') $elements = $post->find('a')
or returnServerError('Unable to find URI!'); or returnServerError('Unable to find URI!');
@ -262,7 +281,8 @@ class FacebookBridge extends BridgeAbstract {
// Find the one that is a permalink // Find the one that is a permalink
if(strpos($anchor->href, 'permalink') !== false) { 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!'); 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) $element = $post->find('h3 a', 0)
or returnServerError('Unable to find timestamp!');
return $element->getAttribute('data-utime');
}
private function extractGroupAuthor($post) {
$element = $post->find('img', 0)
or returnServerError('Unable to find author information!'); 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(); $enclosures = array();
$background_img_regex = '/background-image: ?url\\((.+?)\\);/';
foreach($elements as $enclosure) { 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; 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!'); or returnServerError('Unable to find title!');
if(strpos($element->plaintext, 'shared') === false) { 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: ' . ' posted: '
. substr( . substr(
$content, $content,
@ -348,13 +372,7 @@ class FacebookBridge extends BridgeAbstract {
$urlparts = parse_url($user); $urlparts = parse_url($user);
if($urlparts['host'] !== parse_url(self::URI)['host']) { $this->validateHost($urlparts['host']);
returnClientError('The host you provided is invalid! Received "'
. $urlparts['host']
. '", expected "'
. parse_url(self::URI)['host']
. '"!');
}
if(!array_key_exists('path', $urlparts) if(!array_key_exists('path', $urlparts)
|| $urlparts['path'] === '/') { || $urlparts['path'] === '/') {
@ -555,7 +573,7 @@ EOD;
} }
// No captcha? We can carry on retrieving page contents :) // 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); $loginForm = $html->find('._585r', 0);
if($loginForm != null) { if($loginForm != null) {
@ -729,6 +747,7 @@ EOD;
} }
} }
} }
#endregion (User) #endregion (User)
} }

View file

@ -35,6 +35,8 @@ class FicbookBridge extends BridgeAbstract {
), ),
); );
protected $titleName;
public function getURI() { public function getURI() {
switch($this->queriedContext) { switch($this->queriedContext) {
case 'Site News': { 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() { public function collectData() {
$header = array('Accept-Language: en-US'); $header = array('Accept-Language: en-US');
@ -65,6 +82,10 @@ class FicbookBridge extends BridgeAbstract {
$html = defaultLinkTo($html, self::URI); $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) { switch($this->queriedContext) {
case 'Site News': return $this->collectSiteNews($html); case 'Site News': return $this->collectSiteNews($html);
case 'Fiction Updates': return $this->collectUpdatesData($html); case 'Fiction Updates': return $this->collectUpdatesData($html);
@ -84,7 +105,7 @@ class FicbookBridge extends BridgeAbstract {
} }
private function collectCommentsData($html) { private function collectCommentsData($html) {
foreach($html->find('article.post') as $article) { foreach($html->find('article.comment-container') as $article) {
$this->items[] = array( $this->items[] = array(
'uri' => $article->find('.comment_link_to_fic > a', 0)->href, 'uri' => $article->find('.comment_link_to_fic > a', 0)->href,
'title' => $article->find('.comment_author', 0)->plaintext, 'title' => $article->find('.comment_author', 0)->plaintext,
@ -97,7 +118,7 @@ class FicbookBridge extends BridgeAbstract {
} }
private function collectUpdatesData($html) { 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( $item = array(
'uri' => $chapter->find('a', 0)->href, 'uri' => $chapter->find('a', 0)->href,
'title' => $chapter->find('a', 0)->plaintext, 'title' => $chapter->find('a', 0)->plaintext,
@ -130,10 +151,10 @@ class FicbookBridge extends BridgeAbstract {
'июня', 'июня',
'июля', 'июля',
'августа', 'августа',
'Сентября', 'сентября',
'октября', 'октября',
'Ноября', 'ноября',
'Декабря', 'декабря',
); );
$en_month = array( $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, 'required' => true,
'title' => 'Insert keyword', 'title' => 'Insert keyword',
'exampleValue' => 'bird' '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( 'By username' => array(
@ -29,30 +50,60 @@ class FlickrBridge extends BridgeAbstract {
'required' => true, 'required' => true,
'title' => 'Insert username (as shown in the address bar)', 'title' => 'Insert username (as shown in the address bar)',
'exampleValue' => 'flickr' '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) { switch($this->queriedContext) {
case 'Explore': case 'Explore':
$filter = 'photo-lite-models'; $filter = 'photo-lite-models';
$html = getSimpleHTMLDOM(self::URI . 'explore') $html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request Flickr.'); or returnServerError('Could not request Flickr.');
break; break;
case 'By keyword': case 'By keyword':
$filter = 'photo-lite-models'; $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.'); or returnServerError('No results for this query.');
break; break;
case 'By username': case 'By username':
$filter = 'photo-models'; //$filter = 'photo-models';
$html = getSimpleHTMLDOM(self::URI . 'photos/' . urlencode($this->getInput('u'))) $filter = 'photo-lite-models';
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Requested username can\'t be found.'); 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; break;
default: default:
@ -64,7 +115,6 @@ class FlickrBridge extends BridgeAbstract {
$photo_models = $this->getPhotoModels($model_json, $filter); $photo_models = $this->getPhotoModels($model_json, $filter);
foreach($photo_models as $model) { foreach($photo_models as $model) {
$item = array(); $item = array();
/* Author name depends on scope. On a keyword search the /* 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. * the author is part of the owner data.
*/ */
if(array_key_exists('username', $model)) { if(array_key_exists('username', $model)) {
$item['author'] = $model['username']; $item['author'] = urldecode($model['username']);
} elseif (array_key_exists('owner', reset($model_json)[0])) { } 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']; $item['uri'] = self::URI . 'photo.gne?id=' . $model['id'];
$description = (array_key_exists('description', $model) ? $model['description'] : ''); $description = (array_key_exists('description', $model) ? $model['description'] : '');
@ -87,7 +137,7 @@ class FlickrBridge extends BridgeAbstract {
. '"><img src="' . '"><img src="'
. $this->extractContentImage($model) . $this->extractContentImage($model)
. '" style="max-width: 640px; max-height: 480px;"/></a><br><p>' . '" style="max-width: 640px; max-height: 480px;"/></a><br><p>'
. $description . urldecode($description)
. '</p>'; . '</p>';
$item['enclosures'] = $this->extractEnclosures($model); $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) { private function extractJsonModel($html) {
// Find SCRIPT containing JSON data // 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){ 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); $headline = trim($article->find('p.description', 0)->plaintext);
if(!empty($headline)) if(!empty($headline))
$headline = '<p><b>' . $headline . '</b></p>'; $headline = '<p><b>' . $headline . '</b></p>';
@ -129,6 +129,7 @@ class FuturaSciencesBridge extends FeedExpander {
$contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"'); $contents = stripWithDelimiters($contents, 'fs:xt:clickname="', '"');
$contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>'); $contents = StripWithDelimiters($contents, '<section class="module-toretain module-propal-nl', '</section>');
$contents = stripWithDelimiters($contents, '<script ', '</script>'); $contents = stripWithDelimiters($contents, '<script ', '</script>');
$contents = stripWithDelimiters($contents, '<script>', '</script>');
return $headline . trim($contents); return $headline . trim($contents);
} }

View file

@ -113,8 +113,8 @@ class GBAtempBridge extends BridgeAbstract {
break; break;
case 'T': case 'T':
foreach($html->find('li.portal-tutorial') as $tutorialItem) { foreach($html->find('li.portal-tutorial') as $tutorialItem) {
$url = self::URI . $tutorialItem->find('a', 0)->href; $url = self::URI . $tutorialItem->find('a', 1)->href;
$title = $tutorialItem->find('a', 0)->plaintext; $title = $tutorialItem->find('a', 1)->plaintext;
$time = $this->findItemDate($tutorialItem); $time = $this->findItemDate($tutorialItem);
$author = $tutorialItem->find('a.username', 0)->plaintext; $author = $tutorialItem->find('a.username', 0)->plaintext;
$content = $this->fetchPostContent($url, self::URI); $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); $uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0)->plaintext; $author = $comment->find('.author', 0);
if ($author) {
$title .= ' / ' . trim($comment->plaintext); $author = $author->plaintext;
} else {
$content = $title; $author = '';
if (null !== $comment->nextSibling()) {
$content = $comment->nextSibling()->innertext;
if ($comment->nextSibling()->nodeName() === 'span') {
$content = $comment->nextSibling()->nextSibling()->innertext;
}
} }
$title .= ' / '
. trim(str_replace(
array('octicon','-'), array(''),
$comment->find('.octicon', 0)->getAttribute('class')
));
$content = $comment->plaintext;
$item = array(); $item = array();
$item['author'] = $author; $item['author'] = $author;
$item['uri'] = $uri; $item['uri'] = $uri;
@ -106,8 +109,7 @@ class GithubIssueBridge extends BridgeAbstract {
} }
private function extractIssueComment($issueNbr, $title, $comment){ private function extractIssueComment($issueNbr, $title, $comment){
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->parent->id);
$author = $comment->find('.author', 0)->plaintext; $author = $comment->find('.author', 0)->plaintext;
@ -135,32 +137,20 @@ class GithubIssueBridge extends BridgeAbstract {
substr($issue->find('.gh-header-number', 0)->plaintext, 1) substr($issue->find('.gh-header-number', 0)->plaintext, 1)
); );
$comments = $issue->find(' $comments = $issue->find(
[id^="issue-"] > .comment, '.comment, .TimelineItem-badge'
[id^="issuecomment-"] > .comment, );
[id^="event-"],
[id^="ref-"]
');
foreach($comments as $comment) { foreach($comments as $comment) {
if ($comment->hasClass('comment')) {
if (!$comment->hasChildNodes()) { $comment = $comment->parent;
continue;
}
if (!$comment->hasClass('discussion-item-header')) {
$item = $this->extractIssueComment($issueNbr, $title, $comment); $item = $this->extractIssueComment($issueNbr, $title, $comment);
$items[] = $item; $items[] = $item;
continue; continue;
} } else {
$comment = $comment->parent;
while ($comment->hasClass('discussion-item-header')) {
$item = $this->extractIssueEvent($issueNbr, $title, $comment); $item = $this->extractIssueEvent($issueNbr, $title, $comment);
$items[] = $item; $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': case 'Project Issues':
foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) { foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
$info = $issue->find('.opened-by', 0); $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 = array();
$item['content'] = ''; $item['content'] = '';

View file

@ -27,16 +27,16 @@ class GithubSearchBridge extends BridgeAbstract {
foreach($html->find('li.repo-list-item') as $element) { foreach($html->find('li.repo-list-item') as $element) {
$item = array(); $item = array();
$uri = $element->find('h3 a', 0)->href; $uri = $element->find('.f4 a', 0)->href;
$uri = substr(self::URI, 0, -1) . $uri; $uri = substr(self::URI, 0, -1) . $uri;
$item['uri'] = $uri; $item['uri'] = $uri;
$title = $element->find('h3', 0)->plaintext; $title = $element->find('.f4', 0)->plaintext;
$item['title'] = $title; $item['title'] = $title;
// Description // Description
if (count($element->find('p.d-inline-block')) != 0) { if (count($element->find('p.mb-1')) != 0) {
$content = $element->find('p.d-inline-block', 0)->innertext; $content = $element->find('p.mb-1', 0)->innertext;
} else{ } else{
$content = 'No description'; $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 MAINTAINER = 'polopollo';
const NAME = 'Gizmodo'; const NAME = 'Gizmodo';
const URI = 'http://gizmodo.com/'; const URI = 'https://gizmodo.com';
const CACHE_TIMEOUT = 1800; // 30min 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); $item = parent::parseItem($item);
$articleHTMLContent = getSimpleHTMLDOMCached($item['uri']); $html = getSimpleHTMLDOMCached($item['uri'])
if(!$articleHTMLContent) { or returnServerError('Could not request: ' . $item['uri']);
$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>';
}
$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; return $item;
} }
public function collectData(){ public function collectData() {
$this->collectExpandableDatas('http://feeds.gawker.com/gizmodo/full'); $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) $page = getSimpleHTMLDOM($link)
or returnServerError('Could not request GoComics: ' . $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); $date = explode('/', $link);
$item['id'] = $imagelink; $item['id'] = $imagelink;

View file

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

View file

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

View file

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

View file

@ -19,6 +19,27 @@ class IGNBridge extends FeedExpander {
// $articlePage gets the entire page's contents // $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link); $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 * 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. * 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 // For Wikis and HowTos
$uselessWikiElements = array(
'.wiki-page-tools',
'.feedback-container',
'.paging-container'
);
foreach($articlePage->find('.wiki-page') as $wikiContents) { foreach($articlePage->find('.wiki-page') as $wikiContents) {
$copy = clone $wikiContents; $article = $article . $wikiContents;
// Remove useless elements present in IGN wiki/howtos
foreach($uselessWikiElements as $uslElement) {
$toRemove = $wikiContents->find($uslElement, 0);
$copy = str_replace($toRemove, '', $copy);
}
$article = $article . $copy;
} }
// Add content to feed // 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 <?php
class InstagramBridge extends BridgeAbstract { class InstagramBridge extends BridgeAbstract {
const MAINTAINER = 'pauder'; // const MAINTAINER = 'pauder';
const NAME = 'Instagram Bridge'; const NAME = 'Instagram Bridge';
const URI = 'https://www.instagram.com/'; const URI = 'https://www.instagram.com/';
const DESCRIPTION = 'Returns the newest images'; const DESCRIPTION = 'Returns the newest images';
@ -47,7 +47,7 @@ class InstagramBridge extends BridgeAbstract {
); );
const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951'; const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951';
const TAG_QUERY_HASH = '174a5243287c5f3a7de741089750ab3b'; const TAG_QUERY_HASH = '9b498c08113f1e09617a1703c22b2f32';
const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a'; const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a';
protected function getInstagramUserId($username) { protected function getInstagramUserId($username) {
@ -65,7 +65,7 @@ class InstagramBridge extends BridgeAbstract {
$data = getContents(self::URI . 'web/search/topsearch/?query=' . $username); $data = getContents(self::URI . 'web/search/topsearch/?query=' . $username);
foreach(json_decode($data)->users as $user) { foreach(json_decode($data)->users as $user) {
if($user->user->username === $username) { if(strtolower($user->user->username) === strtolower($username)) {
$key = $user->user->pk; $key = $user->user->pk;
} }
} }
@ -123,31 +123,33 @@ class InstagramBridge extends BridgeAbstract {
$item['title'] = substr($item['title'], 0, $titleLinePos) . '...'; $item['title'] = substr($item['title'], 0, $titleLinePos) . '...';
} }
switch($media->__typename) {
case 'GraphSidecar':
$data = $this->getInstagramSidecarData($item['uri'], $item['title']);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
break;
case 'GraphImage':
if($directLink) { if($directLink) {
$mediaURI = $media->display_url; $mediaURI = $media->display_url;
} else { } else {
$mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l'; $mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
} }
switch($media->__typename) {
case 'GraphSidecar':
$data = $this->getInstagramSidecarData($item['uri'], $item['title'], $media, $textContent);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
break;
case 'GraphImage':
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">'; $item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
$item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />'; $item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent)); $item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
$item['enclosures'] = array($mediaURI); $item['enclosures'] = array($mediaURI);
break; break;
case 'GraphVideo': case 'GraphVideo':
$data = $this->getInstagramVideoData($item['uri']); $data = $this->getInstagramVideoData($item['uri'], $mediaURI, $media, $textContent);
$item['content'] = $data[0]; $item['content'] = $data[0];
if($directLink) { if($directLink) {
$item['enclosures'] = $data[1]; $item['enclosures'] = $data[1];
} else { } else {
$item['enclosures'] = array(self::URI . 'p/' . $media->shortcode . '/media?size=l'); $item['enclosures'] = array($mediaURI);
} }
$item['thumbnail'] = $mediaURI;
break; break;
default: break; default: break;
} }
@ -158,11 +160,7 @@ class InstagramBridge extends BridgeAbstract {
} }
// returns Sidecar(a post which has multiple media)'s contents and enclosures // returns Sidecar(a post which has multiple media)'s contents and enclosures
protected function getInstagramSidecarData($uri, $postTitle) { protected function getInstagramSidecarData($uri, $postTitle, $mediaInfo, $textContent) {
$mediaInfo = $this->getSinglePostData($uri);
$textContent = $this->getTextContent($mediaInfo);
$enclosures = array(); $enclosures = array();
$content = ''; $content = '';
foreach($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) { foreach($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) {
@ -185,11 +183,11 @@ class InstagramBridge extends BridgeAbstract {
} }
// returns Video post's contents and enclosures // returns Video post's contents and enclosures
protected function getInstagramVideoData($uri) { protected function getInstagramVideoData($uri, $mediaURI, $mediaInfo, $textContent) {
$mediaInfo = $this->getSinglePostData($uri); $content = '<video controls>';
$content .= '<source src="' . $mediaInfo->video_url . '" poster="' . $mediaURI . '" type="video/mp4">';
$textContent = $this->getTextContent($mediaInfo); $content .= '<img src="' . $mediaURI . '" alt="">';
$content = '<video controls><source src="' . $mediaInfo->video_url . '" type="video/mp4"></video><br>'; $content .= '</video><br>';
$content .= '<br>' . nl2br(htmlentities($textContent)); $content .= '<br>' . nl2br(htmlentities($textContent));
return array($content, array($mediaInfo->video_url)); 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; return $element->plaintext;
} }
#endregion #endregion
} }

View file

@ -61,6 +61,8 @@ class KernelBugTrackerBridge extends BridgeAbstract {
if($html === false) if($html === false)
returnServerError('Failed to load page!'); returnServerError('Failed to load page!');
$html = defaultLinkTo($html, self::URI);
// Store header information into private members // Store header information into private members
$this->bugid = $html->find('#bugzilla-body', 0)->find('a', 0)->innertext; $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->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']); $item['content'] = str_replace("\n", '<br>', $item['content']);
// Fix relative URIs // Fix relative URIs
$item['content'] = $this->replaceRelativeURI($item['content']); $item['content'] = $item['content'];
$this->items[] = $item; $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 * Adds styles as attributes to tags with known classes
* *

View file

@ -3,7 +3,7 @@ class KoreusBridge extends FeedExpander {
const MAINTAINER = 'pit-fgfjiudghdf'; const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'Koreus'; 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)'; const DESCRIPTION = 'Returns the newest posts from Koreus (full text)';
protected function parseItem($item){ protected function parseItem($item){
@ -17,6 +17,6 @@ class KoreusBridge extends FeedExpander {
} }
public function collectData(){ 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 //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); $content_node = $article_html->find('div.col-primary, div.col-sm-9', 0);
$item['content'] = utf8_encode($this->cleanArticle($content_node->innertext)); $item['content'] = $this->cleanArticle($content_node->innertext);
$item['author'] = utf8_encode($article_html->find('div.author-infos', 0)->find('b', 0)->plaintext); $item['author'] = $article_html->find('div.author-infos', 0)->find('b', 0)->plaintext;
return $item; return $item;
} }

View file

@ -11,7 +11,7 @@ class LesJoiesDuCodeBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI) $html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request LesJoiesDuCode.'); or returnServerError('Could not request LesJoiesDuCode.');
foreach($html->find('div.blog-post') as $element) { foreach($html->find('article.blog-post') as $element) {
$item = array(); $item = array();
$temp = $element->find('h1 a', 0); $temp = $element->find('h1 a', 0);
$titre = html_entity_decode($temp->innertext); $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(){ public function getURI(){
if($this->getInput('canusername')) if($this->getInput('canusername'))
return 'https://' . $this->getInstance() . '/users/' . $this->getUsername() . '.atom'; return 'https://' . $this->getInstance() . '/@' . $this->getUsername() . '.rss';
return parent::getURI(); 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 MAINTAINER = 'Pitchoule';
const NAME = 'Monde Diplomatique'; const NAME = 'Monde Diplomatique';
const URI = 'http://www.monde-diplomatique.fr/'; const URI = 'https://www.monde-diplomatique.fr';
const CACHE_TIMEOUT = 21600; //6h const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'Returns most recent results from MondeDiplo.'; const DESCRIPTION = 'Returns most recent results from MondeDiplo.';
private function cleanText($text) {
return trim(str_replace(array('&nbsp;', '&nbsp'), ' ', $text));
}
public function collectData(){ public function collectData(){
$html = getSimpleHTMLDOM(self::URI) $html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request MondeDiplo. for : ' . self::URI); or returnServerError('Could not request MondeDiplo. for : ' . self::URI);
foreach($html->find('div.unarticle') as $article) { foreach($html->find('div.unarticle') as $article) {
$element = $article->parent(); $element = $article->parent();
$title = $element->find('h3', 0)->plaintext;
$datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext;
$item = array(); $item = array();
$item['uri'] = self::URI . $element->href; $item['uri'] = self::URI . $element->href;
$item['title'] = $element->find('h3', 0)->plaintext; $item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs);
$item['content'] = $element->find('div.dates_auteurs', 0)->plaintext $item['content'] = $this->cleanText(str_replace(array($title, $datesAuteurs), '', $element->plaintext));
. '<br>'
. strstr($element->find('div', 0)->plaintext, $element->find('div.dates_auteurs', 0)->plaintext, true);
$this->items[] = $item; $this->items[] = $item;
} }

View file

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

View file

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

View file

@ -12,10 +12,8 @@ class NasaApodBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI . 'archivepix.html') $html = getSimpleHTMLDOM(self::URI . 'archivepix.html')
or returnServerError('Error while downloading the website content'); or returnServerError('Error while downloading the website content');
$list = explode('<br>', $html->find('b', 0)->innertext); // Start at 1 to skip the "APOD Full Archive" on top of the page
for($i = 1; $i < 4; $i++) {
for($i = 0; $i < 3; $i++) {
$line = $list[$i];
$item = array(); $item = array();
$uri_page = $html->find('a', $i + 3)->href; $uri_page = $html->find('a', $i + 3)->href;
@ -26,9 +24,14 @@ class NasaApodBridge extends BridgeAbstract {
$picture_html_string = $picture_html->innertext; $picture_html_string = $picture_html->innertext;
//Extract image and explanation //Extract image and explanation
$media = $picture_html->find('p', 1)->innertext; $image_wrapper = $picture_html->find('a', 1);
$media = strstr($media, '<br>'); $image_path = $image_wrapper->href;
$media = preg_replace('/<br>/', '', $media, 1); $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; $explanation = $picture_html->find('p', 2)->innertext;
//Extract date from the picture page //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 <?php
class NextInpactBridge extends FeedExpander { class NextInpactBridge extends FeedExpander {
const MAINTAINER = 'qwertygc'; const MAINTAINER = 'qwertygc and ORelio';
const NAME = 'NextInpact Bridge'; const NAME = 'NextInpact Bridge';
const URI = 'https://www.nextinpact.com/'; const URI = 'https://www.nextinpact.com/';
const URI_HARDWARE = 'https://www.inpact-hardware.com/';
const DESCRIPTION = 'Returns the newest articles.'; const DESCRIPTION = 'Returns the newest articles.';
const PARAMETERS = array( array( const PARAMETERS = array( array(
@ -11,10 +12,30 @@ class NextInpactBridge extends FeedExpander {
'name' => 'Feed', 'name' => 'Feed',
'type' => 'list', 'type' => 'list',
'values' => array( 'values' => array(
'Tous nos articles' => 'news', 'Nos actualités' => array(
'Nos contenus en accès libre' => 'acces-libre', 'Toutes nos publications' => 'news',
'Blog' => 'blog', 'Toutes nos publications sauf #LeBrief' => 'nobrief',
'Bons plans' => 'bonsplans' '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( 'filter_premium' => array(
@ -39,9 +60,27 @@ class NextInpactBridge extends FeedExpander {
public function collectData(){ public function collectData(){
$feed = $this->getInput('feed'); $feed = $this->getInput('feed');
if (empty($feed)) $base_uri = self::URI;
$args = '';
if (empty($feed)) {
// Default to All articles
$feed = 'news'; $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){ protected function parseItem($newsItem){
@ -57,9 +96,11 @@ class NextInpactBridge extends FeedExpander {
if (!is_object($html)) if (!is_object($html))
return 'Failed to request NextInpact: ' . $url; return 'Failed to request NextInpact: ' . $url;
// Filter premium and brief articles?
$brief_selector = 'div.brief-container';
foreach(array( foreach(array(
'filter_premium' => 'h2.title_reserve_article', 'filter_premium' => 'p.red-msg',
'filter_brief' => 'div.brief-inner-content' 'filter_brief' => $brief_selector
) as $param_name => $selector) { ) as $param_name => $selector) {
$param_val = intval($this->getInput($param_name)); $param_val = intval($this->getInput($param_name));
if ($param_val != 0) { 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)); // Subtitle
if(is_object($subtitle) && $subtitle->plaintext !== $item['title']) { $subtitle = $html->find('small.subtitle', 0);
$subtitle = '<p><em>' . $subtitle->plaintext . '</em></p>'; 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 { } else {
$subtitle = ''; $subtitle = '';
} }
$postimg = $html->find( // Image
'div.container_main_image_article, div.image-brief-container, div.image-brief-side-container', 0 $postimg = $html->find('div.article-image, div.image-container', 0);
);
if(is_object($postimg)) { if(is_object($postimg)) {
$postimg = '<p><img src="' $postimg = $postimg->find('img', 0);
. $postimg->find('img.dedicated', 0)->src if (!empty($postimg->src)) {
. '" alt="-" /></p>'; $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 { } else {
$postimg = ''; $postimg = '';
} }
$text = $subtitle // Paywall
. $postimg $paywall = $html->find('div.paywall-restriction', 0);
. $html->find('div[itemprop=articleBody], div.brief-inner-content', 0)->outertext; 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 { } else {
$text = $item['content'] $paywall = '';
. '<p><em>Failed retrieve full article content</em></p>';
} }
$premium_article = $html->find('h2.title_reserve_article', 0); // Content
if (is_object($premium_article)) { $article_content = $article_content->outertext;
$text .= '<p><em>' . $premium_article->innertext . '</em></p>'; $article_content = str_replace('>Signaler une erreur</span>', '></span>', $article_content);
// Result
$text = $subtitle
. $postimg
. $article_content
. $paywall;
} else {
$text = '<p><em>Failed to retrieve full article content</em></p>';
if (isset($item['content'])) {
$text = $item['content'] . $text;
}
} }
return $text; return $text;

View file

@ -148,7 +148,7 @@ class NineGagBridge extends BridgeAbstract {
} }
if (!$AvoidElement) { if (!$AvoidElement) {
$item['uri'] = $post['url']; $item['uri'] = preg_replace('/^http:/i', 'https:', $post['url']);
$item['title'] = $post['title']; $item['title'] = $post['title'];
$item['content'] = self::getContent($post); $item['content'] = self::getContent($post);
$item['categories'] = self::getCategories($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 //Retrieve data from page contents
$item_title = str_replace(' :: Nyaa', '', $item_html->find('title', 0)->plaintext); $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_author = extractFromDelimiters($item_html->outertext, 'href="/user/', '"');
$item_date = intval(extractFromDelimiters($item_html->outertext, 'data-timestamp="', '"')); $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 class PcGamerBridge extends BridgeAbstract
{ {
const NAME = 'PC Gamer'; 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 DESCRIPTION = 'PC Gamer Most Read Stories';
const MAINTAINER = 'mdemoss'; const CACHE_TIMEOUT = 3600;
const MAINTAINER = 'IceWreck, mdemoss';
public function collectData() public function collectData()
{ {
$html = getSimpleHTMLDOMCached($this->getURI(), 300); $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) { foreach ($stories as $element) {
if($i == 15) break;
$item['uri'] = $element->find('a', 0)->href; $item['uri'] = $element->find('a', 0)->href;
// error_log(print_r($item['uri'], TRUE));
$articleHtml = getSimpleHTMLDOMCached($item['uri']); $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['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content);
$item['content'] = $articleHtml->find('meta[name=description]', 0)->content; $item['author'] = $articleHtml->find('span.by-author a', 0)->plaintext;
$item['author'] = $articleHtml->find('a[itemprop=author]', 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; $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 { class RainbowSixSiegeBridge extends BridgeAbstract {
const MAINTAINER = 'corenting'; const MAINTAINER = 'corenting';
const NAME = 'Rainbow Six Siege Blog'; const NAME = 'Rainbow Six Siege News';
const URI = 'https://rainbow6.ubisoft.com/siege/en-us/news/'; const URI = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege/news-updates';
const CACHE_TIMEOUT = 7200; // 2h 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() { 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(){ public function collectData(){
$dlUrl = 'https://prod-tridionservice.ubisoft.com/live/v1/News/Latest?templateId=tcm%3A152-7677'; $dlUrl = 'https://www.ubisoft.com/api/updates/items?locale=en-us&categoriesFilter=all';
$dlUrl .= '8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A150-194572-64'; $dlUrl = $dlUrl . '&limit=6&mediaFilter=news&skip=0&startIndex=undefined&tags=BR-rainbow-six%20GA-siege';
$dlUrl .= '&keywordList=233416%2C316144%2C233418%2C233417&siteId=undefined&useSeoFriendlyUrl=true';
$jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content'); $jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
$json = json_decode($jsonString, true); $json = json_decode($jsonString, true);
@ -22,17 +21,20 @@ class RainbowSixSiegeBridge extends BridgeAbstract {
// Start at index 2 to remove highlighted articles // Start at index 2 to remove highlighted articles
for($i = 0; $i < count($json); $i++) { for($i = 0; $i < count($json); $i++) {
$jsonItem = $json[$i]['Content']; $jsonItem = $json[$i];
$article = str_get_html($jsonItem);
$uri = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege';
$uri = $uri . $jsonItem['button']['buttonUrl'];
$thumbnail = '<img src="' . $jsonItem['thumbnail']['url'] . '" alt="Thumbnail">';
$content = $thumbnail . '<br />' . markdownToHtml($jsonItem['content']);
$item = array(); $item = array();
$uri = $article->find('h3 a', 0)->href;
$uri = 'https://rainbow6.ubisoft.com' . $uri;
$item['uri'] = $uri; $item['uri'] = $uri;
$item['title'] = $article->find('h3', 0)->plaintext; $item['id'] = $jsonItem['id'];
$item['content'] = $article->find('img', 0)->outertext . '<br />' . $article->find('strong', 0)->plaintext; $item['title'] = $jsonItem['title'];
$item['timestamp'] = strtotime($article->find('p.news_date', 0)->plaintext); $item['content'] = $content;
$item['timestamp'] = strtotime($jsonItem['date']);
$this->items[] = $item; $this->items[] = $item;
} }

View file

@ -1,10 +1,11 @@
<?php <?php
class RedditBridge extends FeedExpander {
const MAINTAINER = 'leomaradan'; class RedditBridge extends BridgeAbstract {
const MAINTAINER = 'dawidsowa';
const NAME = 'Reddit Bridge'; const NAME = 'Reddit Bridge';
const URI = 'https://www.reddit.com/'; const URI = 'https://www.reddit.com';
const DESCRIPTION = 'Reddit RSS Feed fixer'; const DESCRIPTION = 'Return hot submissions from Reddit';
const PARAMETERS = array( const PARAMETERS = array(
'single' => array( 'single' => array(
@ -25,16 +26,153 @@ class RedditBridge extends FeedExpander {
) )
); );
public function collectData(){ public function getIcon() {
return 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png';
}
switch($this->queriedContext) { public function getName() {
case 'single': $subreddits[] = $this->getInput('r'); break; if ($this->queriedContext == 'single') {
case 'multi': $subreddits = explode(',', $this->getInput('rs')); break; 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) { foreach ($subreddits as $subreddit) {
$name = trim($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 NAME = '3DS Scene Releases';
const URI = 'http://www.3dsdb.com/'; const URI = 'http://www.3dsdb.com/';
const CACHE_TIMEOUT = 10800; // 3h 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(){ public function collectData(){
$this->collectDataUrl(self::URI . 'xml.php');
}
protected function collectDataUrl($dataUrl){
$dataUrl = self::URI . 'xml.php';
$xml = getContents($dataUrl) $xml = getContents($dataUrl)
or returnServerError('Could not request 3dsdb: ' . $dataUrl); or returnServerError('Could not request URL: ' . $dataUrl);
$limit = 0; $limit = 0;
foreach(array_reverse(explode('<release>', $xml)) as $element) { foreach(array_reverse(explode('<release>', $xml)) as $element) {
@ -52,18 +55,26 @@ class Releases3DSBridge extends BridgeAbstract {
$ignSearchUrl = 'https://www.ign.com/search?q=' . urlencode($name); $ignSearchUrl = 'https://www.ign.com/search?q=' . urlencode($name);
if($ignResult = getSimpleHTMLDOMCached($ignSearchUrl)) { if($ignResult = getSimpleHTMLDOMCached($ignSearchUrl)) {
$ignCoverArt = $ignResult->find('div.search-item-media', 0)->find('img', 0)->src; $ignCoverArt = $ignResult->find('div.search-item-media', 0);
$ignDesc = $ignResult->find('div.search-item-description', 0)->plaintext; $ignDesc = $ignResult->find('div.search-item-description', 0);
$ignLink = $ignResult->find('div.search-item-sub-title', 0)->find('a', 1)->href; $ignLink = $ignResult->find('div.search-item-sub-title', 0);
$ignDate = strtotime(trim($ignResult->find('span.publish-date', 0)->plaintext)); $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="' $ignDescription = '<div><img src="'
. $ignCoverArt . $ignCoverArt->src
. '" /></div><div>' . '" /></div><div>'
. $ignDesc . $ignDesc->plaintext
. ' <a href="' . ' <a href="'
. $ignLink . $ignLink->href
. '">More at IGN</a></div>'; . '">More at IGN</a></div>';
} }
}
//Main section : Release description from 3DS database //Main section : Release description from 3DS database
$releaseDescription = '<h3>Release Details</h3><b>Release ID: </b>' . $id $releaseDescription = '<h3>Release Details</h3><b>Release ID: </b>' . $id
@ -111,7 +122,7 @@ class Releases3DSBridge extends BridgeAbstract {
private function typeToString($type){ private function typeToString($type){
switch($type) { switch($type) {
case 1: return '3DS Game'; case 1: return 'Card Game';
case 4: return 'eShop'; case 4: return 'eShop';
default: return '??? (' . $type . ')'; 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){ private function extractContent($url){
$html2 = getSimpleHTMLDOM($url); $html2 = getSimpleHTMLDOM($url);
$html2 = defaultLinkTo($html2, self::URI);
foreach($html2->find('div[style=text-align:justify]') as $e) { foreach($html2->find('div[style=text-align:justify]') as $e) {
$text = $e->outertext; $text = $e->outertext;
@ -16,13 +17,6 @@ class ReporterreBridge extends BridgeAbstract {
$html2->clear(); $html2->clear();
unset($html2); 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>'); $text = strip_tags($text, '<p><br><a><img>');
return $text; 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 URI = 'https://rule34.paheal.net/';
const DESCRIPTION = 'Returns images from given page'; const DESCRIPTION = 'Returns images from given page';
const PATHTODATA = '.shm-thumb';
protected function getItemFromElement($element){ protected function getItemFromElement($element){
$item = array(); $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['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time(); $item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src; $thumbnailUri = $element->find('a', 1)->href;
$item['tags'] = $element->getAttribute('data-tags'); $item['tags'] = $element->getAttribute('data-tags');
$item['title'] = $this->getName() . ' | ' . $item['id']; $item['title'] = $this->getName() . ' | ' . $item['id'];
$item['content'] = '<a href="' $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 MAINTAINER = 'kranack';
const NAME = 'Sens Critique'; const NAME = 'Sens Critique';
const URI = 'http://www.senscritique.com/'; const URI = 'https://www.senscritique.com/';
const CACHE_TIMEOUT = 21600; // 6h const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Sens Critique news'; const DESCRIPTION = 'Sens Critique news';
const PARAMETERS = array( array( const PARAMETERS = array( array(
'm' => array(
'name' => 'Movies',
'type' => 'checkbox'
),
's' => array( 's' => array(
'name' => 'Series', 'name' => 'Series',
'type' => 'checkbox' 'type' => 'checkbox'
@ -40,8 +36,6 @@ class SensCritiqueBridge extends BridgeAbstract {
if($this->getInput($category)) { if($this->getInput($category)) {
$uri = self::URI; $uri = self::URI;
switch($category) { switch($category) {
case 'm': $uri .= 'films/cette-semaine';
break;
case 's': $uri .= 'series/actualite'; case 's': $uri .= 'series/actualite';
break; break;
case 'g': $uri .= 'jeuxvideo/actualite'; case 'g': $uri .= 'jeuxvideo/actualite';
@ -77,20 +71,25 @@ class SensCritiqueBridge extends BridgeAbstract {
. ' ' . ' '
. $movie->find('.elco-date', 0)->plaintext; . $movie->find('.elco-date', 0)->plaintext;
$item['content'] = '<em>' $item['content'] = '';
. $movie->find('.elco-original-title', 0)->plaintext $originalTitle = $movie->find('.elco-original-title', 0);
. '</em><br><br>' $description = $movie->find('.elco-description', 0);
. $movie->find('.elco-baseline', 0)->plaintext
if ($originalTitle) {
$item['content'] = '<em>' . $originalTitle->plaintext . '</em><br><br>';
}
$item['content'] .= $movie->find('.elco-baseline', 0)->plaintext
. '<br>' . '<br>'
. $movie->find('.elco-baseline', 1)->plaintext . $movie->find('.elco-baseline', 1)->plaintext
. '<br><br>' . '<br><br>'
. $movie->find('.elco-description', 0)->plaintext . ($description ? $description->plaintext : '')
. '<br><br>' . '<br><br>'
. trim($movie->find('.erra-ratings .erra-global', 0)->plaintext) . trim($movie->find('.erra-ratings .erra-global', 0)->plaintext)
. ' / 10'; . ' / 10';
$item['id'] = $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() . $movie->find('.elco-title a', 0)->href; $item['uri'] = $this->getURI() . ltrim($movie->find('.elco-title a', 0)->href, '/');
$this->items[] = $item; $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() { public function getName() {
switch($this->queriedContext) { switch($this->queriedContext) {

View file

@ -11,44 +11,57 @@ class SoundCloudBridge extends BridgeAbstract {
'u' => array( 'u' => array(
'name' => 'username', 'name' => 'username',
'required' => true 'required' => true
),
't' => array(
'name' => 'type',
'type' => 'list',
'defaultValue' => 'tracks',
'values' => array(
'Tracks' => 'tracks',
'Playlists' => 'playlists'
)
) )
)); ));
private $feedTitle = null;
private $feedIcon = null; private $feedIcon = null;
private $clientIDCache = null; private $clientIDCache = null;
public function collectData(){ public function collectData(){
$res = $this->apiGet('resolve', array( $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'); )) or returnServerError('No results for this query');
$this->feedTitle = $res->username;
$this->feedIcon = $res->avatar_url; $this->feedIcon = $res->avatar_url;
$tracks = $this->apiGet('users/' . urlencode($res->id) . '/tracks') $tracks = $this->apiGet(
or returnServerError('No results for this user'); 'users/' . urlencode($res->id) . '/' . $this->getInput('t'),
array('limit' => 31)
) or returnServerError('No results for this user/playlist');
$numTracks = min(count($tracks), 10); foreach ($tracks->collection as $index => $track) {
for($i = 0; $i < $numTracks; $i++) {
$item = array(); $item = array();
$item['author'] = $tracks[$i]->user->username; $item['author'] = $track->user->username;
$item['title'] = $tracks[$i]->user->username . ' - ' . $tracks[$i]->title; $item['title'] = $track->user->username . ' - ' . $track->title;
$item['timestamp'] = strtotime($tracks[$i]->created_at); $item['timestamp'] = strtotime($track->created_at);
$item['content'] = $tracks[$i]->description; $item['content'] = nl2br($track->description);
$item['enclosures'] = array($tracks[$i]->uri $item['enclosures'][] = $track->artwork_url;
. '/stream?client_id='
. $this->getClientID());
$item['id'] = self::URI $item['id'] = self::URI
. urlencode($this->getInput('u')) . urlencode($this->getInput('u'))
. '/' . '/'
. urlencode($tracks[$i]->permalink); . urlencode($track->permalink);
$item['uri'] = self::URI $item['uri'] = self::URI
. urlencode($this->getInput('u')) . urlencode($this->getInput('u'))
. '/' . '/'
. urlencode($tracks[$i]->permalink); . urlencode($track->permalink);
$this->items[] = $item; $this->items[] = $item;
}
if (count($this->items) >= 10) {
break;
}
}
} }
public function getIcon(){ public function getIcon(){
@ -59,9 +72,13 @@ class SoundCloudBridge extends BridgeAbstract {
return parent::getIcon(); return parent::getIcon();
} }
public function getURI(){
return 'https://soundcloud.com/' . $this->getInput('u');
}
public function getName(){ public function getName(){
if(!is_null($this->getInput('u'))) { if($this->feedTitle) {
return self::NAME . ' - ' . $this->getInput('u'); return $this->feedTitle . ' - ' . self::NAME;
} }
return parent::getName(); return parent::getName();
@ -113,13 +130,13 @@ class SoundCloudBridge extends BridgeAbstract {
} }
private function buildAPIURL($endpoint, $parameters){ private function buildAPIURL($endpoint, $parameters){
return 'https://api.soundcloud.com/' return 'https://api-v2.soundcloud.com/'
. $endpoint . $endpoint
. '?' . '?'
. http_build_query($parameters); . http_build_query($parameters);
} }
private function apiGet($endpoint, $parameters = array()){ private function apiGet($endpoint, $parameters = array()) {
$parameters['client_id'] = $this->getClientID(); $parameters['client_id'] = $this->getClientID();
try { try {

View file

@ -39,8 +39,8 @@ class TelegramBridge extends BridgeAbstract {
$item = array(); $item = array();
$item['uri'] = $this->processUri($messageDiv); $item['uri'] = $this->processUri($messageDiv);
$item['content'] = html_entity_decode($this->processContent($messageDiv), ENT_QUOTES); $item['content'] = $this->processContent($messageDiv);
$item['title'] = html_entity_decode($this->itemTitle, ENT_QUOTES); $item['title'] = $this->itemTitle;
$item['timestamp'] = $this->processDate($messageDiv); $item['timestamp'] = $this->processDate($messageDiv);
$item['enclosures'] = $this->enclosures; $item['enclosures'] = $this->enclosures;
$author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext); $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) $html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request The Coding Love.'); 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(); $item = array();
$temp = $element->find('h3 a', 0); $temp = $element->find('h1 a', 0);
$titre = $temp->innertext; $title = $temp->innertext;
$url = $temp->href; $url = $temp->href;
$temp = $element->find('div.bodytype', 0); $temp = $element->find('div.blog-post-content', 0);
// retrieve .gif instead of static .jpg // retrieve .gif instead of static .jpg
$images = $temp->find('p.e img'); $images = $temp->find('p.e img');
@ -28,17 +28,13 @@ class TheCodingLoveBridge extends BridgeAbstract {
} }
$content = $temp->innertext; $content = $temp->innertext;
$auteur = $temp->find('i', 0); $temp = $element->find('div.post-meta-info', 0);
$pos = strpos($auteur->innertext, 'by'); $author = $temp->find('span', 0);
$item['author'] = $author->innertext;
if($pos > 0) {
$auteur = trim(str_replace('*/', '', substr($auteur->innertext, ($pos + 2))));
$item['author'] = $auteur;
}
$item['content'] .= trim($content); $item['content'] .= trim($content);
$item['uri'] = $url; $item['uri'] = $url;
$item['title'] = trim($titre); $item['title'] = trim($title);
$this->items[] = $item; $this->items[] = $item;
} }

View file

@ -17,6 +17,7 @@ class TheHackerNewsBridge extends BridgeAbstract {
$article_url = $element->find('a.story-link', 0)->href; $article_url = $element->find('a.story-link', 0)->href;
$article_author = trim($element->find('i.icon-user', 0)->parent()->plaintext); $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; $article_title = $element->find('h2.home-title', 0)->plaintext;
//Date without time //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', 'All' => 'all',
'Archive' => 'archive', 'Archive' => 'archive',
'Highlights' => 'highlight', 'Highlights' => 'highlight',
'Uploads' => 'upload' 'Uploads' => 'upload',
'Past Premieres' => 'past_premiere',
'Premiere Uploads' => 'premiere_upload'
), ),
'defaultValue' => 'archive' 'defaultValue' => 'archive'
) )
@ -32,43 +34,90 @@ class TwitchBridge extends BridgeAbstract {
*/ */
const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; 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(){ public function collectData(){
// get channel user $query = <<<'EOD'
$query_data = array( query VODList($channel: String!, $types: [BroadcastType!]) {
'login' => $this->getInput('channel') 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; $data = $this->apiRequest($query, $variables);
if(count($users) === 0)
returnClientError('User "'
. $this->getInput('channel')
. '" could not be found');
$user = $users[0];
// get video list $user = $data->user;
$query_endpoint = 'channels/' . $user->_id . '/videos'; foreach($user->videos->edges as $edge) {
$query_data = array( $video = $edge->node;
'broadcast_type' => $this->getInput('type'),
'limit' => 10 $url = 'https://www.twitch.tv/videos/' . $video->id;
);
$videos = $this->apiGet($query_endpoint, $query_data)->videos;
foreach($videos as $video) {
$item = array( $item = array(
'uri' => $video->url, 'uri' => $url,
'title' => $video->title, 'title' => $video->title,
'timestamp' => $video->published_at, 'timestamp' => $video->publishedAt,
'author' => $video->channel->display_name, 'author' => $user->displayName,
); );
// Add categories for tags and played game // Add categories for tags and played game
$item['categories'] = array_filter(explode(' ', $video->tag_list)); $item['categories'] = $video->tags;
if(!empty($video->game)) if(!is_null($video->game))
$item['categories'][] = $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 // Add enclosures for thumbnails from a few points in the video
$item['enclosures'] = array(); // Thumbnail list has duplicate entries sometimes so remove those
foreach($video->thumbnails->large as $thumbnail) $item['enclosures'] = array_unique($video->thumbnailURLs);
$item['enclosures'][] = $thumbnail->url;
/* /*
* Content format example: * Content format example:
@ -86,44 +135,45 @@ class TwitchBridge extends BridgeAbstract {
* *
*/ */
$item['content'] = '<p><a href="' $item['content'] = '<p><a href="'
. $video->url . $url
. '"><img src="' . '"><img src="'
. $video->preview->large . $video->previewThumbnailURL
. '" /></a></p><p>' . '" /></a></p><p>'
. $video->description_html . $video->description // in markdown format
. '</p><p><b>Duration:</b> ' . '</p><p><b>Duration:</b> '
. $this->formatTimestampTime($video->length) . $this->formatTimestampTime($video->lengthSeconds)
. '<br/><b>Views:</b> ' . '<br/><b>Views:</b> '
. $video->views . $video->viewCount
. '</p>'; . '</p>';
// Add played games list to content // Add played games list to content
$video_id = trim($video->_id, 'v'); // _id gives 'v1234' but API wants '1234' $item['content'] .= '<p><b>Played games:</b><ul>';
$markers = $this->apiGet('videos/' . $video_id . '/markers')->markers; if(count($video->moments->edges) > 0) {
$item['content'] .= '<p><b>Played games:</b></b><ul><li><a href="' foreach($video->moments->edges as $edge) {
. $video->url $moment = $edge->node;
. '">00:00:00</a> - '
. $video->game $item['categories'][] = $moment->description;
. '</li>';
if(isset($markers->game_changes)) {
usort($markers->game_changes, function($a, $b) {
return $a->time - $b->time;
});
foreach($markers->game_changes as $game_change) {
$item['categories'][] = $game_change->label;
$item['content'] .= '<li><a href="' $item['content'] .= '<li><a href="'
. $video->url . $url
. '?t=' . '?t='
. $this->formatQueryTime($game_change->time) . $this->formatQueryTime($moment->positionMilliseconds / 1000)
. '">' . '">'
. $this->formatTimestampTime($game_change->time) . $this->formatTimestampTime($moment->positionMilliseconds / 1000)
. '</a> - ' . '</a> - '
. $game_change->label . $moment->description
. '</li>'; . '</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['content'] .= '</ul></p>';
$item['categories'] = array_unique($item['categories']);
$this->items[] = $item; $this->items[] = $item;
} }
} }
@ -144,25 +194,37 @@ class TwitchBridge extends BridgeAbstract {
$seconds % 60); $seconds % 60);
} }
/* // GraphQL: https://graphql.org/
* Ideally the new 'helix' API should be used as v5/'kraken' is deprecated. // Tool for developing/testing queries: https://github.com/skevy/graphiql-app
* The new API however still misses many features (markers, played game..) of private function apiRequest($query, $variables) {
* the old one, so let's use the old one for as long as it's available. $request = array(
*/ 'query' => $query,
private function apiGet($endpoint, $query_data = array()) { 'variables' => $variables
$query_data['api_version'] = 5; );
$url = 'https://api.twitch.tv/kraken/'
. $endpoint
. '?'
. http_build_query($query_data);
$header = array( $header = array(
'Client-ID: ' . self::CLIENT_ID 'Client-ID: ' . self::CLIENT_ID
); );
$opts = array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => json_encode($request)
);
$data = json_decode(getContents($url, $header)) Debug::log("Sending GraphQL query:\n" . $query);
or returnServerError('API request to "' . $url . '" failed.'); 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(){ public function getName(){

View file

@ -2,6 +2,9 @@
class TwitterBridge extends BridgeAbstract { class TwitterBridge extends BridgeAbstract {
const NAME = 'Twitter Bridge'; const NAME = 'Twitter Bridge';
const URI = 'https://twitter.com/'; 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 CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'returns tweets'; const DESCRIPTION = 'returns tweets';
const MAINTAINER = 'pmaziere'; const MAINTAINER = 'pmaziere';
@ -72,6 +75,12 @@ EOD
'required' => false, 'required' => false,
'type' => 'checkbox', 'type' => 'checkbox',
'title' => 'Hide retweets' 'title' => 'Hide retweets'
),
'nopinned' => array(
'name' => 'Without pinned tweet',
'required' => false,
'type' => 'checkbox',
'title' => 'Hide pinned tweet'
) )
), ),
'By list' => array( 'By list' => array(
@ -92,6 +101,20 @@ EOD
'required' => false, 'required' => false,
'title' => 'Specify term to search for' '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; break;
case 'By list': case 'By list':
return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user'); return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
case 'By list ID':
return 'Twitter List #' . $this->getInput('listid');
default: return parent::getName(); default: return parent::getName();
} }
return 'Twitter ' . $specific . $this->getInput($param); return 'Twitter ' . $specific . $this->getInput($param);
@ -164,22 +189,46 @@ EOD
. urlencode($this->getInput('user')) . urlencode($this->getInput('user'))
. '/lists/' . '/lists/'
. str_replace(' ', '-', strtolower($this->getInput('list'))); . str_replace(' ', '-', strtolower($this->getInput('list')));
case 'By list ID':
return self::URI
. 'i/lists/'
. urlencode($this->getInput('listid'));
default: return parent::getURI(); 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(){ public function collectData(){
$html = ''; $html = '';
$page = $this->getURI(); $page = $this->getURI();
$data = json_decode($this->getApiContents($this->getApiURI()));
if(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo'))) { if(!$data) {
$cookies = $this->getCookies($page);
$html = getSimpleHTMLDOM($page, array("Cookie: $cookies"));
} else {
$html = getSimpleHTMLDOM($page, array(), array(CURLOPT_COOKIEFILE => ''));
}
if(!$html) {
switch($this->queriedContext) { switch($this->queriedContext) {
case 'By keyword or hashtag': case 'By keyword or hashtag':
returnServerError('No results for this query.'); returnServerError('No results for this query.');
@ -192,75 +241,93 @@ EOD
$hidePictures = $this->getInput('nopic'); $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? $hidePinned = $this->getInput('nopinned');
if($this->getInput('noretweet') if ($hidePinned) {
&& $tweet->find('div.context span.js-retweet-text a', 0)) { $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; continue;
} }
// remove 'invisible' content // Skip promoted tweets
foreach($tweet->find('.invisible') as $invisible) { if (in_array($tweet->id_str, $promotedTweetIds)) {
$invisible->outertext = ''; continue;
} }
// Skip protmoted tweets // Skip pinned tweet
$heading = $tweet->previousSibling(); if ($hidePinned && $tweet->id_str === $pinnedTweetId) {
if(!is_null($heading) &&
$heading->getAttribute('class') === 'promoted-tweet-heading'
) {
continue; continue;
} }
$item = array(); $item = array();
// extract username and sanitize // extract username and sanitize
$item['username'] = htmlspecialchars_decode($tweet->getAttribute('data-screen-name'), ENT_QUOTES); $user_info = $this->getUserInformation($tweet->user_id_str, $data->globalObjects);
// extract fullname (pseudonym)
$item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES); $item['username'] = $user_info->screen_name;
// get author $item['fullname'] = $user_info->name;
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')'; $item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
if($rt = $tweet->find('div.context span.js-retweet-text a', 0)) { if (null !== $this->getInput('u') && strtolower($item['username']) != strtolower($this->getInput('u'))) {
$item['author'] .= ' RT: @' . $rt->plaintext; $item['author'] .= ' RT: @' . $this->getInput('u');
} }
// get avatar link $item['avatar'] = $user_info->profile_image_url_https;
$item['avatar'] = $tweet->find('img', 0)->src;
// get TweetID $item['id'] = $tweet->id_str;
$item['id'] = $tweet->getAttribute('data-tweet-id'); $item['uri'] = self::URI . $item['username'] . '/status/' . $item['id'];
// get tweet link
$item['uri'] = self::URI . substr($tweet->find('a.js-permalink', 0)->getAttribute('href'), 1);
// extract tweet timestamp // extract tweet timestamp
$item['timestamp'] = $tweet->find('span.js-short-timestamp', 0)->getAttribute('data-time'); $item['timestamp'] = $tweet->created_at;
// 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 // generate the title
$item['title'] = strip_tags($this->fixAnchorSpacing(htmlspecialchars_decode( $item['title'] = strip_tags($cleanedTweet);
$tweet->find('p.js-tweet-text', 0), ENT_QUOTES), '<a>'));
switch($this->queriedContext) { // Add avatar
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:
}
$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
$picture_html = ''; $picture_html = '';
if(!$hidePictures) { if(!$hidePictures) {
$picture_html = <<<EOD $picture_html = <<<EOD
@ -274,31 +341,79 @@ EOD
EOD; EOD;
} }
// Add embeded image to content // Get images
$image_html = ''; $media_html = '';
$images = $this->getImageURI($tweet); if(isset($tweet->extended_entities->media) && !$this->getInput('noimg')) {
if(!$this->getInput('noimg') && !is_null($images)) { foreach($tweet->extended_entities->media as $media) {
switch($media->type) {
foreach ($images as $image) { case 'photo':
$image = $media->media_url_https . '?name=orig';
// Set image scaling $display_image = $media->media_url_https;
$image_orig = $this->getInput('noimgscaling') ? $image : $image . ':orig';
$image_thumb = $this->getInput('noimgscaling') ? $image : $image . ':thumb';
// add enclosures // add enclosures
$item['enclosures'][] = $image_orig; $item['enclosures'][] = $image;
$image_html .= <<<EOD $media_html .= <<<EOD
<a href="{$image_orig}"> <a href="{$image}">
<img <img
style="align:top; max-width:558px; border:1px solid black;" style="align:top; max-width:558px; border:1px solid black;"
src="{$image_thumb}" /> referrerpolicy="no-referrer"
src="{$display_image}" />
</a> </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; EOD;
} }
} }
break;
default:
Debug::log('Missing support for media type: ' . $media->type);
}
}
}
switch($this->queriedContext) {
case 'By list':
case 'By list ID':
// Check if filter applies to list (using raw content)
if($this->getInput('filter')) {
if(stripos($cleanedTweet, $this->getInput('filter')) === false) {
continue 2; // switch + for-loop!
}
}
break;
case 'By username':
if ($this->getInput('noretweet') && strtolower($item['username']) != strtolower($this->getInput('u'))) {
continue 2; // switch + for-loop!
}
break;
default:
}
// add content
$item['content'] = <<<EOD $item['content'] = <<<EOD
<div style="display: inline-block; vertical-align: top;"> <div style="display: inline-block; vertical-align: top;">
{$picture_html} {$picture_html}
@ -307,155 +422,149 @@ EOD;
<blockquote>{$cleanedTweet}</blockquote> <blockquote>{$cleanedTweet}</blockquote>
</div> </div>
<div style="display: block; vertical-align: top;"> <div style="display: block; vertical-align: top;">
<blockquote>{$image_html}</blockquote> <blockquote>{$media_html}</blockquote>
</div> </div>
EOD; 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); $item['content'] = htmlspecialchars_decode($item['content'], ENT_QUOTES);
// put out // put out
$this->items[] = $item; $this->items[] = $item;
} }
usort($this->items, array('TwitterBridge', 'compareTweetId'));
} }
private function processEmojis($tweet){ private static function compareTweetId($tweet1, $tweet2) {
// process emojis (reduce size) return (intval($tweet1['id']) < intval($tweet2['id']) ? 1 : -1);
foreach($tweet->find('img.Emoji') as $img) {
$img->style .= ' height: 1em;';
}
} }
private function processContentLinks($tweet){ //The aim of this function is to get an API key and a guest token
// processing content links //This function takes 2 requests, and therefore is cached
foreach($tweet->find('a') as $link) { private function getApiKey() {
if($link->hasAttribute('data-expanded-url')) {
$link->href = $link->getAttribute('data-expanded-url'); $cacheFac = new CacheFactory();
} $cacheFac->setWorkingDir(PATH_LIB_CACHES);
$link->removeAttribute('data-expanded-url'); $r_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$link->removeAttribute('data-query-source'); $r_cache->setScope(get_called_class());
$link->removeAttribute('rel'); $r_cache->setKey(array('refresh'));
$link->removeAttribute('class'); $data = $r_cache->loadData();
$link->removeAttribute('target');
$link->removeAttribute('title'); $refresh = null;
} if($data === null) {
$refresh = time();
$r_cache->saveData($refresh);
} else {
$refresh = $data;
} }
private function fixAnchorSpacing($content){ $cacheFac = new CacheFactory();
// fix anchors missing spaces in-between $cacheFac->setWorkingDir(PATH_LIB_CACHES);
return str_replace( $cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
'<a', $cache->setScope(get_called_class());
' <a', $cache->setKey(array('api_key'));
$content $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)) {
private function getImageURI($tweet){ $jsLink = $jsMainMatches[0][0];
// Find media in tweet break;
$images = array();
$container = $tweet->find('div.AdaptiveMedia-container', 0);
if($container && $container->find('img', 0)) {
foreach ($container->find('img') as $img) {
$images[] = $img->src;
} }
} }
if (!$jsLink) {
if (!empty($images)) { returnServerError('Could not locate main.js link');
return $images;
} }
return null; $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;
} }
private function getQuotedImageURI($tweet){ $cacheFac2 = new CacheFactory();
// Find media in tweet $cacheFac2->setWorkingDir(PATH_LIB_CACHES);
$images = array(); $gt_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$gt_cache->setScope(get_called_class());
$gt_cache->setKey(array('guest_token'));
$guestTokenUses = $gt_cache->loadData();
$container = $tweet->find('div.QuoteMedia-container', 0); $guestToken = null;
if($guestTokenUses === null || !is_array($guestTokenUses) || count($guestTokenUses) != 2
if($container && $container->find('img', 0)) { || $guestTokenUses[0] <= 0 || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) {
foreach ($container->find('img') as $img) { $guestToken = $this->getGuestToken();
$images[] = $img->src; $gt_cache->saveData(array(self::GUEST_TOKEN_USES, $guestToken));
} $r_cache->saveData(time());
} else {
$guestTokenUses[0] -= 1;
$gt_cache->saveData($guestTokenUses);
$guestToken = $guestTokenUses[1];
} }
if (!empty($images)) { return array($apiKey, $guestToken);
return $images;
} }
return null; // 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 getCookies($pageURL){ private function getApiContents($uri) {
$apiKeys = $this->getApiKey();
$ctx = stream_context_create(array( $headers = array('authorization: Bearer ' . $apiKeys[0],
'http' => array( 'x-guest-token: ' . $apiKeys[1],
'follow_location' => false
)
)
); );
$a = file_get_contents($pageURL, 0, $ctx); return getContents($uri, $headers);
//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); 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;
}
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;
}
}
} }
} }

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