mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2025-08-15 21:14:07 +02:00
Compare commits
153 Commits
fix1
...
dvikan-pat
Author | SHA1 | Date | |
---|---|---|---|
|
961a576d15 | ||
|
166ead902d | ||
|
de279de762 | ||
|
347f9a3eda | ||
|
1af6cbeb1e | ||
|
37f211a37e | ||
|
bea0595e5c | ||
|
6df5a4bc14 | ||
|
3927ecd822 | ||
|
1b0a6f2813 | ||
|
8f0d90f653 | ||
|
037d5866ca | ||
|
75c4c9f256 | ||
|
baa4ea8338 | ||
|
12ddee4054 | ||
|
6582a66a2d | ||
|
a4785370fa | ||
|
aa32040bd4 | ||
|
44e8007d9c | ||
|
90d22f0d80 | ||
|
fb501652d5 | ||
|
e85932b1a5 | ||
|
53f9970403 | ||
|
19ad2584da | ||
|
190c95fa62 | ||
|
678e5d9866 | ||
|
4787eb3799 | ||
|
a863234474 | ||
|
4260be26a2 | ||
|
713d06ba08 | ||
|
7256d1138b | ||
|
71310d2c5a | ||
|
92d813fbea | ||
|
3f896f9465 | ||
|
b7e1dc1ab1 | ||
|
8e41887393 | ||
|
8865521b3b | ||
|
8172d10bb5 | ||
|
299ad87168 | ||
|
d60d8313d0 | ||
|
1fd2f37bb4 | ||
|
04b1609ce0 | ||
|
2fa24e780b | ||
|
3b04e318ae | ||
|
05cd1c0b67 | ||
|
cb05cacd6a | ||
|
4d18312604 | ||
|
85e5ce2679 | ||
|
7afc577e97 | ||
|
462319344b | ||
|
5cc34b884a | ||
|
dd025894e9 | ||
|
1d0a0b927b | ||
|
4007afdcf5 | ||
|
7d00b0c5df | ||
|
0212c4790f | ||
|
7a87a09fc5 | ||
|
1e3f5f3ad3 | ||
|
f709778b28 | ||
|
f4a0711b62 | ||
|
4d069fcf99 | ||
|
f00f90328d | ||
|
bb6d553dd5 | ||
|
97b513823d | ||
|
e01f0bcaf2 | ||
|
e5829d37b6 | ||
|
73b1a6a7aa | ||
|
e07fac777a | ||
|
fd449be4eb | ||
|
829fc6cca2 | ||
|
fcc3707210 | ||
|
d5e9dbf47d | ||
|
96a63a8e81 | ||
|
9110b70f07 | ||
|
6547ed0c04 | ||
|
8982995445 | ||
|
76084cdcca | ||
|
a28dca2c9d | ||
|
51f0d046d0 | ||
|
fb2ed95368 | ||
|
36d11fd06e | ||
|
d107592094 | ||
|
0ce71d561d | ||
|
f5a51038cc | ||
|
3476b06ee0 | ||
|
158ee41be4 | ||
|
37843e8777 | ||
|
56e991122b | ||
|
5d77d14f9d | ||
|
0c7a7f320f | ||
|
b2f1d051fc | ||
|
641e2eedf5 | ||
|
bc773a49f8 | ||
|
410daee1d5 | ||
|
adeaede930 | ||
|
9b82ff352d | ||
|
31455b6838 | ||
|
63b08f7da9 | ||
|
61cfbe6c53 | ||
|
4c26950b71 | ||
|
9dc31dfcfa | ||
|
db8462e6fa | ||
|
19a8165fc6 | ||
|
0ef298f9cc | ||
|
b090b17bbf | ||
|
ca749e7bad | ||
|
e1c898848f | ||
|
46a356b0b2 | ||
|
fe042305e4 | ||
|
669e92357a | ||
|
1dec457b7b | ||
|
a38951b911 | ||
|
b11f1368bf | ||
|
1a698b3554 | ||
|
73ebdbf67a | ||
|
ac766aa47f | ||
|
2be613e015 | ||
|
924eaf2011 | ||
|
d082bfca4a | ||
|
91283f3a62 | ||
|
d6beb713b5 | ||
|
d62b977394 | ||
|
183004f954 | ||
|
ff8ece213f | ||
|
3e5675c256 | ||
|
5a7d305e07 | ||
|
7379e2b3d5 | ||
|
57c8806954 | ||
|
b6e8350596 | ||
|
5b7dd45b20 | ||
|
f9801a5c58 | ||
|
563c099d80 | ||
|
b6e8e3ea6e | ||
|
9e2e32a19d | ||
|
df5c259375 | ||
|
6021d2ffa6 | ||
|
908da78113 | ||
|
a28481aaa8 | ||
|
bb81af086f | ||
|
60f1c46779 | ||
|
ae760e40cc | ||
|
5a733b3d82 | ||
|
0b40f51c01 | ||
|
dbee47f1d6 | ||
|
c3a106892d | ||
|
db28bedb23 | ||
|
aacf5812ff | ||
|
bf2f9a06f9 | ||
|
7833d0e6c3 | ||
|
c498749c2b | ||
|
722f9ff0ce | ||
|
5c08984714 | ||
|
dc01891634 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,5 +1,6 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
|
46
.github/CONTRIBUTING.md
vendored
46
.github/CONTRIBUTING.md
vendored
@@ -1,49 +1,7 @@
|
||||
### Pull request policy
|
||||
|
||||
* [Fix one issue per pull request](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#fix-one-issue-per-pull-request)
|
||||
* [Respect the coding style policy](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#respect-the-coding-style-policy)
|
||||
* [Properly name your commits](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#properly-name-your-commits)
|
||||
* When fixing a bridge (located in the `bridges` directory), write `[BridgeName] Feature` <br>(i.e. `[YoutubeBridge] Fix typo in video titles`).
|
||||
* When fixing other files, use `[FileName] Feature` <br>(i.e. `[index.php] Add multilingual support`).
|
||||
* When fixing a general problem that applies to multiple files, write `category: feature` <br>(i.e. `bridges: Fix various typos`).
|
||||
|
||||
Note that all pull-requests must pass all tests before they can be merged.
|
||||
See the [Pull request policy page on the documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/Pull_Request_policy.html) for more information on the pull request policy.
|
||||
|
||||
### Coding style
|
||||
|
||||
* [Whitespace](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace)
|
||||
* [Add a new line at the end of a file](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
|
||||
* [Do not add a whitespace before a semicolon](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
|
||||
* [Do not add whitespace at start or end of a file or end of a line](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#do-not-add-whitespace-at-start-or-end-of-a-file-or-end-of-a-line)
|
||||
* [Indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation)
|
||||
* [Use tabs for indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation#use-tabs-for-indentation)
|
||||
* [Maximum line length](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length)
|
||||
* [The maximum line length should not exceed 80 characters](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length#the-maximum-line-length-should-not-exceed-80-characters)
|
||||
* [Strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings)
|
||||
* [Whenever possible use single quoted strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#whenever-possible-use-single-quote-strings)
|
||||
* [Add spaces around the concatenation operator](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#add-spaces-around-the-concatenation-operator)
|
||||
* [Use a single string instead of concatenating](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#use-a-single-string-instead-of-concatenating)
|
||||
* [Constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants)
|
||||
* [Use UPPERCASE for constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants#use-uppercase-for-constants)
|
||||
* [Keywords](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords)
|
||||
* [Use lowercase for `true`, `false` and `null`](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords#use-lowercase-for-true-false-and-null)
|
||||
* [Operators](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators)
|
||||
* [Operators must have a space around them](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators#operators-must-have-a-space-around-them)
|
||||
* [Functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions)
|
||||
* [Parameters with default values must appear last in functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#parameters-with-default-values-must-appear-last-in-functions)
|
||||
* [Calling functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#calling-functions)
|
||||
* [Do not add spaces after opening or before closing bracket](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#do-not-add-spaces-after-opening-or-before-closing-bracket)
|
||||
* [Structures](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures)
|
||||
* [Structures must always be formatted as multi-line blocks](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures#structures-must-always-be-formatted-as-multi-line-blocks)
|
||||
* [If-Statement](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement)
|
||||
* [Use `elseif` instead of `else if`](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#use-elseif-instead-of-else-if)
|
||||
* [Do not write empty statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-empty-statements)
|
||||
* [Do not write unconditional if-statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-unconditional-if-statements)
|
||||
* [Classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes)
|
||||
* [Use PascalCase for class names](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#use-pascalcase-for-class-names)
|
||||
* [Do not use final statements inside final classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-use-final-statements-inside-final-classes)
|
||||
* [Do not override methods to call their parent](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-override-methods-to-call-their-parent)
|
||||
* [abstract and final declarations MUST precede the visibility declaration](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#abstract-and-final-declarations-must-precede-the-visibility-declaration)
|
||||
* [static declaration MUST come after the visibility declaration](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#static-declaration-must-come-after-the-visibility-declaration)
|
||||
* [Casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting)
|
||||
* [Do not add spaces when casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting#do-not-add-spaces-when-casting)
|
||||
See the [Coding style policy page on the documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/Coding_style_policy.html) for more information on the coding style of the project.
|
||||
|
2
.github/ISSUE_TEMPLATE/bridge-request.md
vendored
2
.github/ISSUE_TEMPLATE/bridge-request.md
vendored
@@ -60,5 +60,5 @@ Please describe what you expect from the bridge. Whenever possible provide sampl
|
||||
|
||||
Keep in mind that opening a request does not guarantee the bridge being implemented! That depends entirely on the interest and time of others to make the bridge for you.
|
||||
|
||||
You can also implement your own bridge (with support of the community if needed). Find more information in the [RSS-Bridge Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/For-developers) developer section.
|
||||
You can also implement your own bridge (with support of the community if needed). Find more information in the [RSS-Bridge Documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/index.html) developer section.
|
||||
-->
|
||||
|
12
.github/prtester.py
vendored
12
.github/prtester.py
vendored
@@ -1,4 +1,5 @@
|
||||
import requests
|
||||
import itertools
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
import os.path
|
||||
@@ -47,15 +48,18 @@ def testBridges(bridges,status):
|
||||
if parameter.get('type') == 'checkbox':
|
||||
if parameter.has_attr('checked'):
|
||||
formstring = formstring + '&' + parameter.get('name') + '=on'
|
||||
for list in lists:
|
||||
for listing in lists:
|
||||
selectionvalue = ''
|
||||
for selectionentry in list.contents:
|
||||
listname = listing.get('name')
|
||||
if 'optgroup' in listing.contents[0].name:
|
||||
listing = list(itertools.chain.from_iterable(listing))
|
||||
for selectionentry in listing:
|
||||
if 'selected' in selectionentry.attrs:
|
||||
selectionvalue = selectionentry.get('value')
|
||||
break
|
||||
if selectionvalue == '':
|
||||
selectionvalue = list.contents[0].get('value')
|
||||
formstring = formstring + '&' + list.get('name') + '=' + selectionvalue
|
||||
selectionvalue = listing.contents[0].get('value')
|
||||
formstring = formstring + '&' + listname + '=' + selectionvalue
|
||||
if not errormessages:
|
||||
# if all example/default values are present, form the full request string, run the request, replace the static css
|
||||
# file with the url of em's public instance and then upload it to termpad.com, a pastebin-like-site.
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -229,6 +229,7 @@ pip-log.txt
|
||||
/whitelist.txt
|
||||
DEBUG
|
||||
config.ini.php
|
||||
config/*
|
||||
|
||||
######################
|
||||
## VisualStudioCode ##
|
||||
|
30
Dockerfile
30
Dockerfile
@@ -1,24 +1,24 @@
|
||||
FROM php:7-apache-buster
|
||||
FROM php:7.4.29-fpm
|
||||
|
||||
LABEL description="RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one."
|
||||
LABEL repository="https://github.com/RSS-Bridge/rss-bridge"
|
||||
LABEL website="https://github.com/RSS-Bridge/rss-bridge"
|
||||
|
||||
ENV APACHE_DOCUMENT_ROOT=/app
|
||||
RUN apt-get update && \
|
||||
apt-get install --yes --no-install-recommends \
|
||||
nginx \
|
||||
zlib1g-dev \
|
||||
libzip-dev \
|
||||
libmemcached-dev && \
|
||||
docker-php-ext-install zip && \
|
||||
pecl install memcached && \
|
||||
docker-php-ext-enable memcached && \
|
||||
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 --no-install-recommends install \
|
||||
zlib1g-dev \
|
||||
libmemcached-dev \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& pecl install memcached \
|
||||
&& docker-php-ext-enable memcached \
|
||||
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
|
||||
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
|
||||
&& sed -ri -e 's/(MinProtocol\s*=\s*)TLSv1\.2/\1None/' /etc/ssl/openssl.cnf \
|
||||
&& sed -ri -e 's/(CipherString\s*=\s*DEFAULT)@SECLEVEL=2/\1/' /etc/ssl/openssl.cnf
|
||||
COPY ./config/nginx.conf /etc/nginx/sites-enabled/default
|
||||
|
||||
COPY --chown=www-data:www-data ./ /app/
|
||||
|
||||
CMD ["/app/docker-entrypoint.sh"]
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
43
README.md
43
README.md
@@ -15,17 +15,17 @@ Supported sites/pages (examples)
|
||||
===
|
||||
|
||||
* `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
|
||||
* `Cryptome` : Returns the most recent documents from [Cryptome.org](http://cryptome.org/)
|
||||
* `DansTonChat`: Most recent quotes from [danstonchat.com](http://danstonchat.com/)
|
||||
* `Cryptome` : Returns the most recent documents from [Cryptome.org](https://cryptome.org/)
|
||||
* `DansTonChat`: Most recent quotes from [danstonchat.com](https://danstonchat.com/)
|
||||
* `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
|
||||
* `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/) (There is an [issue](https://github.com/RSS-Bridge/rss-bridge/issues/2047) for public instances)
|
||||
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
|
||||
* `FlickrExplore` : [Latest interesting images](https://www.flickr.com/explore) from Flickr
|
||||
* `GoogleSearch` : Most recent results from Google Search
|
||||
* `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
|
||||
* `Instagram`: Most recent photos from an Instagram user (It is recommended to [configure](https://rss-bridge.github.io/rss-bridge/Bridge_Specific/Instagram.html) this bridge to work)
|
||||
* `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
|
||||
* `OpenClassrooms`: Lastest tutorials from [openclassrooms.com](https://openclassrooms.com/)
|
||||
* `Pinterest`: Most recent photos from user or search
|
||||
* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://secouchermoinsbete.fr/)
|
||||
* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](https://secouchermoinsbete.fr/)
|
||||
* `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
|
||||
* `Twitter` : Return keyword/hashtag search or user timeline
|
||||
* `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
|
||||
@@ -44,20 +44,18 @@ RSS-Bridge is capable of producing several output formats:
|
||||
* `Mrss` : MRSS feed, for use in feed readers
|
||||
* `Plaintext` : Raw text, for consumption by other applications
|
||||
|
||||
You can extend RSS-Bridge with your own format, using the [Format API](https://github.com/RSS-Bridge/rss-bridge/wiki/Format-API)!
|
||||
You can extend RSS-Bridge with your own format, using the [Format API](https://rss-bridge.github.io/rss-bridge/Format_API/index.html)!
|
||||
|
||||
Screenshot
|
||||
===
|
||||
|
||||
Welcome screen:
|
||||
|
||||

|
||||
|
||||
***
|
||||

|
||||
|
||||
RSS-Bridge hashtag (#rss-bridge) search on Twitter, in Atom format (as displayed by Firefox):
|
||||
|
||||

|
||||

|
||||
|
||||
Requirements
|
||||
===
|
||||
@@ -71,18 +69,19 @@ RSS-Bridge requires PHP 7.1 or higher with following extensions enabled:
|
||||
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
|
||||
- [`json`](https://secure.php.net/manual/en/book.json.php)
|
||||
- [`filter`](https://secure.php.net/manual/en/book.filter.php)
|
||||
- [`sqlite3`](http://php.net/manual/en/book.sqlite3.php) (only when using SQLiteCache)
|
||||
- [`zip`](https://secure.php.net/manual/en/book.zip.php) (for some bridges)
|
||||
- [`sqlite3`](https://www.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 [Documentation](https://rss-bridge.github.io/rss-bridge/index.html)
|
||||
|
||||
Enable / Disable bridges
|
||||
===
|
||||
|
||||
RSS-Bridge allows you to take full control over which bridges are displayed to the user. That way you can host your own RSS-Bridge service with your favorite collection of bridges!
|
||||
|
||||
Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting)
|
||||
Find more information on the [Documentation](https://rss-bridge.github.io/rss-bridge/For_Hosts/Whitelisting.html)
|
||||
|
||||
**Notice**: By default, RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge!
|
||||
**Notice**: By default, RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://rss-bridge.github.io/rss-bridge/For_Hosts/Whitelisting.html) to unlock the full potential of RSS-Bridge!
|
||||
|
||||
Deploy
|
||||
===
|
||||
@@ -104,13 +103,13 @@ There are many ways for you to getting involved with RSS-Bridge. Here are a few
|
||||
- Request new features or suggest ideas (via [Issues](https://github.com/RSS-Bridge/rss-bridge/issues))
|
||||
- Discuss bugs, features, ideas or [issues](https://github.com/RSS-Bridge/rss-bridge/issues)
|
||||
- Add new bridges or improve the API
|
||||
- Improve the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki)
|
||||
- Improve the [Documentation](https://rss-bridge.github.io/rss-bridge/)
|
||||
- Host an instance of RSS-Bridge for your personal use or make it available to the community :sparkling_heart:
|
||||
|
||||
Authors
|
||||
===
|
||||
|
||||
We are RSS-Bridge community, a group of developers continuing the project initiated by sebsauvage, webmaster of [sebsauvage.net](http://sebsauvage.net), author of [Shaarli](http://sebsauvage.net/wiki/doku.php?id=php:shaarli) and [ZeroBin](http://sebsauvage.net/wiki/doku.php?id=php:zerobin).
|
||||
We are RSS-Bridge community, a group of developers continuing the project initiated by sebsauvage, webmaster of [sebsauvage.net](https://sebsauvage.net), author of [Shaarli](https://sebsauvage.net/wiki/doku.php?id=php:shaarli) and [ZeroBin](https://sebsauvage.net/wiki/doku.php?id=php:zerobin).
|
||||
|
||||
**Contributors** (sorted alphabetically):
|
||||
<!--
|
||||
@@ -318,16 +317,18 @@ The source code for RSS-Bridge is [Public Domain](UNLICENSE).
|
||||
|
||||
RSS-Bridge uses third party libraries with their own license:
|
||||
|
||||
* [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](http://opensource.org/licenses/MIT)
|
||||
* [`PHP Simple HTML DOM Parser`](http://simplehtmldom.sourceforge.net/) licensed under the [MIT License](http://opensource.org/licenses/MIT)
|
||||
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT)
|
||||
* [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
* [`PHP Simple HTML DOM Parser`](https://simplehtmldom.sourceforge.io/docs/1.9/index.html) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
* [php polyfills](https://github.com/symfony/polyfill) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
* [`Laravel framework`](https://github.com/laravel/framework/) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
|
||||
Technical notes
|
||||
===
|
||||
|
||||
* RSS-Bridge uses caching to prevent services from banning your server for repeatedly updating feeds. The specific cache duration can be different between bridges. Cached files are deleted automatically after 24 hours.
|
||||
* You can implement your own bridge, [following these instructions](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API).
|
||||
* You can enable debug mode to disable caching. Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Debug-mode)
|
||||
* You can implement your own bridge, [following these instructions](https://rss-bridge.github.io/rss-bridge/Bridge_API/index.html).
|
||||
* You can enable debug mode to disable caching. Find more information on the [Wiki](https://rss-bridge.github.io/rss-bridge/For_Developers/Debug_mode.html)
|
||||
|
||||
Rant
|
||||
===
|
||||
|
@@ -25,7 +25,7 @@ class ConnectivityAction extends ActionAbstract {
|
||||
public function execute() {
|
||||
|
||||
if(!Debug::isEnabled()) {
|
||||
returnError('This action is only available in debug mode!');
|
||||
returnError('This action is only available in debug mode!', 400);
|
||||
}
|
||||
|
||||
if(!isset($this->userData['bridge'])) {
|
||||
@@ -55,7 +55,6 @@ class ConnectivityAction extends ActionAbstract {
|
||||
private function reportBridgeConnectivity($bridgeName) {
|
||||
|
||||
$bridgeFac = new \BridgeFactory();
|
||||
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
|
||||
|
||||
if(!$bridgeFac->isWhitelisted($bridgeName)) {
|
||||
header('Content-Type: text/html');
|
||||
@@ -84,12 +83,10 @@ class ConnectivityAction extends ActionAbstract {
|
||||
try {
|
||||
$reply = getContents($bridge::URI, array(), $curl_opts, true);
|
||||
|
||||
if($reply) {
|
||||
if($reply['code'] === 200) {
|
||||
$retVal['successful'] = true;
|
||||
if (isset($reply['header'])) {
|
||||
if (strpos($reply['header'], 'HTTP/1.1 301 Moved Permanently') !== false) {
|
||||
$retVal['http_code'] = 301;
|
||||
}
|
||||
if (strpos(implode('', $reply['status_lines']), '301 Moved Permanently')) {
|
||||
$retVal['http_code'] = 301;
|
||||
}
|
||||
}
|
||||
} catch(Exception $e) {
|
||||
|
@@ -20,7 +20,6 @@ class DetectAction extends ActionAbstract {
|
||||
or returnClientError('You must specify a format!');
|
||||
|
||||
$bridgeFac = new \BridgeFactory();
|
||||
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
|
||||
|
||||
foreach($bridgeFac->getBridgeNames() as $bridgeName) {
|
||||
|
||||
|
@@ -28,7 +28,6 @@ class DisplayAction extends ActionAbstract {
|
||||
or returnClientError('You must specify a format!');
|
||||
|
||||
$bridgeFac = new \BridgeFactory();
|
||||
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
|
||||
|
||||
// whitelist control
|
||||
if(!$bridgeFac->isWhitelisted($bridge)) {
|
||||
@@ -245,8 +244,14 @@ class DisplayAction extends ActionAbstract {
|
||||
$format = $formatFac->create($format);
|
||||
$format->setItems($items);
|
||||
$format->setExtraInfos($infos);
|
||||
$format->setLastModified($cache->getTime());
|
||||
$format->display();
|
||||
$lastModified = $cache->getTime();
|
||||
$format->setLastModified($lastModified);
|
||||
if ($lastModified) {
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT');
|
||||
}
|
||||
header('Content-Type: ' . $format->getMimeType() . '; charset=' . $format->getCharset());
|
||||
|
||||
echo $format->stringify();
|
||||
} catch(Error $e) {
|
||||
error_log($e);
|
||||
header('Content-Type: text/html', true, $e->getCode());
|
||||
|
@@ -18,7 +18,6 @@ class ListAction extends ActionAbstract {
|
||||
$list->total = 0;
|
||||
|
||||
$bridgeFac = new \BridgeFactory();
|
||||
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
|
||||
|
||||
foreach($bridgeFac->getBridgeNames() as $bridgeName) {
|
||||
|
||||
|
4
app.json
4
app.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"service": "Heroku",
|
||||
"name": "RSS-Bridge",
|
||||
"name": "rss-bridge-heroku",
|
||||
"description": "RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one.",
|
||||
"repository": "https://github.com/RSS-Bridge/rss-bridge",
|
||||
"repository": "https://github.com/RSS-Bridge/rss-bridge?1651005770",
|
||||
"keywords": ["php", "rss-bridge", "rss"]
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@ class AllocineFRBridge extends BridgeAbstract {
|
||||
const MAINTAINER = 'superbaillot.net';
|
||||
const NAME = 'Allo Cine Bridge';
|
||||
const CACHE_TIMEOUT = 25200; // 7h
|
||||
const URI = 'https://www.allocine.fr/';
|
||||
const URI = 'https://www.allocine.fr';
|
||||
const DESCRIPTION = 'Bridge for allocine.fr';
|
||||
const PARAMETERS = array( array(
|
||||
'category' => array(
|
||||
@@ -35,26 +35,26 @@ class AllocineFRBridge extends BridgeAbstract {
|
||||
if(!is_null($this->getInput('category'))) {
|
||||
|
||||
$categories = array(
|
||||
'faux-raccord' => 'video/programme-12284/saison-37054/',
|
||||
'fanzone' => 'video/programme-12298/saison-37059/',
|
||||
'game-in-cine' => 'video/programme-12288/saison-22971/',
|
||||
'pour-la-faire-courte' => 'video/programme-20960/saison-29678/',
|
||||
'home-cinema' => 'video/programme-12287/saison-34703/',
|
||||
'pils-par-ici-les-sorties' => 'video/programme-25789/saison-37253/',
|
||||
'allocine-lemission-sur-lestream' => 'video/programme-25123/saison-36067/',
|
||||
'give-me-five' => 'video/programme-21919/saison-34518/',
|
||||
'aviez-vous-remarque' => 'video/programme-19518/saison-37084/',
|
||||
'et-paf-il-est-mort' => 'video/programme-25113/saison-36657/',
|
||||
'the-big-fan-theory' => 'video/programme-20403/saison-37419/',
|
||||
'cliches' => 'video/programme-24834/saison-35591/',
|
||||
'completement' => 'video/programme-23859/saison-34102/',
|
||||
'fun-facts' => 'video/programme-23040/saison-32686/',
|
||||
'origin-story' => 'video/programme-25667/saison-37041/'
|
||||
'faux-raccord' => '/video/programme-12284/',
|
||||
'fanzone' => '/video/programme-12298/',
|
||||
'game-in-cine' => '/video/programme-12288/',
|
||||
'pour-la-faire-courte' => '/video/programme-20960/',
|
||||
'home-cinema' => '/video/programme-12287/',
|
||||
'pils-par-ici-les-sorties' => '/video/programme-25789/',
|
||||
'allocine-lemission-sur-lestream' => '/video/programme-25123/',
|
||||
'give-me-five' => '/video/programme-21919/saison-34518/',
|
||||
'aviez-vous-remarque' => '/video/programme-19518/',
|
||||
'et-paf-il-est-mort' => '/video/programme-25113/',
|
||||
'the-big-fan-theory' => '/video/programme-20403/',
|
||||
'cliches' => '/video/programme-24834/',
|
||||
'completement' => '/video/programme-23859/',
|
||||
'fun-facts' => '/video/programme-23040/',
|
||||
'origin-story' => '/video/programme-25667/'
|
||||
);
|
||||
|
||||
$category = $this->getInput('category');
|
||||
if(array_key_exists($category, $categories)) {
|
||||
return static::URI . $categories[$category];
|
||||
return static::URI . $this->getLastSeasonURI($categories[$category]);
|
||||
} else {
|
||||
returnClientError('Emission inconnue');
|
||||
}
|
||||
@@ -63,6 +63,14 @@ class AllocineFRBridge extends BridgeAbstract {
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
private function getLastSeasonURI($category)
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached(static::URI . $category, 86400);
|
||||
$seasonLink = $html->find('section[class=section-wrap section]', 0)->find('div[class=cf]', 0)->find('a', 0);
|
||||
$URI = $seasonLink->href;
|
||||
return $URI;
|
||||
}
|
||||
|
||||
public function getName(){
|
||||
if(!is_null($this->getInput('category'))) {
|
||||
return self::NAME . ' : '
|
||||
@@ -83,12 +91,11 @@ class AllocineFRBridge extends BridgeAbstract {
|
||||
$this->getInput('category'),
|
||||
self::PARAMETERS[$this->queriedContext]['category']['values']
|
||||
);
|
||||
|
||||
foreach($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
|
||||
$item = array();
|
||||
|
||||
$title = $element->find('a[class*=meta-title-link]', 0);
|
||||
$content = trim($element->outertext);
|
||||
$content = trim(defaultLinkTo($element->outertext, static::URI));
|
||||
|
||||
// Replace image 'src' with the one in 'data-src'
|
||||
$content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content);
|
||||
@@ -99,7 +106,7 @@ class AllocineFRBridge extends BridgeAbstract {
|
||||
|
||||
$item['content'] = $content;
|
||||
$item['title'] = trim($title->innertext);
|
||||
$item['uri'] = static::URI . substr($title->href, 1);
|
||||
$item['uri'] = static::URI . '/' . substr($title->href, 1);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
@@ -42,6 +42,8 @@ class AmazonBridge extends BridgeAbstract {
|
||||
'Mexico' => 'com.mx',
|
||||
'Netherlands' => 'nl',
|
||||
'Spain' => 'es',
|
||||
'Sweden' => 'se',
|
||||
'Turkey' => 'com.tr',
|
||||
'United Kingdom' => 'co.uk',
|
||||
'United States' => 'com',
|
||||
),
|
||||
@@ -49,6 +51,48 @@ class AmazonBridge extends BridgeAbstract {
|
||||
),
|
||||
));
|
||||
|
||||
public function collectData() {
|
||||
|
||||
$baseUrl = sprintf('https://www.amazon.%s', $this->getInput('tld'));
|
||||
|
||||
$url = sprintf(
|
||||
'%s/s/?field-keywords=%s&sort=%s',
|
||||
$baseUrl,
|
||||
urlencode($this->getInput('q')),
|
||||
$this->getInput('sort')
|
||||
);
|
||||
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
|
||||
$elements = $dom->find('div.s-result-item');
|
||||
|
||||
foreach($elements as $element) {
|
||||
$item = [];
|
||||
|
||||
$title = $element->find('h2', 0);
|
||||
if (!$title) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item['title'] = $title->innertext;
|
||||
|
||||
$itemUrl = $element->find('a', 0)->href;
|
||||
$item['uri'] = urljoin($baseUrl, $itemUrl);
|
||||
|
||||
$image = $element->find('img', 0);
|
||||
if ($image) {
|
||||
$item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />';
|
||||
}
|
||||
|
||||
$price = $element->find('span.a-price > .a-offscreen', 0);
|
||||
if ($price) {
|
||||
$item['content'] .= $price->innertext;
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(){
|
||||
if(!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
|
||||
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q');
|
||||
@@ -56,40 +100,4 @@ class AmazonBridge extends BridgeAbstract {
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
|
||||
$uri = 'https://www.amazon.' . $this->getInput('tld') . '/';
|
||||
$uri .= 's/?field-keywords=' . urlencode($this->getInput('q')) . '&sort=' . $this->getInput('sort');
|
||||
|
||||
$html = getSimpleHTMLDOM($uri);
|
||||
|
||||
foreach($html->find('li.s-result-item') as $element) {
|
||||
|
||||
$item = array();
|
||||
|
||||
// Title
|
||||
$title = $element->find('h2', 0);
|
||||
if (is_null($title)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item['title'] = html_entity_decode($title->innertext, ENT_QUOTES);
|
||||
|
||||
// Url
|
||||
$uri = $title->parent()->getAttribute('href');
|
||||
$uri = substr($uri, 0, strrpos($uri, '/'));
|
||||
|
||||
$item['uri'] = substr($uri, 0, strrpos($uri, '/'));
|
||||
|
||||
// Content
|
||||
$image = $element->find('img', 0);
|
||||
$price = $element->find('span.s-price', 0);
|
||||
$price = ($price) ? $price->innertext : '';
|
||||
|
||||
$item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />' . $price;
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ class AmazonPriceTrackerBridge extends BridgeAbstract {
|
||||
'name' => 'Country',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'Australia' => 'com.au',
|
||||
'Australia' => 'com.au',
|
||||
'Brazil' => 'com.br',
|
||||
'Canada' => 'ca',
|
||||
'China' => 'cn',
|
||||
@@ -30,9 +30,10 @@ class AmazonPriceTrackerBridge extends BridgeAbstract {
|
||||
'Italy' => 'it',
|
||||
'Japan' => 'co.jp',
|
||||
'Mexico' => 'com.mx',
|
||||
'Netherlands' => 'nl',
|
||||
'Netherlands' => 'nl',
|
||||
'Spain' => 'es',
|
||||
'Sweden' => 'se',
|
||||
'Turkey' => 'com.tr',
|
||||
'United Kingdom' => 'co.uk',
|
||||
'United States' => 'com',
|
||||
),
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
class Arte7Bridge extends BridgeAbstract {
|
||||
|
||||
// const MAINTAINER = 'mitsukarenai';
|
||||
const NAME = 'Arte +7';
|
||||
const URI = 'https://www.arte.tv/';
|
||||
const MAINTAINER = 'imagoiq';
|
||||
const CACHE_TIMEOUT = 1800; // 30min
|
||||
const DESCRIPTION = 'Returns newest videos from ARTE +7';
|
||||
|
||||
@@ -11,11 +11,39 @@ class Arte7Bridge extends BridgeAbstract {
|
||||
|
||||
const PARAMETERS = array(
|
||||
'global' => [
|
||||
'video_duration_filter' => [
|
||||
'name' => 'Exclude short videos',
|
||||
'sort_by' => array(
|
||||
'type' => 'list',
|
||||
'name' => 'Sort by',
|
||||
'required' => false,
|
||||
'defaultValue' => null,
|
||||
'values' => array(
|
||||
'Default' => null,
|
||||
'Video rights start date' => 'videoRightsBegin',
|
||||
'Video rights end date' => 'videoRightsEnd',
|
||||
'Brodcast date' => 'broadcastBegin',
|
||||
'Creation date' => 'creationDate',
|
||||
'Last modified' => 'lastModified',
|
||||
'Number of views' => 'views',
|
||||
'Number of views per period' => 'viewsPeriod',
|
||||
'Available screens' => 'availableScreens',
|
||||
'Episode' => 'episode'
|
||||
),
|
||||
),
|
||||
'sort_direction' => array(
|
||||
'type' => 'list',
|
||||
'name' => 'Sort direction',
|
||||
'required' => false,
|
||||
'defaultValue' => 'DESC',
|
||||
'values' => array(
|
||||
'Ascending' => 'ASC',
|
||||
'Descending' => 'DESC'
|
||||
),
|
||||
),
|
||||
'exclude_trailers' => [
|
||||
'name' => 'Exclude trailers',
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Exclude videos that are shorter than 3 minutes',
|
||||
'defaultValue' => false,
|
||||
'required' => false,
|
||||
'defaultValue' => false
|
||||
],
|
||||
],
|
||||
'Category' => array(
|
||||
@@ -30,8 +58,6 @@ class Arte7Bridge extends BridgeAbstract {
|
||||
'Polski' => 'pl',
|
||||
'Italiano' => 'it'
|
||||
),
|
||||
'title' => 'ex. RC-014095 pour https://www.arte.tv/fr/videos/RC-014095/blow-up/',
|
||||
'exampleValue' => 'RC-014095'
|
||||
),
|
||||
'cat' => array(
|
||||
'type' => 'list',
|
||||
@@ -73,7 +99,6 @@ class Arte7Bridge extends BridgeAbstract {
|
||||
);
|
||||
|
||||
public function collectData(){
|
||||
$lang = $this->getInput('lang');
|
||||
switch($this->queriedContext) {
|
||||
case 'Category':
|
||||
$category = $this->getInput('cat');
|
||||
@@ -85,8 +110,13 @@ class Arte7Bridge extends BridgeAbstract {
|
||||
break;
|
||||
}
|
||||
|
||||
$url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=15&language='
|
||||
$lang = $this->getInput('lang');
|
||||
$sort_by = $this->getInput('sort_by');
|
||||
$sort_direction = $this->getInput('sort_direction') == 'ASC' ? '' : '-';
|
||||
|
||||
$url = 'https://api.arte.tv/api/opa/v3/videos?limit=15&language='
|
||||
. $lang
|
||||
. ($sort_by != null ? '&sort=' . $sort_direction . $sort_by : '')
|
||||
. ($category != null ? '&category.code=' . $category : '')
|
||||
. ($collectionId != null ? '&collections.collectionId=' . $collectionId : '');
|
||||
|
||||
@@ -98,12 +128,12 @@ class Arte7Bridge extends BridgeAbstract {
|
||||
$input_json = json_decode($input, true);
|
||||
|
||||
foreach($input_json['videos'] as $element) {
|
||||
$durationSeconds = $element['durationSeconds'];
|
||||
|
||||
if ($this->getInput('video_duration_filter') && $durationSeconds < 60 * 3) {
|
||||
if($this->getInput('exclude_trailers') && $element['platform'] == 'EXTRAIT') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$durationSeconds = $element['durationSeconds'];
|
||||
|
||||
$item = array();
|
||||
$item['uri'] = $element['url'];
|
||||
$item['id'] = $element['id'];
|
||||
|
@@ -20,9 +20,10 @@ class AsahiShimbunAJWBridge extends BridgeAbstract {
|
||||
'Culture » Style' => 'culture/style',
|
||||
'Culture » Movies' => 'culture/movies',
|
||||
'Culture » Manga & Anime' => 'culture/manga_anime',
|
||||
'Asia » China' => 'asia/china',
|
||||
'Asia » Korean Peninsula' => 'asia/korean_peninsula',
|
||||
'Asia » Around Asia' => 'asia/around_asia',
|
||||
'Asia » China' => 'asia_world/china',
|
||||
'Asia » Korean Peninsula' => 'asia_world/korean_peninsula',
|
||||
'Asia » Around Asia' => 'asia_world/around_asia',
|
||||
'Asia » World' => 'asia_world/world',
|
||||
'Opinion » Editorial' => 'opinion/editorial',
|
||||
'Opinion » Vox Populi' => 'opinion/vox',
|
||||
),
|
||||
|
@@ -7,7 +7,7 @@ class BandcampDailyBridge extends BridgeAbstract {
|
||||
const PARAMETERS = array(
|
||||
'Latest articles' => array(),
|
||||
'Best of' => array(
|
||||
'content' => array(
|
||||
'best-content' => array(
|
||||
'name' => 'content',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
@@ -28,7 +28,7 @@ class BandcampDailyBridge extends BridgeAbstract {
|
||||
),
|
||||
),
|
||||
'Genres' => array(
|
||||
'content' => array(
|
||||
'genres-content' => array(
|
||||
'name' => 'content',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
@@ -62,7 +62,7 @@ class BandcampDailyBridge extends BridgeAbstract {
|
||||
),
|
||||
),
|
||||
'Franchises' => array(
|
||||
'content' => array(
|
||||
'franchises-content' => array(
|
||||
'name' => 'content',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
@@ -133,23 +133,28 @@ class BandcampDailyBridge extends BridgeAbstract {
|
||||
case 'Best of':
|
||||
case 'Genres':
|
||||
case 'Franchises':
|
||||
return self::URI . '/' . $this->getInput('content');
|
||||
// TODO Switch to array_key_first once php >= 7.3
|
||||
$contentKey = key(self::PARAMETERS[$this->queriedContext]);
|
||||
return self::URI . '/' . $this->getInput($contentKey);
|
||||
default:
|
||||
return parent::getURI();
|
||||
}
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
if ($this->queriedContext === 'Latest articles') {
|
||||
return $this->queriedContext . ' - Bandcamp Daily';
|
||||
switch($this->queriedContext) {
|
||||
case 'Latest articles':
|
||||
return $this->queriedContext . ' - Bandcamp Daily';
|
||||
case 'Best of':
|
||||
case 'Genres':
|
||||
case 'Franchises':
|
||||
// TODO Switch to array_key_first once php >= 7.3
|
||||
$contentKey = key(self::PARAMETERS[$this->queriedContext]);
|
||||
$contentValues = array_flip(self::PARAMETERS[$this->queriedContext][$contentKey]['values']);
|
||||
|
||||
return $contentValues[$this->getInput($contentKey)] . ' - Bandcamp Daily';
|
||||
default:
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
if (!is_null($this->getInput('content'))) {
|
||||
$contentValues = array_flip(self::PARAMETERS[$this->queriedContext]['content']['values']);
|
||||
|
||||
return $contentValues[$this->getInput('content')] . ' - Bandcamp Daily';
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
|
1458
bridges/BookMyShowBridge.php
Normal file
1458
bridges/BookMyShowBridge.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('DanbooruBridge.php');
|
||||
|
||||
class BooruprojectBridge extends DanbooruBridge {
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
class BrutBridge extends BridgeAbstract {
|
||||
const NAME = 'Brut Bridge';
|
||||
const URI = 'https://www.brut.media';
|
||||
const DESCRIPTION = 'Returns 5 newest videos by category and edition';
|
||||
const DESCRIPTION = 'Returns 10 newest videos by category and edition';
|
||||
const MAINTAINER = 'VerifiedJoseph';
|
||||
const PARAMETERS = array(array(
|
||||
'category' => array(
|
||||
@@ -38,9 +38,7 @@ class BrutBridge extends BridgeAbstract {
|
||||
|
||||
const CACHE_TIMEOUT = 1800; // 30 mins
|
||||
|
||||
private $videoId = '';
|
||||
private $videoType = '';
|
||||
private $videoImage = '';
|
||||
private $jsonRegex = '/window\.__PRELOADED_STATE__ = ((?:.*)});/';
|
||||
|
||||
public function collectData() {
|
||||
|
||||
@@ -48,36 +46,38 @@ class BrutBridge extends BridgeAbstract {
|
||||
|
||||
$results = $html->find('div.results', 0);
|
||||
|
||||
foreach($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $index => $li) {
|
||||
foreach($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $li) {
|
||||
$item = array();
|
||||
|
||||
$videoPath = self::URI . $li->children(0)->href;
|
||||
|
||||
$videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600);
|
||||
|
||||
$this->videoImage = $videoPageHtml->find('meta[name="twitter:image"]', 0)->content;
|
||||
|
||||
$this->processTwitterImage();
|
||||
|
||||
$description = $videoPageHtml->find('div.description', 0);
|
||||
$json = $this->extractJson($videoPageHtml);
|
||||
$id = array_keys((array) $json->media->index)[0];
|
||||
|
||||
$item['uri'] = $videoPath;
|
||||
$item['title'] = $description->find('h1', 0)->plaintext;
|
||||
$item['title'] = $json->media->index->$id->title;
|
||||
$item['timestamp'] = $json->media->index->$id->published_at;
|
||||
$item['enclosures'][] = $json->media->index->$id->media->thumbnail;
|
||||
|
||||
if ($description->find('div.date', 0)->children(0)) {
|
||||
$description->find('div.date', 0)->children(0)->outertext = '';
|
||||
$description = $json->media->index->$id->description;
|
||||
$article = '';
|
||||
|
||||
if (is_null($json->media->index->$id->media->seo_article) === false) {
|
||||
$article = markdownToHtml($json->media->index->$id->media->seo_article);
|
||||
}
|
||||
|
||||
$item['content'] = $this->processContent(
|
||||
$description
|
||||
);
|
||||
|
||||
$item['timestamp'] = $this->processDate($description);
|
||||
$item['enclosures'][] = $this->videoImage;
|
||||
$item['content'] = <<<EOD
|
||||
<video controls poster="{$json->media->index->$id->media->thumbnail}" preload="none">
|
||||
<source src="{$json->media->index->$id->media->mp4_url}" type="video/mp4">
|
||||
</video>
|
||||
<p>{$description}</p>
|
||||
{$article}
|
||||
EOD;
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
if (count($this->items) >= 5) {
|
||||
if (count($this->items) >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -107,51 +107,21 @@ class BrutBridge extends BridgeAbstract {
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
private function processDate($description) {
|
||||
/**
|
||||
* Extract JSON from page
|
||||
*/
|
||||
private function extractJson($html) {
|
||||
|
||||
if ($this->getInput('edition') === 'uk') {
|
||||
$date = DateTime::createFromFormat('d/m/Y H:i', $description->find('div.date', 0)->innertext);
|
||||
return strtotime($date->format('Y-m-d H:i:s'));
|
||||
if (!preg_match($this->jsonRegex, $html, $parts)) {
|
||||
returnServerError('Failed to extract data from page');
|
||||
}
|
||||
|
||||
return strtotime($description->find('div.date', 0)->innertext);
|
||||
}
|
||||
$data = json_decode($parts[1]);
|
||||
|
||||
private function processContent($description) {
|
||||
|
||||
$content = '<video controls poster="' . $this->videoImage . '" preload="none">
|
||||
<source src="https://content.brut.media/video/' . $this->videoId . '-' . $this->videoType . '-web.mp4"
|
||||
type="video/mp4">
|
||||
</video>';
|
||||
$content .= '<p>' . $description->find('h2.mb-1', 0)->innertext . '</p>';
|
||||
|
||||
if ($description->find('div.text.pb-3', 0)->children(1)->class != 'date') {
|
||||
$content .= '<p>' . $description->find('div.text.pb-3', 0)->children(1)->innertext . '</p>';
|
||||
if ($data === false) {
|
||||
returnServerError('Failed to decode extracted data');
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function processTwitterImage() {
|
||||
/**
|
||||
* Extract video ID + type from twitter image
|
||||
*
|
||||
* Example (wrapped):
|
||||
* https://img.brut.media/thumbnail/
|
||||
* the-life-of-rita-moreno-2cce75b5-d448-44d2-a97c-ca50d6470dd4-square.jpg
|
||||
* ?ts=1559337892
|
||||
*/
|
||||
$fpath = parse_url($this->videoImage, PHP_URL_PATH);
|
||||
$fname = basename($fpath);
|
||||
$fname = substr($fname, 0, strrpos($fname, '.'));
|
||||
$parts = explode('-', $fname);
|
||||
|
||||
if (end($parts) === 'auto') {
|
||||
$key = array_search('auto', $parts);
|
||||
unset($parts[$key]);
|
||||
}
|
||||
|
||||
$this->videoId = implode('-', array_splice($parts, -6, 5));
|
||||
$this->videoType = end($parts);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ class CastorusBridge extends BridgeAbstract {
|
||||
if(!$title)
|
||||
returnServerError('Cannot find title!');
|
||||
|
||||
return htmlspecialchars(trim($title->plaintext));
|
||||
return trim($title->plaintext);
|
||||
}
|
||||
|
||||
// Extracts the url from an actitiy
|
||||
|
98
bridges/CubariBridge.php
Normal file
98
bridges/CubariBridge.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
class CubariBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Cubari';
|
||||
const URI = 'https://cubari.moe';
|
||||
const DESCRIPTION = 'Parses given cubari-formatted JSON file for updates.';
|
||||
const MAINTAINER = 'KamaleiZestri';
|
||||
const PARAMETERS = array(array(
|
||||
'gist' => array(
|
||||
'name' => 'Gist/Raw Url',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'exampleValue' => 'https://raw.githubusercontent.com/kurisumx/baka/main/ikedan'
|
||||
)
|
||||
));
|
||||
|
||||
private $mangaTitle = '';
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!empty($this->mangaTitle))
|
||||
return $this->mangaTitle . ' - ' . self::NAME;
|
||||
else
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if ($this->getInput('gist') != '')
|
||||
return self::URI . '/read/gist/' . $this->getEncodedGist();
|
||||
else
|
||||
return self::URI;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Cubari bridge.
|
||||
*
|
||||
* Cubari urls are base64 encodes of a given github raw or gist link described as below:
|
||||
* https://cubari.moe/read/gist/${bаse64.url_encode(raw/<rest of the url...>)}/
|
||||
* https://cubari.moe/read/gist/${bаse64.url_encode(gist/<rest of the url...>)}/
|
||||
* https://cubari.moe/read/gist/${gitio shortcode}
|
||||
*
|
||||
* This bridge uses just the raw/gist and generates matching cubari urls.
|
||||
*/
|
||||
public function collectData()
|
||||
{
|
||||
$jsonSite = getContents($this->getInput('gist'));
|
||||
$jsonFile = json_decode($jsonSite, true);
|
||||
|
||||
$this->mangaTitle = $jsonFile['title'];
|
||||
|
||||
$chapters = $jsonFile['chapters'];
|
||||
|
||||
foreach ($chapters as $chapnum => $chapter) {
|
||||
$item = $this->getItemFromChapter($chapnum, $chapter);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
array_multisort(array_column($this->items, 'timestamp'), SORT_DESC, $this->items);
|
||||
}
|
||||
|
||||
protected function getEncodedGist()
|
||||
{
|
||||
$url = $this->getInput('gist');
|
||||
|
||||
preg_match('/\/([a-z]*)\.githubusercontent.com(.*)/', $url, $matches);
|
||||
|
||||
// raw or gist is first match.
|
||||
$unencoded = $matches[1] . $matches[2];
|
||||
|
||||
return base64_encode($unencoded);
|
||||
}
|
||||
|
||||
private function getSanitizedHash($string)
|
||||
{
|
||||
return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
|
||||
}
|
||||
|
||||
protected function getItemFromChapter($chapnum, $chapter)
|
||||
{
|
||||
$item = array();
|
||||
|
||||
$item['uri'] = $this->getURI() . '/' . $chapnum;
|
||||
$item['title'] = 'Chapter ' . $chapnum . ' - ' . $chapter['title'] . ' - ' . $this->mangaTitle;
|
||||
foreach ($chapter['groups'] as $key => $value)
|
||||
$item['author'] = $key;
|
||||
$item['timestamp'] = $chapter['last_updated'];
|
||||
|
||||
$item['content'] = '<p>Manga: <a href=' . $this->getURI() . '>' . $this->mangaTitle . '</a> </p>
|
||||
<p>Chapter Number: ' . $chapnum . '</p>
|
||||
<p>Chapter Title: <a href=' . $item['uri'] . '>' . $chapter['title'] . '</a></p>
|
||||
<p>Group: ' . $item['author'] . '</p>';
|
||||
|
||||
$item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
37
bridges/CyanideAndHappinessBridge.php
Normal file
37
bridges/CyanideAndHappinessBridge.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
class CyanideAndHappinessBridge extends BridgeAbstract {
|
||||
const NAME = 'Cyanide & Happiness';
|
||||
const URI = 'https://explosm.net/';
|
||||
const DESCRIPTION = 'The Webcomic from Explosm.';
|
||||
const MAINTAINER = 'sal0max';
|
||||
const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours
|
||||
|
||||
public function getIcon() {
|
||||
return self::URI . 'favicon-32x32.png';
|
||||
}
|
||||
|
||||
public function getURI(){
|
||||
return self::URI . 'comics/latest#comic';
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$html = getSimpleHTMLDOM($this->getUri());
|
||||
|
||||
foreach ($html->find('[class*=ComicImage]') as $element) {
|
||||
$date = $element->find('[class^=Author__Right] p', 0)->plaintext;
|
||||
$author = str_replace('by ', '', $element->find('[class^=Author__Right] p', 1)->plaintext);
|
||||
$image = $element->find('img', 0)->src;
|
||||
$link = $html->find('[rel=canonical]', 0)->href;
|
||||
|
||||
$item = array(
|
||||
'uid' => $link,
|
||||
'author' => $author,
|
||||
'title' => $date,
|
||||
'uri' => $link . '#comic',
|
||||
'timestamp' => str_replace('.', '-', $date) . 'T00:00:00Z',
|
||||
'content' => "<img src=\"$image\" />"
|
||||
);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,7 +15,9 @@ class DanbooruBridge extends BridgeAbstract {
|
||||
'type' => 'number'
|
||||
),
|
||||
't' => array(
|
||||
'name' => 'tags'
|
||||
'type' => 'text',
|
||||
'name' => 'tags',
|
||||
'exampleValue' => 'cosplay',
|
||||
)
|
||||
),
|
||||
0 => array()
|
||||
|
@@ -33,7 +33,8 @@ class DarkReadingBridge extends FeedExpander {
|
||||
'Insider Threats' => '663_Insider%20Threats',
|
||||
'Vulnerability Management' => '664_Vulnerability%20Management',
|
||||
)
|
||||
)
|
||||
),
|
||||
'limit' => self::LIMIT,
|
||||
));
|
||||
|
||||
public function collectData(){
|
||||
@@ -48,7 +49,8 @@ class DarkReadingBridge extends FeedExpander {
|
||||
if ($feed_id != '000') {
|
||||
$feed_url .= '?f_n=' . $feed_id . '&f_ln=' . $feed_name;
|
||||
}
|
||||
$this->collectExpandableDatas($feed_url, 20);
|
||||
$limit = $this->getInput('limit') ?? 10;
|
||||
$this->collectExpandableDatas($feed_url, $limit);
|
||||
}
|
||||
|
||||
protected function parseItem($newsItem){
|
||||
|
@@ -10,7 +10,7 @@ class DealabsBridge extends PepperBridgeAbstract {
|
||||
'q' => array(
|
||||
'name' => 'Mot(s) clé(s)',
|
||||
'type' => 'text',
|
||||
'exampleValue' => 'lamp',
|
||||
'exampleValue' => 'lampe',
|
||||
'required' => true
|
||||
),
|
||||
'hide_expired' => array(
|
||||
@@ -1886,7 +1886,7 @@ class DealabsBridge extends PepperBridgeAbstract {
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'title' => 'URL discussion à surveiller: https://www.dealabs.com/discussions/titre-1234',
|
||||
'exampleValue' => 'https://www.dealabs.com/discussions/titre-1234',
|
||||
'exampleValue' => 'https://www.dealabs.com/discussions/jeux-steam-gratuits-gleam-woobox-etc-1071415',
|
||||
),
|
||||
|
||||
'only_with_url' => array(
|
||||
@@ -1963,682 +1963,3 @@ class DealabsBridge extends PepperBridgeAbstract {
|
||||
|
||||
|
||||
}
|
||||
|
||||
class PepperBridgeAbstract extends BridgeAbstract {
|
||||
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
|
||||
public function collectData(){
|
||||
switch($this->queriedContext) {
|
||||
case $this->i8n('context-keyword'):
|
||||
return $this->collectDataKeywords();
|
||||
break;
|
||||
case $this->i8n('context-group'):
|
||||
return $this->collectDataGroup();
|
||||
break;
|
||||
case $this->i8n('context-talk'):
|
||||
return $this->collectDataTalk();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Deal data from the choosen group in the choosed order
|
||||
*/
|
||||
protected function collectDataGroup()
|
||||
{
|
||||
$url = $this->getGroupURI();
|
||||
$this->collectDeals($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Deal data from the choosen keywords and parameters
|
||||
*/
|
||||
protected function collectDataKeywords()
|
||||
{
|
||||
/* Even if the original website uses POST with the search page, GET works too */
|
||||
$url = $this->getSearchURI();
|
||||
$this->collectDeals($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Deal data using the given URL
|
||||
*/
|
||||
protected function collectDeals($url){
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
$list = $html->find('article[id]');
|
||||
|
||||
// Deal Image Link CSS Selector
|
||||
$selectorImageLink = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'cept-thread-image-link',
|
||||
'imgFrame',
|
||||
'imgFrame--noBorder',
|
||||
'thread-listImgCell',
|
||||
)
|
||||
);
|
||||
|
||||
// Deal Link CSS Selector
|
||||
$selectorLink = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'cept-tt',
|
||||
'thread-link',
|
||||
'linkPlain',
|
||||
)
|
||||
);
|
||||
|
||||
// Deal Hotness CSS Selector
|
||||
$selectorHot = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'cept-vote-box',
|
||||
'vote-box'
|
||||
)
|
||||
);
|
||||
|
||||
// Deal Description CSS Selector
|
||||
$selectorDescription = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'cept-description-container',
|
||||
'overflow--wrap-break'
|
||||
)
|
||||
);
|
||||
|
||||
// Deal Date CSS Selector
|
||||
$selectorDate = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'size--all-s',
|
||||
'flex',
|
||||
'boxAlign-jc--all-fe'
|
||||
)
|
||||
);
|
||||
|
||||
// If there is no results, we don't parse the content because it display some random deals
|
||||
$noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
|
||||
if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
|
||||
$this->items = array();
|
||||
} else {
|
||||
foreach ($list as $deal) {
|
||||
$item = array();
|
||||
$item['uri'] = $this->getDealURI($deal);
|
||||
$item['title'] = $this->GetTitle($deal);
|
||||
$item['author'] = $deal->find('span.thread-username', 0)->plaintext;
|
||||
|
||||
$item['content'] = '<table><tr><td><a href="'
|
||||
. $item['uri']
|
||||
. '"><img src="'
|
||||
. $this->getImage($deal)
|
||||
. '"/></td><td>'
|
||||
. $this->getHTMLTitle($item)
|
||||
. $this->getPrice($deal)
|
||||
. $this->getDiscount($deal)
|
||||
. $this->getShipsFrom($deal)
|
||||
. $this->getShippingCost($deal)
|
||||
. $this->GetSource($deal)
|
||||
. $deal->find('div[class*=' . $selectorDescription . ']', 0)->innertext
|
||||
. '</td><td>'
|
||||
. $deal->find('div[class*=' . $selectorHot . ']', 0)
|
||||
->find('span', 1)->outertext
|
||||
. '</td></table>';
|
||||
$dealDateDiv = $deal->find('div[class*=' . $selectorDate . ']', 0)
|
||||
->find('span[class=hide--toW3]');
|
||||
$itemDate = end($dealDateDiv)->plaintext;
|
||||
// In case of a Local deal, there is no date, but we can use
|
||||
// this case for other reason (like date not in the last field)
|
||||
if ($this->contains($itemDate, $this->i8n('localdeal'))) {
|
||||
$item['timestamp'] = time();
|
||||
} else if ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) {
|
||||
$item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
|
||||
} else {
|
||||
$item['timestamp'] = $this->parseDate($itemDate);
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Talk lastest comments
|
||||
*/
|
||||
protected function collectDataTalk(){
|
||||
$threadURL = $this->getInput('url');
|
||||
$onlyWithUrl = $this->getInput('only_with_url');
|
||||
|
||||
// Get Thread ID from url passed in parameter
|
||||
$threadSearch = preg_match('/-([0-9]{1,20})$/', $threadURL, $matches);
|
||||
|
||||
// Show an error message if we can't find the thread ID in the URL sent by the user
|
||||
if($threadSearch !== 1) {
|
||||
returnClientError($this->i8n('thread-error'));
|
||||
}
|
||||
$threadID = $matches[1];
|
||||
|
||||
$url = $this->i8n('bridge-uri') . 'graphql';
|
||||
|
||||
// Get Cookies header to do the query
|
||||
$cookies = $this->getCookies($url);
|
||||
|
||||
// GraphQL String
|
||||
// This was extracted from https://www.dealabs.com/assets/js/modern/common_211b99.js
|
||||
// This string was extracted during a Website visit, and minified using this neat tool :
|
||||
// https://codepen.io/dangodev/pen/Baoqmoy
|
||||
$graphqlString = <<<'HEREDOC'
|
||||
query comments($filter:CommentFilter!,$limit:Int,$page:Int){comments(filter:$filter,limit:$limit,page:$page){
|
||||
items{...commentFields}pagination{...paginationFields}}}fragment commentFields on Comment{commentId threadId url
|
||||
preparedHtmlContent user{...userMediumAvatarFields...userNameFields...userPersonaFields bestBadge{...badgeFields}}
|
||||
reactionCounts{type count}deletable currentUserReaction{type}reported reportable source status createdAt updatedAt
|
||||
ignored popular deletedBy{username}notes{content createdAt user{username}}lastEdit{reason timeAgo userId}}fragment
|
||||
userMediumAvatarFields on User{userId isDeletedOrPendingDeletion imageUrls(slot:"default",variations:
|
||||
["user_small_avatar"])}fragment userNameFields on User{userId username isUserProfileHidden isDeletedOrPendingDeletion}
|
||||
fragment userPersonaFields on User{persona{type text}}fragment badgeFields on Badge{badgeId level{...badgeLevelFields}}
|
||||
fragment badgeLevelFields on BadgeLevel{key name description}fragment paginationFields on Pagination{count current last
|
||||
next previous size order}
|
||||
HEREDOC;
|
||||
|
||||
// Construct the JSON object to send to the Website
|
||||
$queryArray = array (
|
||||
'query' => $graphqlString,
|
||||
'variables' => array (
|
||||
'filter' => array (
|
||||
'threadId' => array (
|
||||
'eq' => $threadID,
|
||||
),
|
||||
'order' => array (
|
||||
'direction' => 'Descending',
|
||||
),
|
||||
|
||||
),
|
||||
'page' => 1,
|
||||
),
|
||||
);
|
||||
$queryJSON = json_encode($queryArray);
|
||||
|
||||
// HTTP headers
|
||||
$header = array(
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json, text/plain, */*',
|
||||
'X-Pepper-Txn: threads.show',
|
||||
'X-Request-Type: application/vnd.pepper.v1+json',
|
||||
'X-Requested-With: XMLHttpRequest',
|
||||
$cookies,
|
||||
);
|
||||
// CURL Options
|
||||
$opts = array(
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => $queryJSON
|
||||
);
|
||||
$json = getContents($url, $header, $opts);
|
||||
$objects = json_decode($json);
|
||||
foreach($objects->data->comments->items as $comment) {
|
||||
$item = array();
|
||||
$item['uri'] = $comment->url;
|
||||
$item['title'] = $comment->user->username . ' - ' . $comment->createdAt;
|
||||
$item['author'] = $comment->user->username;
|
||||
$item['content'] = $comment->preparedHtmlContent;
|
||||
$item['uid'] = $comment->commentId;
|
||||
// Timestamp handling needs a new parsing function
|
||||
if($onlyWithUrl == true) {
|
||||
// Count Links and Quote Links
|
||||
$content = str_get_html($item['content']);
|
||||
$countLinks = count($content->find('a[href]'));
|
||||
$countQuoteLinks = count($content->find('a[href][class=userHtml-quote-source]'));
|
||||
// Only add element if there are Links ans more links tant Quote links
|
||||
if($countLinks > 0 && $countLinks > $countQuoteLinks) {
|
||||
$this->items[] = $item;
|
||||
}
|
||||
} else {
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the cookies obtained from the URL
|
||||
* @return array the array containing the cookies set by the URL
|
||||
*/
|
||||
private function getCookies($url)
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
// get headers too with this line
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
$result = curl_exec($ch);
|
||||
// get cookie
|
||||
// multi-cookie variant contributed by @Combuster in comments
|
||||
preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $matches);
|
||||
$cookies = array();
|
||||
foreach($matches[1] as $item) {
|
||||
parse_str($item, $cookie);
|
||||
$cookies = array_merge($cookies, $cookie);
|
||||
}
|
||||
$header = 'Cookie: ';
|
||||
foreach($cookies as $name => $content) {
|
||||
$header .= $name . '=' . $content . '; ';
|
||||
}
|
||||
return $header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the string $str contains any of the string of the array $arr
|
||||
* @return boolean true if the string matched anything otherwise false
|
||||
*/
|
||||
private function contains($str, array $arr)
|
||||
{
|
||||
foreach ($arr as $a) {
|
||||
if (stripos($str, $a) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Price from a Deal if it exists
|
||||
* @return string String of the deal price
|
||||
*/
|
||||
private function getPrice($deal)
|
||||
{
|
||||
if ($deal->find(
|
||||
'span[class*=thread-price]', 0) != null) {
|
||||
return '<div>' . $this->i8n('price') . ' : '
|
||||
. $deal->find(
|
||||
'span[class*=thread-price]', 0
|
||||
)->plaintext
|
||||
. '</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Title from a Deal if it exists
|
||||
* @return string String of the deal title
|
||||
*/
|
||||
private function getTitle($deal)
|
||||
{
|
||||
|
||||
$titleRoot = $deal->find('div[class*=threadGrid-title]', 0);
|
||||
$titleA = $titleRoot->find('a[class*=thread-link]', 0);
|
||||
$titleFirstChild = $titleRoot->first_child();
|
||||
if($titleA !== null) {
|
||||
$title = $titleA->plaintext;
|
||||
} else {
|
||||
// Inb ssome case, expired deals have a different format
|
||||
$title = $titleRoot->find('span', 0)->plaintext;
|
||||
}
|
||||
|
||||
return $title;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Title from a Talk if it exists
|
||||
* @return string String of the Talk title
|
||||
*/
|
||||
private function getTalkTitle()
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached($this->getInput('url'));
|
||||
$title = $html->find('h1[class=thread-title]', 0)->plaintext;
|
||||
return $title;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML Title code from an item
|
||||
* @return string String of the deal title
|
||||
*/
|
||||
private function getHTMLTitle($item)
|
||||
{
|
||||
if($item['uri'] == '') {
|
||||
$html = '<h2>' . $item['title'] . '</h2>';
|
||||
} else {
|
||||
$html = '<h2><a href="' . $item['uri'] . '">'
|
||||
. $item['title'] . '</a></h2>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URI from a Deal if it exists
|
||||
* @return string String of the deal URI
|
||||
*/
|
||||
private function getDealURI($deal)
|
||||
{
|
||||
|
||||
$uriA = $deal->find('div[class*=threadGrid-title]', 0)->find('a[class*=thread-link]', 0);
|
||||
if($uriA === null) {
|
||||
$uri = '';
|
||||
} else {
|
||||
$uri = $uriA->href;
|
||||
}
|
||||
|
||||
return $uri;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Shipping costs from a Deal if it exists
|
||||
* @return string String of the deal shipping Cost
|
||||
*/
|
||||
private function getShippingCost($deal)
|
||||
{
|
||||
if ($deal->find('span[class*=cept-shipping-price]', 0) != null) {
|
||||
if ($deal->find('span[class*=cept-shipping-price]', 0)->children(0) != null) {
|
||||
return '<div>' . $this->i8n('shipping') . ' : '
|
||||
. $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
|
||||
. '</div>';
|
||||
} else {
|
||||
return '<div>' . $this->i8n('shipping') . ' : '
|
||||
. $deal->find('span[class*=cept-shipping-price]', 0)->innertext
|
||||
. '</div>';
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source of a Deal if it exists
|
||||
* @return string String of the deal source
|
||||
*/
|
||||
private function GetSource($deal)
|
||||
{
|
||||
if ($deal->find('a[class=text--color-greyShade]', 0) != null) {
|
||||
return '<div>' . $this->i8n('origin') . ' : '
|
||||
. $deal->find('a[class=text--color-greyShade]', 0)->outertext
|
||||
. '</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original Price and discout from a Deal if it exists
|
||||
* @return string String of the deal original price and discount
|
||||
*/
|
||||
private function getDiscount($deal)
|
||||
{
|
||||
if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
|
||||
$discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0);
|
||||
if ($discountHtml != null) {
|
||||
$discount = $discountHtml->plaintext;
|
||||
} else {
|
||||
$discount = '';
|
||||
}
|
||||
return '<div>' . $this->i8n('discount') . ' : <span style="text-decoration: line-through;">'
|
||||
. $deal->find(
|
||||
'span[class*=mute--text text--lineThrough]', 0
|
||||
)->plaintext
|
||||
. '</span> '
|
||||
. $discount
|
||||
. '</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Picture URL from a Deal if it exists
|
||||
* @return string String of the deal Picture URL
|
||||
*/
|
||||
private function getImage($deal)
|
||||
{
|
||||
$selectorLazy = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'thread-image',
|
||||
'width--all-auto',
|
||||
'height--all-auto',
|
||||
'imgFrame-img',
|
||||
'cept-thread-img',
|
||||
'img--dummy',
|
||||
'js-lazy-img'
|
||||
)
|
||||
);
|
||||
|
||||
$selectorPlain = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'thread-image',
|
||||
'width--all-auto',
|
||||
'height--all-auto',
|
||||
'imgFrame-img',
|
||||
'cept-thread-img'
|
||||
)
|
||||
);
|
||||
if ($deal->find('img[class=' . $selectorLazy . ']', 0) != null) {
|
||||
return json_decode(
|
||||
html_entity_decode(
|
||||
$deal->find('img[class=' . $selectorLazy . ']', 0)
|
||||
->getAttribute('data-lazy-img')))->{'src'};
|
||||
} else {
|
||||
return $deal->find('img[class*=' . $selectorPlain . ']', 0 )->src;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the originating country from a Deal if it exists
|
||||
* @return string String of the deal originating country
|
||||
*/
|
||||
private function getShipsFrom($deal)
|
||||
{
|
||||
$selector = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'meta-ribbon',
|
||||
'overflow--wrap-off',
|
||||
'space--l-3',
|
||||
'text--color-greyShade'
|
||||
)
|
||||
);
|
||||
if ($deal->find('span[class=' . $selector . ']', 0) != null) {
|
||||
return '<div>'
|
||||
. $deal->find('span[class=' . $selector . ']', 0)->children(2)->plaintext
|
||||
. '</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a local date into a timestamp
|
||||
* @return int timestamp of the input date
|
||||
*/
|
||||
private function parseDate($string)
|
||||
{
|
||||
$month_local = $this->i8n('local-months');
|
||||
$month_en = array(
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
);
|
||||
|
||||
// A date can be prfixed with some words, we remove theme
|
||||
$string = $this->removeDatePrefixes($string);
|
||||
// We translate the local months name in the english one
|
||||
$date_str = trim(str_replace($month_local, $month_en, $string));
|
||||
|
||||
// If the date does not contain any year, we add the current year
|
||||
if (!preg_match('/[0-9]{4}/', $string)) {
|
||||
$date_str .= ' ' . date('Y');
|
||||
}
|
||||
|
||||
// Add the Hour and minutes
|
||||
$date_str .= ' 00:00';
|
||||
$date = DateTime::createFromFormat('j F Y H:i', $date_str);
|
||||
// In some case, the date is not recognized : as a workaround the actual date is taken
|
||||
if($date === false) {
|
||||
$date = new DateTime();
|
||||
}
|
||||
return $date->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the prefix of a date if it has one
|
||||
* @return the date without prefiux
|
||||
*/
|
||||
private function removeDatePrefixes($string)
|
||||
{
|
||||
$string = str_replace($this->i8n('date-prefixes'), array(), $string);
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the suffix of a relative date if it has one
|
||||
* @return the relative date without suffixes
|
||||
*/
|
||||
private function removeRelativeDateSuffixes($string)
|
||||
{
|
||||
if (count($this->i8n('relative-date-ignore-suffix')) > 0) {
|
||||
$string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string);
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a relative local date into a timestamp
|
||||
* @return int timestamp of the input date
|
||||
*/
|
||||
private function relativeDateToTimestamp($str) {
|
||||
$date = new DateTime();
|
||||
|
||||
// In case of update date, replace it by the regular relative date first word
|
||||
$str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str);
|
||||
|
||||
$str = $this->removeRelativeDateSuffixes($str);
|
||||
|
||||
$search = $this->i8n('local-time-relative');
|
||||
|
||||
$replace = array(
|
||||
'-',
|
||||
'minute',
|
||||
'hour',
|
||||
'day',
|
||||
'month',
|
||||
'year',
|
||||
''
|
||||
);
|
||||
|
||||
$date->modify(str_replace($search, $replace, $str));
|
||||
return $date->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed title according to the parameters
|
||||
* @return string the RSS feed Tiyle
|
||||
*/
|
||||
public function getName(){
|
||||
switch($this->queriedContext) {
|
||||
case $this->i8n('context-keyword'):
|
||||
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-keyword') . ' : ' . $this->getInput('q');
|
||||
break;
|
||||
case $this->i8n('context-group'):
|
||||
$values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
|
||||
$group = array_search($this->getInput('group'), $values);
|
||||
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-group') . ' : ' . $group;
|
||||
break;
|
||||
case $this->i8n('context-talk'):
|
||||
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-talk') . ' : ' . $this->getTalkTitle();
|
||||
break;
|
||||
default: // Return default value
|
||||
return static::NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed URI according to the parameters
|
||||
* @return string the RSS feed Title
|
||||
*/
|
||||
public function getURI(){
|
||||
switch($this->queriedContext) {
|
||||
case $this->i8n('context-keyword'):
|
||||
return $this->getSearchURI();
|
||||
break;
|
||||
case $this->i8n('context-group'):
|
||||
return $this->getGroupURI();
|
||||
break;
|
||||
case $this->i8n('context-talk'):
|
||||
return $this->getTalkURI();
|
||||
break;
|
||||
default: // Return default value
|
||||
return static::URI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed URI for a keyword Feed
|
||||
* @return string the RSS feed URI
|
||||
*/
|
||||
private function getSearchURI(){
|
||||
$q = $this->getInput('q');
|
||||
$hide_expired = $this->getInput('hide_expired');
|
||||
$hide_local = $this->getInput('hide_local');
|
||||
$priceFrom = $this->getInput('priceFrom');
|
||||
$priceTo = $this->getInput('priceTo');
|
||||
$url = $this->i8n('bridge-uri')
|
||||
. 'search/advanced?q='
|
||||
. urlencode($q)
|
||||
. '&hide_expired=' . $hide_expired
|
||||
. '&hide_local=' . $hide_local
|
||||
. '&priceFrom=' . $priceFrom
|
||||
. '&priceTo=' . $priceTo
|
||||
/* Some default parameters
|
||||
* search_fields : Search in Titres & Descriptions & Codes
|
||||
* sort_by : Sort the search by new deals
|
||||
* time_frame : Search will not be on a limited timeframe
|
||||
*/
|
||||
. '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0';
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed URI for a group Feed
|
||||
* @return string the RSS feed URI
|
||||
*/
|
||||
private function getGroupURI(){
|
||||
$group = $this->getInput('group');
|
||||
$order = $this->getInput('order');
|
||||
|
||||
$url = $this->i8n('bridge-uri')
|
||||
. $this->i8n('uri-group') . $group . $order;
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed URI for a Talk Feed
|
||||
* @return string the RSS feed URI
|
||||
*/
|
||||
private function getTalkURI(){
|
||||
$url = $this->getInput('url');
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is some "localisation" function that returns the needed content using
|
||||
* the "$lang" class variable in the local class
|
||||
* @return various the local content needed
|
||||
*/
|
||||
protected function i8n($key)
|
||||
{
|
||||
if (array_key_exists($key, $this->lang)) {
|
||||
return $this->lang[$key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,47 +1,407 @@
|
||||
<?php
|
||||
class DeveloppezDotComBridge extends FeedExpander {
|
||||
|
||||
const MAINTAINER = 'polopollo';
|
||||
class DeveloppezDotComBridge extends FeedExpander
|
||||
{
|
||||
|
||||
const MAINTAINER = 'Binnette';
|
||||
const NAME = 'Developpez.com Actus (FR)';
|
||||
const URI = 'https://www.developpez.com/';
|
||||
const DOMAIN = '.developpez.com/';
|
||||
const RSS_URL = 'index/rss';
|
||||
const CACHE_TIMEOUT = 1800; // 30min
|
||||
const DESCRIPTION = 'Returns the 15 newest posts from DeveloppezDotCom (full text).';
|
||||
const DESCRIPTION = 'Returns complete posts from developpez.com';
|
||||
// Encodings used by Developpez.com in their articles body
|
||||
const ENCONDINGS = array('Windows-1252', 'UTF-8');
|
||||
const PARAMETERS = array(
|
||||
array(
|
||||
'limit' => array(
|
||||
'name' => 'Max items',
|
||||
'type' => 'number',
|
||||
'defaultValue' => 5,
|
||||
),
|
||||
// list of the differents RSS availables
|
||||
'domain' => array(
|
||||
'type' => 'list',
|
||||
'name' => 'Domaine',
|
||||
'title' => 'Chosissez un sous-domaine',
|
||||
'values' => array(
|
||||
'= Domaine principal =' => 'www',
|
||||
'4d' => '4d',
|
||||
'abbyy' => 'abbyy',
|
||||
'access' => 'access',
|
||||
'agile' => 'agile',
|
||||
'ajax' => 'ajax',
|
||||
'algo' => 'algo',
|
||||
'alm' => 'alm',
|
||||
'android' => 'android',
|
||||
'apache' => 'apache',
|
||||
'applications' => 'applications',
|
||||
'arduino' => 'arduino',
|
||||
'asm' => 'asm',
|
||||
'asp' => 'asp',
|
||||
'aspose' => 'aspose',
|
||||
'bacasable' => 'bacasable',
|
||||
'big-data' => 'big-data',
|
||||
'bpm' => 'bpm',
|
||||
'bsd' => 'bsd',
|
||||
'business-intelligence' => 'business-intelligence',
|
||||
'c' => 'c',
|
||||
'cloud-computing' => 'cloud-computing',
|
||||
'club' => 'club',
|
||||
'cms' => 'cms',
|
||||
'cpp' => 'cpp',
|
||||
'crm' => 'crm',
|
||||
'css' => 'css',
|
||||
'd' => 'd',
|
||||
'dart' => 'dart',
|
||||
'data-science' => 'data-science',
|
||||
'db2' => 'db2',
|
||||
'delphi' => 'delphi',
|
||||
'dotnet' => 'dotnet',
|
||||
'droit' => 'droit',
|
||||
'eclipse' => 'eclipse',
|
||||
'edi' => 'edi',
|
||||
'embarque' => 'embarque',
|
||||
'emploi' => 'emploi',
|
||||
'etudes' => 'etudes',
|
||||
'excel' => 'excel',
|
||||
'firebird' => 'firebird',
|
||||
'flash' => 'flash',
|
||||
'go' => 'go',
|
||||
'green-it' => 'green-it',
|
||||
'gtk' => 'gtk',
|
||||
'hardware' => 'hardware',
|
||||
'hpc' => 'hpc',
|
||||
'humour' => 'humour',
|
||||
'ibmcloud' => 'ibmcloud',
|
||||
'intelligence-artificielle' => 'intelligence-artificielle',
|
||||
'interbase' => 'interbase',
|
||||
'ios' => 'ios',
|
||||
'java' => 'java',
|
||||
'javascript' => 'javascript',
|
||||
'javaweb' => 'javaweb',
|
||||
'jetbrains' => 'jetbrains',
|
||||
'jeux' => 'jeux',
|
||||
'kotlin' => 'kotlin',
|
||||
'labview' => 'labview',
|
||||
'laravel' => 'laravel',
|
||||
'latex' => 'latex',
|
||||
'lazarus' => 'lazarus',
|
||||
'linux' => 'linux',
|
||||
'mac' => 'mac',
|
||||
'matlab' => 'matlab',
|
||||
'megaoffice' => 'megaoffice',
|
||||
'merise' => 'merise',
|
||||
'microsoft' => 'microsoft',
|
||||
'mobiles' => 'mobiles',
|
||||
'mongodb' => 'mongodb',
|
||||
'mysql' => 'mysql',
|
||||
'netbeans' => 'netbeans',
|
||||
'nodejs' => 'nodejs',
|
||||
'nosql' => 'nosql',
|
||||
'objective-c' => 'objective-c',
|
||||
'office' => 'office',
|
||||
'open-source' => 'open-source',
|
||||
'openoffice-libreoffice' => 'openoffice-libreoffice',
|
||||
'oracle' => 'oracle',
|
||||
'outlook' => 'outlook',
|
||||
'pascal' => 'pascal',
|
||||
'perl' => 'perl',
|
||||
'php' => 'php',
|
||||
'portail-emploi' => 'portail-emploi',
|
||||
'portail-projets' => 'portail-projets',
|
||||
'postgresql' => 'postgresql',
|
||||
'powerpoint' => 'powerpoint',
|
||||
'preprod-emploi' => 'preprod-emploi',
|
||||
'programmation' => 'programmation',
|
||||
'project' => 'project',
|
||||
'purebasic' => 'purebasic',
|
||||
'pyqt' => 'pyqt',
|
||||
'python' => 'python',
|
||||
'qt-creator' => 'qt-creator',
|
||||
'qt' => 'qt',
|
||||
'r' => 'r',
|
||||
'raspberry-pi' => 'raspberry-pi',
|
||||
'reseau' => 'reseau',
|
||||
'ruby' => 'ruby',
|
||||
'rust' => 'rust',
|
||||
'sap' => 'sap',
|
||||
'sas' => 'sas',
|
||||
'scilab' => 'scilab',
|
||||
'securite' => 'securite',
|
||||
'sgbd' => 'sgbd',
|
||||
'sharepoint' => 'sharepoint',
|
||||
'solutions-entreprise' => 'solutions-entreprise',
|
||||
'spring' => 'spring',
|
||||
'sqlserver' => 'sqlserver',
|
||||
'stages' => 'stages',
|
||||
'supervision' => 'supervision',
|
||||
'swift' => 'swift',
|
||||
'sybase' => 'sybase',
|
||||
'symfony' => 'symfony',
|
||||
'systeme' => 'systeme',
|
||||
'talend' => 'talend',
|
||||
'typescript' => 'typescript',
|
||||
'uml' => 'uml',
|
||||
'unix' => 'unix',
|
||||
'vb' => 'vb',
|
||||
'vba' => 'vba',
|
||||
'virtualisation' => 'virtualisation',
|
||||
'visualstudio' => 'visualstudio',
|
||||
'web-semantique' => 'web-semantique',
|
||||
'web' => 'web',
|
||||
'webmarketing' => 'webmarketing',
|
||||
'wind' => 'wind',
|
||||
'windows-azure' => 'windows-azure',
|
||||
'windows' => 'windows',
|
||||
'windowsphone' => 'windowsphone',
|
||||
'word' => 'word',
|
||||
'xhtml' => 'xhtml',
|
||||
'xml' => 'xml',
|
||||
'zend-framework' => 'zend-framework'
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function collectData(){
|
||||
$this->collectExpandableDatas(self::URI . 'index/rss', 15);
|
||||
/**
|
||||
* Return the RSS url for selected domain
|
||||
*/
|
||||
private function getRssUrl()
|
||||
{
|
||||
$domain = $this->getInput('domain');
|
||||
if (!empty($domain)) {
|
||||
return 'https://' . $domain . self::DOMAIN . self::RSS_URL;
|
||||
}
|
||||
|
||||
return self::URI . self::RSS_URL;
|
||||
}
|
||||
|
||||
protected function parseItem($newsItem){
|
||||
/**
|
||||
* Grabs the RSS item from Developpez.com
|
||||
*/
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getRssUrl();
|
||||
$this->collectExpandableDatas($url, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the content of every RSS item. And will try to get the full article
|
||||
* pointed by the item URL intead of the default abstract.
|
||||
*/
|
||||
protected function parseItem($newsItem)
|
||||
{
|
||||
if (count($this->items) >= $this->getInput('limit')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This function parse each entry in the RSS with the default parse
|
||||
$item = parent::parseItem($newsItem);
|
||||
$item['content'] = $this->extractContent($item['uri']);
|
||||
|
||||
// There is a bug in Developpez RSS, coma are writtent as '~?' in the
|
||||
// title, so I have to fix it manually
|
||||
$item['title'] = $this->fixComaInTitle($item['title']);
|
||||
|
||||
// We get the content of the full article behind the RSS item URL
|
||||
$articleHTMLContent = getSimpleHTMLDOMCached($item['uri']);
|
||||
|
||||
// Here we call our custom parser
|
||||
$fullText = $this->extractFullText($articleHTMLContent);
|
||||
if (!is_null($fullText)) {
|
||||
// if we manage to parse the page behind the url of the RSS item
|
||||
// then we set it as the new content. Otherwise we keep the default
|
||||
// content to avoid RSS Bridge to return an empty item
|
||||
$item['content'] = $fullText;
|
||||
}
|
||||
|
||||
// Now we will attach video url in item
|
||||
$videosUrl = $this->getAllVideoUrl($articleHTMLContent);
|
||||
if (!empty($videosUrl)) {
|
||||
$item['enclosures'] = array_merge($item['enclosures'], $videosUrl);
|
||||
}
|
||||
|
||||
// Now we can look for the blog writer/creator
|
||||
$author = $articleHTMLContent->find('[itemprop="creator"]', 0);
|
||||
if (!empty($author)) {
|
||||
$item['author'] = $author->outertext;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
// F***ing quotes from Microsoft Word badly encoded, here was the trick:
|
||||
// http://stackoverflow.com/questions/1262038/how-to-replace-microsoft-encoded-quotes-in-php
|
||||
private function convertSmartQuotes($string)
|
||||
/**
|
||||
* Replace '~?' by a proper coma ','
|
||||
*/
|
||||
private function fixComaInTitle($txt)
|
||||
{
|
||||
$search = array(chr(145),
|
||||
chr(146),
|
||||
chr(147),
|
||||
chr(148),
|
||||
chr(151));
|
||||
|
||||
$replace = array(
|
||||
"'",
|
||||
"'",
|
||||
'"',
|
||||
'"',
|
||||
'-'
|
||||
);
|
||||
|
||||
return str_replace($search, $replace, $string);
|
||||
return str_replace('~?', ',', $txt);
|
||||
}
|
||||
|
||||
private function extractContent($url){
|
||||
$articleHTMLContent = getSimpleHTMLDOMCached($url);
|
||||
$text = $this->convertSmartQuotes($articleHTMLContent->find('div.content', 0)->innertext);
|
||||
$text = utf8_encode($text);
|
||||
return trim($text);
|
||||
/**
|
||||
* Return the full article pointed by the url in the RSS item
|
||||
* Since Developpez.com only provides a short abstract of the article, we
|
||||
* use the url to retrieve the complete article and return it as the content
|
||||
*/
|
||||
private function extractFullText($articleHTMLContent)
|
||||
{
|
||||
// All blog entry contains a div with the class 'content'. This div
|
||||
// contains the complete blog article. But the RSS can also return
|
||||
// announcement and not a blog article. So the next if, should take
|
||||
// care of the "non blog" entry
|
||||
$divArticleEntry = $articleHTMLContent->find('div.content', 0);
|
||||
if (is_null($divArticleEntry)) {
|
||||
// Didn't find the div with class content. It is probably not a blog
|
||||
// entry. It is probably just an announcement for an ebook, a PDF,
|
||||
// etc. So we can use the default RSS item content.
|
||||
return null;
|
||||
}
|
||||
|
||||
// The following code is a bit hacky, but I really manage to get the
|
||||
// full content of articles without any encoding issues. What is very
|
||||
// weird and ugly in Developpez.com is the fact the some paragraphs of
|
||||
// the article will be encoded as UTF-8 and some other paragraphs will
|
||||
// be encoded as Windows-1252. So we can NOT decode the full article
|
||||
// with only one encoding. We have to check every paragraph and
|
||||
// determine its encoding
|
||||
|
||||
// This contains all the 'paragraphs' of the article. It includes the
|
||||
// pictures, the text and the links at the bottom of the article
|
||||
$paragraphs = $divArticleEntry->nodes;
|
||||
// This will store the complete decoded content
|
||||
$fullText = '';
|
||||
|
||||
// For each paragraph, we will identify the encoding, then decode it
|
||||
// and finally store the decoded content in $text
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
// We have to recreate a new DOM document from the current node
|
||||
// otherwise the find function will look in the complet article and
|
||||
// not only in the current paragraph. This is an ugly behavior of
|
||||
// the library Simple HTML DOM Parser...
|
||||
$html = str_get_html($paragraph->outertext);
|
||||
$fullText .= $this->decodeParagraph($html);
|
||||
}
|
||||
|
||||
// Finally we return the full 'well' enconded content of the article
|
||||
return $fullText;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function decodeParagraph($p)
|
||||
{
|
||||
// First we check if this paragraph is a video
|
||||
$videoUrl = $this->getVideoUrl($p);
|
||||
if (!empty($videoUrl)) {
|
||||
// If this is a video, we just return a link to the video
|
||||
// 📺 => 🎞️
|
||||
return '<p>
|
||||
<b>📺 <a href="' . $videoUrl . '">Voir la vidéo</a></b>
|
||||
</p>';
|
||||
}
|
||||
|
||||
// We take outertext to get the complete paragraph not only the text
|
||||
// inside it. That way we still graph block <img> and so on.
|
||||
$pTxt = $p->outertext;
|
||||
// This will store the decoded text if we manage to decode it
|
||||
$decodedTxt = '';
|
||||
|
||||
// This is the only way to properly decode each paragraph. I tried
|
||||
// many stuffs but this is the only working way I found.
|
||||
foreach (self::ENCONDINGS as $enc) {
|
||||
// We check the encoding of the current paragraph
|
||||
if (mb_check_encoding($pTxt, $enc)) {
|
||||
// If the encoding is well recognized, we can convert from
|
||||
// this encoding to UTF-8
|
||||
$decodedTxt = iconv($enc, 'UTF-8', $pTxt);
|
||||
}
|
||||
}
|
||||
|
||||
// We should not trim the strings to avoid the <a> to be glued to the
|
||||
// text like: the software<a href="...">started</a>to...
|
||||
if (!empty($decodedTxt)) {
|
||||
// We manage to decode the text, so we take the decoded version
|
||||
return $this->formatParagraph($decodedTxt);
|
||||
} else {
|
||||
// Otherwise we take the non decoded version and hope it will
|
||||
// be displayed not too ugly in the fulltext content
|
||||
return $this->formatParagraph($pTxt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true in $txt is a HTML tag and not plain text
|
||||
*/
|
||||
private function isHtmlTagNotTxt($txt)
|
||||
{
|
||||
$html = str_get_html($txt);
|
||||
return $html && $html->root && count($html->root->children) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add a space before paragraph when needed
|
||||
*/
|
||||
private function formatParagraph($txt)
|
||||
{
|
||||
// If the paragraph is an html tag, we add a space before
|
||||
if ($this->isHtmlTagNotTxt($txt)) {
|
||||
// the first element is an html tag and not a text, so we can add a
|
||||
// space before it
|
||||
return ' ' . $txt;
|
||||
}
|
||||
// If the text start with word (not punctation), we had a space
|
||||
$pattern = '/^\w/';
|
||||
if (preg_match($pattern, $txt)) {
|
||||
return ' ' . $txt;
|
||||
}
|
||||
return $txt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all video url in the article
|
||||
*/
|
||||
private function getAllVideoUrl($item)
|
||||
{
|
||||
// Array of video url
|
||||
$url = array();
|
||||
|
||||
// Developpez use a div with the class video-container
|
||||
$divsVideo = $item->find('div.video-container');
|
||||
if (empty($divsVideo)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
// get the url of the video
|
||||
foreach ($divsVideo as $div) {
|
||||
$html = str_get_html($div->outertext);
|
||||
$url[] = $this->getVideoUrl($html);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve URL video. We have to check for the src of an iframe
|
||||
* Work for Youtube. Will have to test for other video platform
|
||||
*/
|
||||
private function getVideoUrl($p)
|
||||
{
|
||||
$divVideo = $p->find('div.video-container', 0);
|
||||
if (empty($divVideo)) {
|
||||
return null;
|
||||
}
|
||||
$iframe = $divVideo->find('iframe', 0);
|
||||
if (empty($iframe)) {
|
||||
return null;
|
||||
}
|
||||
$src = trim($iframe->getAttribute('src'));
|
||||
if (empty($src)) {
|
||||
return null;
|
||||
}
|
||||
if (str_starts_with($src, '//')) {
|
||||
$src = 'https:' . $src;
|
||||
}
|
||||
return $src;
|
||||
}
|
||||
}
|
||||
|
141
bridges/EconomistWorldInBriefBridge.php
Normal file
141
bridges/EconomistWorldInBriefBridge.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
class EconomistWorldInBriefBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'sqrtminusone';
|
||||
const NAME = 'Economist the World in Brief Bridge';
|
||||
const URI = 'https://www.economist.com/the-world-in-brief';
|
||||
|
||||
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||
const DESCRIPTION = 'Returns stories from the World in Brief section';
|
||||
|
||||
const PARAMETERS = array(
|
||||
'' => array(
|
||||
'splitGobbets' => array(
|
||||
'name' => 'Split the short stories',
|
||||
'type' => 'checkbox',
|
||||
'defaultValue' => false,
|
||||
'title' => 'Whether to split the short stories into separate entries'
|
||||
),
|
||||
'limit' => array(
|
||||
'name' => 'Truncate headers for the short stories',
|
||||
'type' => 'number',
|
||||
'defaultValue' => 100
|
||||
),
|
||||
'agenda' => array(
|
||||
'name' => 'Add agenda for the day',
|
||||
'type' => 'checkbox',
|
||||
'defaultValue' => 'checked'
|
||||
),
|
||||
'agendaPictures' => array(
|
||||
'name' => 'Include pictures to the agenda',
|
||||
'type' => 'checkbox',
|
||||
'defaultValue' => 'checked'
|
||||
),
|
||||
'quote' => array(
|
||||
'name' => 'Include the quote of the day',
|
||||
'type' => 'checkbox'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
$gobbets = $html->find('._gobbets', 0);
|
||||
if ($this->getInput('splitGobbets') == 1) {
|
||||
$this->splitGobbets($gobbets);
|
||||
} else {
|
||||
$this->mergeGobbets($gobbets);
|
||||
};
|
||||
if ($this->getInput('agenda') == 1) {
|
||||
$articles = $html->find('._articles', 0);
|
||||
$this->collectArticles($articles);
|
||||
}
|
||||
if ($this->getInput('quote') == 1) {
|
||||
$quote = $html->find('._quote-container', 0);
|
||||
$this->addQuote($quote);
|
||||
}
|
||||
}
|
||||
|
||||
private function splitGobbets($gobbets)
|
||||
{
|
||||
$today = new Datetime();
|
||||
$today->setTime(0, 0, 0, 0);
|
||||
$limit = $this->getInput('limit');
|
||||
foreach ($gobbets->find('._gobbet') as $gobbet) {
|
||||
$title = $gobbet->plaintext;
|
||||
$match = preg_match('/[\.,]/', $title, $matches, PREG_OFFSET_CAPTURE);
|
||||
if ($match > 0) {
|
||||
$point = $matches[0][1];
|
||||
$title = mb_substr($title, 0, $point);
|
||||
}
|
||||
if ($limit && mb_strlen($title) > $limit) {
|
||||
$title = mb_substr($title, 0, $limit) . '...';
|
||||
}
|
||||
$item = array(
|
||||
'uri' => self::URI,
|
||||
'title' => $title,
|
||||
'content' => $gobbet->innertext,
|
||||
'timestamp' => $today->format('U'),
|
||||
'uid' => md5($gobbet->plaintext)
|
||||
);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function mergeGobbets($gobbets)
|
||||
{
|
||||
$today = new Datetime();
|
||||
$today->setTime(0, 0, 0, 0);
|
||||
$contents = '';
|
||||
foreach ($gobbets->find('._gobbet') as $gobbet) {
|
||||
$contents .= "<p>{$gobbet->innertext}";
|
||||
}
|
||||
$this->items[] = array(
|
||||
'uri' => self::URI,
|
||||
'title' => 'World in brief at ' . $today->format('Y.m.d'),
|
||||
'content' => $contents,
|
||||
'timestamp' => $today->format('U'),
|
||||
'uid' => 'world-in-brief-' . $today->format('U')
|
||||
);
|
||||
}
|
||||
|
||||
private function collectArticles($articles)
|
||||
{
|
||||
$i = 0;
|
||||
$today = new Datetime();
|
||||
$today->setTime(0, 0, 0, 0);
|
||||
foreach ($articles->find('._article') as $article) {
|
||||
$title = $article->find('._headline', 0)->plaintext;
|
||||
$image = $article->find('._main-image', 0);
|
||||
$content = $article->find('._content', 0);
|
||||
|
||||
$res_content = '';
|
||||
if ($image != null && $this->getInput('agendaPictures') == 1) {
|
||||
$img = $image->find('img', 0);
|
||||
$res_content .= '<img src="' . $img->src . '" />';
|
||||
}
|
||||
$res_content .= $content->innertext;
|
||||
$this->items[] = array(
|
||||
'uri' => self::URI,
|
||||
'title' => $title,
|
||||
'content' => $res_content,
|
||||
'timestamp' => $today->format('U'),
|
||||
'uid' => 'story-' . $today->format('U') . "{$i}",
|
||||
);
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
private function addQuote($quote) {
|
||||
$today = new Datetime();
|
||||
$today->setTime(0, 0, 0, 0);
|
||||
$this->items[] = array(
|
||||
'uri' => self::URI,
|
||||
'title' => 'Quote of the day ' . $today->format('Y.m.d'),
|
||||
'content' => $quote->innertext,
|
||||
'timestamp' => $today->format('U'),
|
||||
'uid' => 'quote-' . $today->format('U')
|
||||
);
|
||||
}
|
||||
}
|
209
bridges/EuronewsBridge.php
Normal file
209
bridges/EuronewsBridge.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
class EuronewsBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'sqrtminusone';
|
||||
const NAME = 'Euronews Bridge';
|
||||
const URI = 'https://www.euronews.com/';
|
||||
const CACHE_TIMEOUT = 600; // 10 minutes
|
||||
const DESCRIPTION = 'Return articles from the "Just In" feed of Euronews.';
|
||||
|
||||
const PARAMETERS = array(
|
||||
'' => array(
|
||||
'lang' => array(
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'defaultValue' => 'euronews.com',
|
||||
'values' => array(
|
||||
'English' => 'euronews.com',
|
||||
'French' => 'fr.euronews.com',
|
||||
'German' => 'de.euronews.com',
|
||||
'Italian' => 'it.euronews.com',
|
||||
'Spanish' => 'es.euronews.com',
|
||||
'Portuguese' => 'pt.euronews.com',
|
||||
'Russian' => 'ru.euronews.com',
|
||||
'Turkish' => 'tr.euronews.com',
|
||||
'Greek' => 'gr.euronews.com',
|
||||
'Hungarian' => 'hu.euronews.com',
|
||||
'Persian' => 'per.euronews.com',
|
||||
'Arabic' => 'arabic.euronews.com',
|
||||
/* These versions don't have timeline.json */
|
||||
// 'Albanian' => 'euronews.al',
|
||||
// 'Romanian' => 'euronews.ro',
|
||||
// 'Georigian' => 'euronewsgeorgia.com',
|
||||
// 'Bulgarian' => 'euronewsbulgaria.com'
|
||||
// 'Serbian' => 'euronews.rs'
|
||||
)
|
||||
),
|
||||
'limit' => array(
|
||||
'name' => 'Limit of items per feed',
|
||||
'required' => true,
|
||||
'type' => 'number',
|
||||
'defaultValue' => 10,
|
||||
'title' => 'Maximum number of returned feed items. Maximum 50, default 10'
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$limit = $this->getInput('limit');
|
||||
$root_url = 'https://' . $this->getInput('lang');
|
||||
$url = $root_url . '/api/timeline.json?limit=' . $limit;
|
||||
$json = getContents($url);
|
||||
$data = json_decode($json, true);
|
||||
|
||||
foreach ($data as $datum) {
|
||||
$datum_uri = $root_url . $datum['fullUrl'];
|
||||
$url_datum = $this->getItemContent($datum_uri);
|
||||
$categories = array();
|
||||
if (array_key_exists('program', $datum)) {
|
||||
if (array_key_exists('title', $datum['program'])) {
|
||||
$categories[] = $datum['program']['title'];
|
||||
}
|
||||
}
|
||||
if (array_key_exists('themes', $datum)) {
|
||||
foreach ($datum['themes'] as $theme) {
|
||||
$categories[] = $theme['title'];
|
||||
}
|
||||
}
|
||||
$item = array(
|
||||
'uri' => $datum_uri,
|
||||
'title' => $datum['title'],
|
||||
'uid' => strval($datum['id']),
|
||||
'timestamp' => $datum['publishedAt'],
|
||||
'content' => $url_datum['content'],
|
||||
'author' => $url_datum['author'],
|
||||
'enclosures' => $url_datum['enclosures'],
|
||||
'categories' => array_unique($categories)
|
||||
);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function getItemContent($url)
|
||||
{
|
||||
try {
|
||||
$html = getSimpleHTMLDOMCached($url);
|
||||
} catch (Exception $e) {
|
||||
// Every once in a while it fails with too many redirects
|
||||
return array('author' => null, 'content' => null, 'enclosures' => null);
|
||||
}
|
||||
$data = $html->find('script[type="application/ld+json"]', 0)->innertext;
|
||||
$json = json_decode($data, true);
|
||||
$author = 'Euronews';
|
||||
$content = '';
|
||||
$enclosures = array();
|
||||
if (array_key_exists('@graph', $json)) {
|
||||
foreach ($json['@graph'] as $item) {
|
||||
if ($item['@type'] == 'NewsArticle') {
|
||||
if (array_key_exists('author', $item)) {
|
||||
$author = $item['author']['name'];
|
||||
}
|
||||
if (array_key_exists('image', $item)) {
|
||||
$content .= '<figure>';
|
||||
$content .= '<img src="' . $item['image']['url'] . '">';
|
||||
$content .= '<figcaption>' . $item['image']['caption'] . '</figcaption>';
|
||||
$content .= '</figure><br>';
|
||||
}
|
||||
if (array_key_exists('video', $item)) {
|
||||
$enclosures[] = $item['video']['contentUrl'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normal article
|
||||
$article_content = $html->find('.c-article-content', 0);
|
||||
if ($article_content) {
|
||||
// Usually the .c-article-content is the root of the
|
||||
// content, but once in a blue moon the root is the second
|
||||
// div
|
||||
if ((count($article_content->children()) == 2)
|
||||
&& ($article_content->children(1)->tag == 'div')
|
||||
) {
|
||||
$article_content = $article_content->children(1);
|
||||
}
|
||||
// The content is interspersed with links and stuff, so we
|
||||
// iterate over the children
|
||||
foreach ($article_content->children() as $element) {
|
||||
if ($element->tag == 'p') {
|
||||
$scribble_live = $element->find('#scribblelive-items', 0);
|
||||
if (is_null($scribble_live)) {
|
||||
// A normal paragraph
|
||||
$content .= '<p>' . $element->innertext . '</p>';
|
||||
} else {
|
||||
// LIVE mode
|
||||
foreach ($scribble_live->children() as $child) {
|
||||
if ($child->tag == 'div') {
|
||||
$content .= '<div>' . $child->innertext . '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (preg_match('/h[1-6]/', $element->tag)) {
|
||||
// Header
|
||||
$content .= '<h' . $element->tag[1] . '>' . $element->innertext . '</h' . $element->tag[1] . '>';
|
||||
} elseif ($element->tag == 'div') {
|
||||
if (preg_match('/.*widget--type-image.*/', $element->class)) {
|
||||
// Image
|
||||
$content .= '<figure>';
|
||||
$content .= '<img src="' . $element->find('img', 0)->src . '">';
|
||||
$caption = $element->find('figcaption', 0);
|
||||
if ($caption) {
|
||||
$content .= '<figcaption>' . $element->plaintext . '</figcaption>';
|
||||
}
|
||||
$content .= '</figure><br>';
|
||||
} elseif (preg_match('/.*widget--type-quotation.*/', $element->class)) {
|
||||
// Quotation
|
||||
$quote = $element->find('.widget__quoteText', 0);
|
||||
$author = $element->find('.widget__author', 0);
|
||||
$content .= '<figure>';
|
||||
$content .= '<blockquote>' . $quote->plaintext . '</blockquote>';
|
||||
if ($author) {
|
||||
$content .= '<figcaption>' . $author->plaintext . '</figcaption>';
|
||||
}
|
||||
$content .= '</figure><br>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Video article
|
||||
if (is_null($article_content)) {
|
||||
$image = $html->find('.c-article-media__img', 0);
|
||||
if ($image) {
|
||||
$content .= '<figure>';
|
||||
$content .= '<img src="' . $image->src . '">';
|
||||
$content .= '</figure><br>';
|
||||
}
|
||||
|
||||
$description = $html->find('.m-object__description', 0);
|
||||
if ($description) {
|
||||
// In some editions the description is a link to the
|
||||
// current page
|
||||
$content .= '<div>' . $description->plaintext . '</div>';
|
||||
}
|
||||
|
||||
// Euronews usually hosts videos on dailymotion...
|
||||
$player_div = $html->find('.dmPlayer', 0);
|
||||
if ($player_div) {
|
||||
$video_id = $player_div->getAttribute('data-video-id');
|
||||
$video_url = 'https://www.dailymotion.com/video/' . $video_id;
|
||||
$content .= '<a href="' . $video_url . '">' . $video_url . '</a>';
|
||||
}
|
||||
|
||||
// ...or on YouTube
|
||||
$player_div = $html->find('.js-player-pfp', 0);
|
||||
if ($player_div) {
|
||||
$video_id = $player_div->getAttribute('data-video-id');
|
||||
$video_url = 'https://www.youtube.com/watch?v=' . $video_id;
|
||||
$content .= '<a href="' . $video_url . '">' . $video_url . '</a>';
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'author' => $author,
|
||||
'content' => $content,
|
||||
'enclosures' => $enclosures
|
||||
);
|
||||
}
|
||||
}
|
197
bridges/FDroidRepoBridge.php
Normal file
197
bridges/FDroidRepoBridge.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
class FDroidRepoBridge extends BridgeAbstract {
|
||||
const NAME = 'F-Droid Repository Bridge';
|
||||
const URI = 'https://f-droid.org/';
|
||||
const DESCRIPTION = 'Query any F-Droid Repository for its latest updates.';
|
||||
const MAINTAINER = 'Yaman Qalieh';
|
||||
|
||||
const ITEM_LIMIT = 50;
|
||||
|
||||
const PARAMETERS = array(
|
||||
'global' => array(
|
||||
'url' => array(
|
||||
'name' => 'Repository URL',
|
||||
'title' => 'Usually ends with /repo/',
|
||||
'required' => true,
|
||||
'exampleValue' => 'https://srv.tt-rss.org/fdroid/repo'
|
||||
)
|
||||
),
|
||||
'Latest Updates' => array(
|
||||
'sorting' => array(
|
||||
'name' => 'Sort By',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'Latest added apps' => 'added',
|
||||
'Latest updated apps' => 'lastUpdated'
|
||||
)
|
||||
),
|
||||
'locale' => array(
|
||||
'name' => 'Locale',
|
||||
'defaultValue' => 'en-US'
|
||||
)
|
||||
),
|
||||
'Follow Package' => array(
|
||||
'package' => array(
|
||||
'name' => 'Package Identifier',
|
||||
'required' => true,
|
||||
'exampleValue' => 'org.fox.ttrss'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Stores repo information
|
||||
private $repo;
|
||||
|
||||
public function getURI() {
|
||||
if (empty($this->queriedContext))
|
||||
return parent::getURI();
|
||||
|
||||
$url = rtrim($this->GetInput('url'), '/');
|
||||
return strstr($url, '?', true) ?: $url;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
if (empty($this->queriedContext))
|
||||
return parent::getName();
|
||||
|
||||
$name = $this->repo['repo']['name'];
|
||||
switch($this->queriedContext) {
|
||||
case 'Latest Updates':
|
||||
return $name;
|
||||
case 'Follow Package':
|
||||
return $this->getInput('package') . ' - ' . $name;
|
||||
default:
|
||||
returnServerError('Unimplemented Context (getName)');
|
||||
}
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$this->repo = $this->getRepo();
|
||||
switch($this->queriedContext) {
|
||||
case 'Latest Updates':
|
||||
$this->getAllUpdates();
|
||||
break;
|
||||
case 'Follow Package':
|
||||
$this->getPackage($this->getInput('package'));
|
||||
break;
|
||||
default:
|
||||
returnServerError('Unimplemented Context (collectData)');
|
||||
}
|
||||
}
|
||||
|
||||
private function getRepo() {
|
||||
$url = $this->getURI();
|
||||
|
||||
// Get repo information (only available as JAR)
|
||||
$jar = getContents($url . '/index-v1.jar');
|
||||
$jar_loc = tempnam(sys_get_temp_dir(), '');
|
||||
file_put_contents($jar_loc, $jar);
|
||||
|
||||
// JAR files are specially formatted ZIP files
|
||||
$jar = new ZipArchive;
|
||||
if ($jar->open($jar_loc) !== true) {
|
||||
returnServerError('Failed to extract archive');
|
||||
}
|
||||
|
||||
// Get file pointer to the relevant JSON inside
|
||||
$fp = $jar->getStream('index-v1.json');
|
||||
if (!$fp) {
|
||||
returnServerError('Failed to get file pointer');
|
||||
}
|
||||
|
||||
$data = json_decode(stream_get_contents($fp), true);
|
||||
fclose($fp);
|
||||
$jar->close();
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getAllUpdates() {
|
||||
$apps = $this->repo['apps'];
|
||||
usort($apps, function($a, $b) {
|
||||
return $b[$this->getInput('sorting')] <=> $a[$this->getInput('sorting')];
|
||||
});
|
||||
$apps = array_slice($apps, 0, self::ITEM_LIMIT);
|
||||
foreach($apps as $app) {
|
||||
$latest = reset($this->repo['packages'][$app['packageName']]);
|
||||
|
||||
if (isset($app['localized'])) {
|
||||
// Try provided locale, then en-US, then any
|
||||
$lang = $app['localized'];
|
||||
$lang = $lang[$this->getInput('locale')] ?? $lang['en-US'] ?? reset($lang);
|
||||
} else
|
||||
$lang = array();
|
||||
|
||||
$item = array();
|
||||
$item['uri'] = $this->getURI() . '/' . $latest['apkName'];
|
||||
$item['title'] = $lang['name'] ?? $app['packageName'];
|
||||
$item['title'] .= ' ' . $latest['versionName'];
|
||||
$item['timestamp'] = date(DateTime::ISO8601, (int) ($app['lastUpdated'] / 1000));
|
||||
if (isset($app['authorName']))
|
||||
$item['author'] = $app['authorName'];
|
||||
if (isset($app['categories']))
|
||||
$item['categories'] = $app['categories'];
|
||||
|
||||
// Adding Content
|
||||
$icon = $app['icon'] ?? '';
|
||||
if (!empty($icon)) {
|
||||
$icon = $this->getURI() . '/icons-320/' . $icon;
|
||||
$item['enclosures'] = array($icon);
|
||||
$icon = '<img src="' . $icon . '">';
|
||||
}
|
||||
$summary = $lang['summary'] ?? $app['summary'] ?? '';
|
||||
$description = markdownToHtml(trim($lang['description'] ?? $app['description'] ?? 'None'));
|
||||
$whatsNew = markdownToHtml(trim($lang['whatsNew'] ?? 'None'));
|
||||
$website = $this->link($lang['webSite'] ?? $app['webSite'] ?? $app['authorWebSite'] ?? null);
|
||||
$source = $this->link($app['sourceCode'] ?? null);
|
||||
$issueTracker = $this->link($app['issueTracker'] ?? null);
|
||||
$license = $app['license'] ?? 'None';
|
||||
$item['content'] = <<<EOD
|
||||
{$icon}
|
||||
<p>{$summary}</p>
|
||||
<h1>Description</h1>
|
||||
{$description}
|
||||
<h1>What's New</h1>
|
||||
{$whatsNew}
|
||||
<h1>Information</h1>
|
||||
<p>Website: {$website}</p>
|
||||
<p>Source Code: {$source}</p>
|
||||
<p>Issue Tracker: {$issueTracker}</p>
|
||||
<p>license: {$app['license']}</p>
|
||||
EOD;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function getPackage($package) {
|
||||
if (!isset($this->repo['packages'][$package])) {
|
||||
returnClientError('Invalid Package Name');
|
||||
}
|
||||
$package = $this->repo['packages'][$package];
|
||||
|
||||
$count = self::ITEM_LIMIT;
|
||||
foreach($package as $version) {
|
||||
$item = array();
|
||||
$item['uri'] = $this->getURI() . '/' . $version['apkName'];
|
||||
$item['title'] = $version['versionName'];
|
||||
$item['timestamp'] = date(DateTime::ISO8601, (int) ($version['added'] / 1000));
|
||||
$item['uid'] = $version['versionCode'];
|
||||
$size = round($version['size'] / 1048576, 1); // Bytes -> MB
|
||||
$sdk_link = 'https://developer.android.com/studio/releases/platforms';
|
||||
$item['content'] = <<<EOD
|
||||
<p>size: {$size}MB</p>
|
||||
<p>Minimum SDK: {$version['minSdkVersion']}
|
||||
(<a href="{$sdk_link}">SDK to Android Version List</a>)</p>
|
||||
<p>hash ({$version['hashType']}): {$version['hash']}</p>
|
||||
EOD;
|
||||
$this->items[] = $item;
|
||||
if (--$count <= 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function link($url) {
|
||||
if (empty($url))
|
||||
return null;
|
||||
return '<a href="' . $url . '">' . $url . '</a>';
|
||||
}
|
||||
}
|
@@ -18,7 +18,7 @@ class FSecureBlogBridge extends BridgeAbstract {
|
||||
),
|
||||
'oldest_date' => array(
|
||||
'name' => 'Oldest article date',
|
||||
'exampleValue' => '-2 months',
|
||||
'exampleValue' => '-6 months',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
@@ -26,7 +26,8 @@ class FindACrewBridge extends BridgeAbstract {
|
||||
'distance' => array(
|
||||
'name' => 'Limit boundary of search in KM',
|
||||
'title' => 'Boundary of the search in kilometers when using longitude and latitude'
|
||||
)
|
||||
),
|
||||
'limit' => self::LIMIT,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -59,7 +60,8 @@ class FindACrewBridge extends BridgeAbstract {
|
||||
$html = getSimpleHTMLDOM($url, $header, $opts) or returnClientError('No results for this query.');
|
||||
|
||||
$annonces = $html->find('.css_SrhRst');
|
||||
foreach ($annonces as $annonce) {
|
||||
$limit = $this->getInput('limit') ?? 10;
|
||||
foreach (array_slice($annonces, 0, $limit) as $annonce) {
|
||||
$item = array();
|
||||
|
||||
$link = parent::getURI() . $annonce->find('.lstsum-btn-con a', 0)->href;
|
||||
|
@@ -63,6 +63,7 @@ class FolhaDeSaoPauloBridge extends FeedExpander {
|
||||
$feed_url = self::URI . '/' . $this->getInput('feed');
|
||||
}
|
||||
Debug::log('URL: ' . $feed_url);
|
||||
$this->collectExpandableDatas($feed_url, $this->getInput('amount'));
|
||||
$limit = $this->getInput('amount');
|
||||
$this->collectExpandableDatas($feed_url, $limit);
|
||||
}
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ class GQMagazineBridge extends BridgeAbstract
|
||||
'required' => true,
|
||||
'exampleValue' => 'sexe/news'
|
||||
),
|
||||
'limit' => self::LIMIT,
|
||||
));
|
||||
|
||||
const REPLACED_ATTRIBUTES = array(
|
||||
@@ -76,7 +77,12 @@ class GQMagazineBridge extends BridgeAbstract
|
||||
|
||||
// Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content !
|
||||
$main = $html->find('main', 0);
|
||||
$limit = $this->getInput('limit') ?? 10;
|
||||
foreach ($main->find('a') as $link) {
|
||||
if (count($this->items) >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
$uri = $link->href;
|
||||
$date = $link->parent()->find('time', 0);
|
||||
|
||||
|
@@ -15,7 +15,7 @@ class GelbooruBridge extends BridgeAbstract {
|
||||
),
|
||||
't' => array(
|
||||
'name' => 'tags',
|
||||
'exampleValue' => 'pinup',
|
||||
'exampleValue' => 'solo',
|
||||
'title' => 'Tags to search for'
|
||||
),
|
||||
'l' => array(
|
||||
@@ -68,6 +68,7 @@ class GelbooruBridge extends BridgeAbstract {
|
||||
|
||||
public function collectData(){
|
||||
$content = getContents($this->getFullURI());
|
||||
// $content is empty string
|
||||
|
||||
// Most other Gelbooru-based boorus put their content in the root of
|
||||
// the JSON. This check is here for Bridges that inherit from this one
|
||||
|
@@ -30,7 +30,7 @@ class GettrBridge extends BridgeAbstract
|
||||
$api = sprintf(
|
||||
'https://api.gettr.com/u/user/%s/posts?offset=0&max=%s&dir=fwd&incl=posts&fp=f_uo',
|
||||
$this->getInput('user'),
|
||||
max($this->getInput('limit'), 20)
|
||||
min($this->getInput('limit'), 20)
|
||||
);
|
||||
$data = json_decode(getContents($api), false);
|
||||
|
||||
|
@@ -31,6 +31,15 @@ class GiphyBridge extends BridgeAbstract {
|
||||
)
|
||||
));
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->getInput('s'))) {
|
||||
return $this->getInput('s') . ' - ' . parent::getName();
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
protected function getGiphyItems($entries){
|
||||
foreach($entries as $entry) {
|
||||
$createdAt = new \DateTime($entry->import_datetime);
|
||||
@@ -56,12 +65,16 @@ HTML
|
||||
|
||||
public function collectData() {
|
||||
/**
|
||||
* This uses a public beta key which has severe rate limiting.
|
||||
* This uses Giphy's own undocumented public prod api key,
|
||||
* which should not have any rate limiting.
|
||||
* There is a documented public beta api key (dc6zaTOxFJmzC),
|
||||
* but it has severe rate limiting.
|
||||
*
|
||||
* https://giphy.api-docs.io/1.0/welcome/access-and-api-keys
|
||||
* https://giphy.api-docs.io/1.0/gifs/search-1
|
||||
* https://developers.giphy.com/branch/master/docs/api/endpoint/#search
|
||||
*/
|
||||
$apiKey = 'dc6zaTOxFJmzC';
|
||||
$apiKey = 'Gc7131jiJuvI7IdN0HZ1D7nh0ow5BU6g';
|
||||
$bundle = 'low_bandwidth';
|
||||
$limit = min($this->getInput('n') ?: 10, 50);
|
||||
$endpoints = array();
|
||||
if (empty($this->getInput('noGif'))) {
|
||||
@@ -73,10 +86,11 @@ HTML
|
||||
|
||||
foreach ($endpoints as $endpoint) {
|
||||
$uri = sprintf(
|
||||
'https://api.giphy.com/v1/%s/search?q=%s&limit=%s&api_key=%s',
|
||||
'https://api.giphy.com/v1/%s/search?q=%s&limit=%s&bundle=%s&api_key=%s',
|
||||
$endpoint,
|
||||
rawurlencode($this->getInput('s')),
|
||||
$limit,
|
||||
$bundle,
|
||||
$apiKey
|
||||
);
|
||||
|
||||
|
@@ -1,27 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* Gitea is a fork of Gogs which may diverge in the future.
|
||||
* Gitea is a community managed lightweight code hosting solution.
|
||||
* https://docs.gitea.io/en-us/
|
||||
*/
|
||||
require_once 'GogsBridge.php';
|
||||
|
||||
class GiteaBridge extends GogsBridge {
|
||||
class GiteaBridge extends BridgeAbstract {
|
||||
|
||||
const NAME = 'Gitea';
|
||||
const URI = 'https://gitea.io';
|
||||
const DESCRIPTION = 'Returns the latest issues, commits or releases';
|
||||
const MAINTAINER = 'logmanoriginal';
|
||||
const MAINTAINER = 'gileri';
|
||||
const CACHE_TIMEOUT = 300; // 5 minutes
|
||||
|
||||
const PARAMETERS = array(
|
||||
'global' => array(
|
||||
'host' => array(
|
||||
'name' => 'Host',
|
||||
'exampleValue' => 'https://gitea.com',
|
||||
'required' => true,
|
||||
'title' => 'Host name with its protocol, without trailing slash',
|
||||
),
|
||||
'user' => array(
|
||||
'name' => 'Username',
|
||||
'exampleValue' => 'gitea',
|
||||
'required' => true,
|
||||
'title' => 'User name as it appears in the URL',
|
||||
),
|
||||
'project' => array(
|
||||
'name' => 'Project name',
|
||||
'exampleValue' => 'helm-chart',
|
||||
'required' => true,
|
||||
'title' => 'Project name as it appears in the URL',
|
||||
),
|
||||
),
|
||||
'Commits' => array(
|
||||
'branch' => array(
|
||||
'name' => 'Branch name',
|
||||
'defaultValue' => 'master',
|
||||
'required' => true,
|
||||
'title' => 'Branch name as it appears in the URL',
|
||||
),
|
||||
),
|
||||
'Issues' => array(
|
||||
'include_description' => array(
|
||||
'name' => 'Include issue description',
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Activate to include the issue description',
|
||||
),
|
||||
),
|
||||
'Single issue' => array(
|
||||
'issue' => array(
|
||||
'name' => 'Issue number',
|
||||
'type' => 'number',
|
||||
'exampleValue' => 100,
|
||||
'required' => true,
|
||||
'title' => 'Issue number from the issues list',
|
||||
),
|
||||
),
|
||||
'Single pull request' => array(
|
||||
'pull_request' => array(
|
||||
'name' => 'Pull request number',
|
||||
'type' => 'number',
|
||||
'exampleValue' => 100,
|
||||
'required' => true,
|
||||
'title' => 'Pull request number from the issues list',
|
||||
),
|
||||
),
|
||||
'Pull requests' => array(
|
||||
'include_description' => array(
|
||||
'name' => 'Include pull request description',
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Activate to include the pull request description',
|
||||
),
|
||||
),
|
||||
'Releases' => array(),
|
||||
'Tags' => array(),
|
||||
);
|
||||
|
||||
private $title = '';
|
||||
|
||||
public function getIcon() {
|
||||
return 'https://gitea.io/images/gitea.png';
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
switch($this->queriedContext) {
|
||||
case 'Commits':
|
||||
case 'Issues':
|
||||
case 'Pull requests':
|
||||
case 'Releases':
|
||||
case 'Tags': return $this->title . ' ' . $this->queriedContext;
|
||||
case 'Single issue': return 'Issue ' . $this->getInput('issue') . ': ' . $this->title;
|
||||
case 'Single pull request': return 'Pull request ' . $this->getInput('pull_request') . ': ' . $this->title;
|
||||
default: return parent::getName();
|
||||
}
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
switch($this->queriedContext) {
|
||||
case 'Commits': {
|
||||
return $this->getInput('host')
|
||||
. '/' . $this->getInput('user')
|
||||
. '/' . $this->getInput('project')
|
||||
. '/commits/' . $this->getInput('branch');
|
||||
} break;
|
||||
case 'Issues': {
|
||||
return $this->getInput('host')
|
||||
. '/' . $this->getInput('user')
|
||||
. '/' . $this->getInput('project')
|
||||
. '/issues/';
|
||||
} break;
|
||||
case 'Single issue': {
|
||||
return $this->getInput('host')
|
||||
. '/' . $this->getInput('user')
|
||||
. '/' . $this->getInput('project')
|
||||
. '/issues/' . $this->getInput('issue');
|
||||
} break;
|
||||
case 'Releases': {
|
||||
return $this->getInput('host')
|
||||
. '/' . $this->getInput('user')
|
||||
. '/' . $this->getInput('project')
|
||||
. '/releases/';
|
||||
} break;
|
||||
case 'Tags': {
|
||||
return $this->getInput('host')
|
||||
. '/' . $this->getInput('user')
|
||||
. '/' . $this->getInput('project')
|
||||
. '/tags/';
|
||||
} break;
|
||||
case 'Pull requests': {
|
||||
return $this->getInput('host')
|
||||
. '/' . $this->getInput('user')
|
||||
. '/' . $this->getInput('project')
|
||||
. '/pulls/';
|
||||
} break;
|
||||
case 'Single pull request': {
|
||||
return $this->getInput('host')
|
||||
. '/' . $this->getInput('user')
|
||||
. '/' . $this->getInput('project')
|
||||
. '/pulls/' . $this->getInput('pull_request');
|
||||
} break;
|
||||
default: return parent::getURI();
|
||||
}
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$html = getSimpleHTMLDOM($this->getURI())
|
||||
or returnServerError('Could not request ' . $this->getURI());
|
||||
$html = defaultLinkTo($html, $this->getURI());
|
||||
|
||||
$this->title = $html->find('[property="og:title"]', 0)->content;
|
||||
|
||||
switch($this->queriedContext) {
|
||||
case 'Commits': {
|
||||
$this->collectCommitsData($html);
|
||||
} break;
|
||||
case 'Issues': {
|
||||
$this->collectIssuesData($html);
|
||||
} break;
|
||||
case 'Pull requests': {
|
||||
$this->collectPullRequestsData($html);
|
||||
} break;
|
||||
case 'Single issue': {
|
||||
$this->collectSingleIssueOrPrData($html);
|
||||
} break;
|
||||
case 'Single pull request': {
|
||||
$this->collectSingleIssueOrPrData($html);
|
||||
} break;
|
||||
case 'Releases': {
|
||||
$this->collectReleasesData($html);
|
||||
} break;
|
||||
case 'Tags': {
|
||||
$this->collectTagsData($html);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function collectReleasesData($html) {
|
||||
$releases = $html->find('#release-list > li')
|
||||
or returnServerError('Unable to find releases');
|
||||
|
||||
foreach($releases as $release) {
|
||||
$this->items[] = array(
|
||||
'author' => $release->find('.author', 0)->plaintext,
|
||||
'uri' => $release->find('a', 0)->href,
|
||||
'title' => 'Release ' . $release->find('h3', 0)->plaintext,
|
||||
'title' => 'Release ' . $release->find('h4', 0)->plaintext,
|
||||
'timestamp' => $release->find('.time-since', 0)->title,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function collectTagsData($html) {
|
||||
$tags = $html->find('table#tags-table > tbody > tr')
|
||||
or returnServerError('Unable to find tags');
|
||||
|
||||
foreach($tags as $tag) {
|
||||
$this->items[] = array(
|
||||
'uri' => $tag->find('a', 0)->href,
|
||||
'title' => 'Tag ' . $tag->find('.release-tag-name > a', 0)->plaintext,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function collectCommitsData($html) {
|
||||
$commits = $html->find('#commits-table tbody tr')
|
||||
or returnServerError('Unable to find commits');
|
||||
|
||||
foreach($commits as $commit) {
|
||||
$this->items[] = array(
|
||||
'uri' => $commit->find('a.sha', 0)->href,
|
||||
'title' => $commit->find('.message span', 0)->plaintext,
|
||||
'author' => $commit->find('.author', 0)->plaintext,
|
||||
'timestamp' => $commit->find('.time-since', 0)->title,
|
||||
'uid' => $commit->find('.sha', 0)->plaintext,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function collectIssuesData($html) {
|
||||
$issues = $html->find('.issue.list li')
|
||||
or returnServerError('Unable to find issues');
|
||||
|
||||
foreach($issues as $issue) {
|
||||
$uri = $issue->find('a', 0)->href;
|
||||
|
||||
$item = array(
|
||||
'uri' => $uri,
|
||||
'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext,
|
||||
'author' => $issue->find('.desc a', 1)->plaintext,
|
||||
'timestamp' => $issue->find('.time-since', 0)->title,
|
||||
);
|
||||
|
||||
if($this->getInput('include_description')) {
|
||||
$issue_html = getSimpleHTMLDOMCached($uri, 3600)
|
||||
or returnServerError('Unable to load issue description');
|
||||
|
||||
$issue_html = defaultLinkTo($issue_html, $uri);
|
||||
|
||||
$item['content'] = $issue_html->find('.comment .markup', 0);
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
protected function collectSingleIssueOrPrData($html) {
|
||||
$comments = $html->find('.comment')
|
||||
or returnServerError('Unable to find comments');
|
||||
|
||||
foreach($comments as $comment) {
|
||||
if (strpos($comment->getAttribute('class'), 'form') !== false
|
||||
|| strpos($comment->getAttribute('class'), 'merge') !== false
|
||||
) {
|
||||
// Ignore comment form and merge information
|
||||
continue;
|
||||
}
|
||||
$commentLink = $comment->find('a[href*="#issue"]', 0);
|
||||
$item = array(
|
||||
'author' => $comment->find('a.author', 0)->plaintext,
|
||||
'content' => $comment->find('.render-content', 0),
|
||||
);
|
||||
if ($commentLink !== null) {
|
||||
// Regular comment
|
||||
$item['uri'] = $commentLink->href;
|
||||
$item['title'] = str_replace($commentLink->plaintext, '', $comment->find('span', 0)->plaintext);
|
||||
$item['timestamp'] = $comment->find('.time-since', 0)->title;
|
||||
} else {
|
||||
// Change request comment
|
||||
$item['uri'] = $this->getURI() . '#' . $comment->getAttribute('id');
|
||||
$item['title'] = $comment->find('.comment-header .text', 0)->plaintext;
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
$this->items = array_reverse($this->items);
|
||||
}
|
||||
|
||||
protected function collectPullRequestsData($html) {
|
||||
$issues = $html->find('.issue.list li')
|
||||
or returnServerError('Unable to find pull requests');
|
||||
|
||||
foreach($issues as $issue) {
|
||||
$uri = $issue->find('a', 0)->href;
|
||||
|
||||
$item = array(
|
||||
'uri' => $uri,
|
||||
'title' => trim($issue->find('a.index', 0)->plaintext) . ' | ' . $issue->find('a.title', 0)->plaintext,
|
||||
'author' => $issue->find('.desc a', 1)->plaintext,
|
||||
'timestamp' => $issue->find('.time-since', 0)->title,
|
||||
);
|
||||
|
||||
if($this->getInput('include_description')) {
|
||||
$issue_html = getSimpleHTMLDOMCached($uri, 3600)
|
||||
or returnServerError('Unable to load issue description');
|
||||
|
||||
$issue_html = defaultLinkTo($issue_html, $uri);
|
||||
|
||||
$item['content'] = $issue_html->find('.comment .markup', 0);
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require_once('GithubIssueBridge.php');
|
||||
|
||||
class GitHubPullRequestBridge extends GithubIssueBridge {
|
||||
const MAINTAINER = 'Yaman Qalieh';
|
||||
const NAME = 'GitHub Pull Request';
|
||||
|
205
bridges/GitlabIssueBridge.php
Normal file
205
bridges/GitlabIssueBridge.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
class GitlabIssueBridge extends BridgeAbstract {
|
||||
|
||||
const MAINTAINER = 'Mynacol';
|
||||
const NAME = 'Gitlab Issue/Merge Request';
|
||||
const URI = 'https://gitlab.com/';
|
||||
const CACHE_TIMEOUT = 1800; // 30min
|
||||
const DESCRIPTION = 'Returns comments of an issue/MR of a gitlab project';
|
||||
|
||||
const PARAMETERS = array(
|
||||
'global' => array(
|
||||
'h' => array(
|
||||
'name' => 'Gitlab instance host name',
|
||||
'exampleValue' => 'gitlab.com',
|
||||
'defaultValue' => 'gitlab.com',
|
||||
'required' => true
|
||||
),
|
||||
'u' => array(
|
||||
'name' => 'User/Organization name',
|
||||
'exampleValue' => 'fdroid',
|
||||
'required' => true
|
||||
),
|
||||
'p' => array(
|
||||
'name' => 'Project name',
|
||||
'exampleValue' => 'fdroidclient',
|
||||
'required' => true
|
||||
)
|
||||
|
||||
),
|
||||
'Issue comments' => array(
|
||||
'i' => array(
|
||||
'name' => 'Issue number',
|
||||
'type' => 'number',
|
||||
'exampleValue' => '2099',
|
||||
'required' => true
|
||||
)
|
||||
),
|
||||
'Merge Request comments' => array(
|
||||
'i' => array(
|
||||
'name' => 'Merge Request number',
|
||||
'type' => 'number',
|
||||
'exampleValue' => '2099',
|
||||
'required' => true
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function getName(){
|
||||
$name = $this->getInput('h') . '/' . $this->getInput('u') . '/' . $this->getInput('p');
|
||||
switch ($this->queriedContext) {
|
||||
case 'Issue comments':
|
||||
$name .= ' Issue #' . $this->getInput('i');
|
||||
break;
|
||||
case 'Merge Request comments':
|
||||
$name .= ' MR !' . $this->getInput('i');
|
||||
break;
|
||||
default:
|
||||
return parent::getName();
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
$host = $this->getInput('h') ?? 'gitlab.com';
|
||||
$uri = 'https://' . $host . '/' . $this->getInput('u') . '/'
|
||||
. $this->getInput('p') . '/';
|
||||
switch ($this->queriedContext) {
|
||||
case 'Issue comments':
|
||||
$uri .= '-/issues';
|
||||
break;
|
||||
case 'Merge Request comments':
|
||||
$uri .= '-/merge_requests';
|
||||
break;
|
||||
default:
|
||||
return $uri;
|
||||
}
|
||||
$uri .= '/' . $this->getInput('i');
|
||||
return $uri;
|
||||
}
|
||||
|
||||
public function getIcon() {
|
||||
return 'https://' . $this->getInput('h') . '/favicon.ico';
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
switch ($this->queriedContext) {
|
||||
case 'Issue comments':
|
||||
$this->items[] = $this->parseIssueDescription();
|
||||
break;
|
||||
case 'Merge Request comments':
|
||||
$this->items[] = $this->parseMergeRequestDescription();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/* parse issue/MR comments */
|
||||
$comments_uri = $this->getURI() . '/discussions.json';
|
||||
$comments = getContents($comments_uri);
|
||||
$comments = json_decode($comments, false);
|
||||
|
||||
foreach ($comments as $value) {
|
||||
foreach ($value->notes as $comment) {
|
||||
$item = array();
|
||||
$item['uri'] = $comment->noteable_note_url;
|
||||
$item['uid'] = $item['uri'];
|
||||
|
||||
// TODO fix invalid timestamps (fdroid bot)
|
||||
$item['timestamp'] = $comment->created_at ?? $comment->updated_at ?? $comment->last_edited_at;
|
||||
$author = $comment->author ?? $comment->last_edited_by;
|
||||
$item['author'] = '<img src="' . $author->avatar_url . '" width=24></img> <a href="https://' .
|
||||
$this->getInput('h') . $author->path . '">' . $author->name . ' @' . $author->username . '</a>';
|
||||
|
||||
$content = '';
|
||||
if ($comment->system) {
|
||||
$content = $comment->note_html;
|
||||
if ($comment->type === 'StateNote') {
|
||||
$content .= ' the issue';
|
||||
} elseif ($comment->type === null) {
|
||||
// e.g. "added 900 commits\n800 from master\n175h4d - commit message\n..."
|
||||
$content = str_get_html($comment->note_html)->find('p', 0);
|
||||
}
|
||||
} else {
|
||||
// no switch-case to do strict comparison
|
||||
if ($comment->type === null || $comment->type === 'DiscussionNote') {
|
||||
$content = 'commented';
|
||||
} elseif ($comment->type === 'DiffNote') {
|
||||
$content = 'commented on a thread';
|
||||
} else {
|
||||
$content = $comment->note_html;
|
||||
}
|
||||
}
|
||||
$item['title'] = $author->name . " $content";
|
||||
|
||||
$content = $this->fixImgSrc($comment->note_html);
|
||||
$item['content'] = defaultLinkTo($content, 'https://' . $this->getInput('h') . '/');
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseIssueDescription() {
|
||||
$description_uri = $this->getURI() . '.json';
|
||||
$description = getContents($description_uri);
|
||||
$description = json_decode($description, false);
|
||||
$description_html = getSimpleHtmlDomCached($this->getURI());
|
||||
|
||||
$item = array();
|
||||
$item['uri'] = $this->getURI();
|
||||
$item['uid'] = $item['uri'];
|
||||
|
||||
$item['timestamp'] = $description->created_at ?? $description->updated_at;
|
||||
|
||||
$item['author'] = $this->parseAuthor($description_html);
|
||||
|
||||
$item['title'] = $description->title;
|
||||
$item['content'] = markdownToHtml($description->description);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function parseMergeRequestDescription() {
|
||||
$description_uri = $this->getURI() . '/cached_widget.json';
|
||||
$description = getContents($description_uri);
|
||||
$description = json_decode($description, false);
|
||||
$description_html = getSimpleHtmlDomCached($this->getURI());
|
||||
|
||||
$item = array();
|
||||
$item['uri'] = $this->getURI();
|
||||
$item['uid'] = $item['uri'];
|
||||
|
||||
$item['timestamp'] = $description_html->find('.merge-request-details time', 0)->datetime;
|
||||
|
||||
$item['author'] = $this->parseAuthor($description_html);
|
||||
|
||||
$item['title'] = 'Merge Request ' . $description->title;
|
||||
$item['content'] = markdownToHtml($description->description);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function fixImgSrc($html) {
|
||||
if (is_string($html)) {
|
||||
$html = str_get_html($html);
|
||||
}
|
||||
|
||||
foreach ($html->find('img') as $img) {
|
||||
$img->src = $img->getAttribute('data-src');
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function parseAuthor($description_html) {
|
||||
$description_html = $this->fixImgSrc($description_html);
|
||||
|
||||
$authors = $description_html->find('.issuable-meta a.author-link, .merge-request a.author-link');
|
||||
$editors = $description_html->find('.edited-text a.author-link');
|
||||
$author_str = implode(' ', $authors);
|
||||
if ($editors) {
|
||||
$author_str .= ', ' . implode(' ', $editors);
|
||||
}
|
||||
return defaultLinkTo($author_str, 'https://' . $this->getInput('h') . '/');
|
||||
}
|
||||
}
|
@@ -11,9 +11,9 @@ class GogsBridge extends BridgeAbstract {
|
||||
'global' => array(
|
||||
'host' => array(
|
||||
'name' => 'Host',
|
||||
'exampleValue' => 'notabug.org',
|
||||
'exampleValue' => 'https://notabug.org',
|
||||
'required' => true,
|
||||
'title' => 'Host name without trailing slash',
|
||||
'title' => 'Host name with its protocol, without trailing slash',
|
||||
),
|
||||
'user' => array(
|
||||
'name' => 'Username',
|
||||
|
125
bridges/GolemBridge.php
Normal file
125
bridges/GolemBridge.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
class GolemBridge extends FeedExpander {
|
||||
const MAINTAINER = 'Mynacol';
|
||||
const NAME = 'Golem Bridge';
|
||||
const URI = 'https://www.golem.de/';
|
||||
const CACHE_TIMEOUT = 1800; // 30min
|
||||
const DESCRIPTION = 'Returns the full articles instead of only the intro';
|
||||
const PARAMETERS = array(array(
|
||||
'category' => array(
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'Alle News'
|
||||
=> 'https://rss.golem.de/rss.php?feed=ATOM1.0',
|
||||
'Audio/Video'
|
||||
=> 'https://rss.golem.de/rss.php?ms=audio-video&feed=ATOM1.0',
|
||||
'Auto'
|
||||
=> 'https://rss.golem.de/rss.php?ms=auto&feed=ATOM1.0',
|
||||
'Foto'
|
||||
=> 'https://rss.golem.de/rss.php?ms=foto&feed=ATOM1.0',
|
||||
'Games'
|
||||
=> 'https://rss.golem.de/rss.php?ms=games&feed=ATOM1.0',
|
||||
'Handy'
|
||||
=> 'https://rss.golem.de/rss.php?ms=handy&feed=ATOM1.0',
|
||||
'Internet'
|
||||
=> 'https://rss.golem.de/rss.php?ms=internet&feed=ATOM1.0',
|
||||
'Mobil'
|
||||
=> 'https://rss.golem.de/rss.php?ms=mobil&feed=ATOM1.0',
|
||||
'Open Source'
|
||||
=> 'https://rss.golem.de/rss.php?ms=open-source&feed=ATOM1.0',
|
||||
'Politik/Recht'
|
||||
=> 'https://rss.golem.de/rss.php?ms=politik-recht&feed=ATOM1.0',
|
||||
'Security'
|
||||
=> 'https://rss.golem.de/rss.php?ms=security&feed=ATOM1.0',
|
||||
'Desktop-Applikationen'
|
||||
=> 'https://rss.golem.de/rss.php?ms=desktop-applikationen&feed=ATOM1.0',
|
||||
'Software-Entwicklung'
|
||||
=> 'https://rss.golem.de/rss.php?ms=softwareentwicklung&feed=ATOM1.0',
|
||||
'Wirtschaft'
|
||||
=> 'https://rss.golem.de/rss.php?ms=wirtschaft&feed=ATOM1.0',
|
||||
'Wissenschaft'
|
||||
=> 'https://rss.golem.de/rss.php?ms=wissenschaft&feed=ATOM1.0'
|
||||
)
|
||||
),
|
||||
'limit' => array(
|
||||
'name' => 'Limit',
|
||||
'type' => 'number',
|
||||
'required' => false,
|
||||
'title' => 'Specify number of full articles to return',
|
||||
'defaultValue' => 5
|
||||
)
|
||||
));
|
||||
const LIMIT = 5;
|
||||
const HEADERS = array('Cookie: golem_consent20=simple|220101;');
|
||||
|
||||
public function collectData() {
|
||||
$this->collectExpandableDatas(
|
||||
$this->getInput('category'),
|
||||
$this->getInput('limit') ?: static::LIMIT
|
||||
);
|
||||
}
|
||||
|
||||
protected function parseItem($item) {
|
||||
$item = parent::parseItem($item);
|
||||
$item['content'] = $item['content'] ?? '';
|
||||
$uri = $item['uri'];
|
||||
|
||||
while ($uri) {
|
||||
$articlePage = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT, static::HEADERS);
|
||||
|
||||
// URI without RSS feed reference
|
||||
$item['uri'] = $articlePage->find('head meta[name="twitter:url"]', 0)->content;
|
||||
|
||||
$author = $articlePage->find('article header .authors .authors__name', 0);
|
||||
if ($author) {
|
||||
$item['author'] = $author->innertext;
|
||||
}
|
||||
|
||||
$item['content'] .= $this->extractContent($articlePage);
|
||||
|
||||
// next page
|
||||
$nextUri = $articlePage->find('link[rel="next"]', 0);
|
||||
$uri = $nextUri ? static::URI . $nextUri->href : null;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function extractContent($page) {
|
||||
$item = '';
|
||||
|
||||
$article = $page->find('article', 0);
|
||||
|
||||
// delete known bad elements
|
||||
foreach($article->find('div[id*="adtile"], #job-market, #seminars,
|
||||
div.gbox_affiliate, div.toc, .embedcontent') as $bad) {
|
||||
$bad->remove();
|
||||
}
|
||||
// reload html, as remove() is buggy
|
||||
$article = str_get_html($article->outertext);
|
||||
|
||||
if ($pageHeader = $article->find('header.paged-cluster-header h1', 0)) {
|
||||
$item .= $pageHeader;
|
||||
}
|
||||
|
||||
$header = $article->find('header', 0);
|
||||
foreach($header->find('p, figure') as $element) {
|
||||
$item .= $element;
|
||||
}
|
||||
|
||||
$content = $article->find('div.formatted', 0);
|
||||
|
||||
// full image quality
|
||||
foreach($content->find('img[data-src-full][src*="."]') as $img) {
|
||||
$img->src = $img->getAttribute('data-src-full');
|
||||
}
|
||||
|
||||
foreach($content->find('p, h1, h3, img[src*="."]') as $element) {
|
||||
$item .= $element;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
95
bridges/GoodreadsBridge.php
Normal file
95
bridges/GoodreadsBridge.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
class GoodreadsBridge extends BridgeAbstract {
|
||||
|
||||
const MAINTAINER = 'captn3m0';
|
||||
const NAME = 'Goodreads Bridge';
|
||||
const URI = 'https://www.goodreads.com/';
|
||||
const CACHE_TIMEOUT = 0; // 30min
|
||||
const DESCRIPTION = 'Various RSS feeds from Goodreads';
|
||||
|
||||
const CONTEXT_AUTHOR_BOOKS = 'Books by Author';
|
||||
|
||||
// Using a specific context because I plan to expand this soon
|
||||
const PARAMETERS = array(
|
||||
'Books by Author' => array(
|
||||
'author_url' => array(
|
||||
'name' => 'Link to author\'s page on Goodreads',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'title' => 'Should look somewhat like goodreads.com/author/show/',
|
||||
'pattern' => '^(https:\/\/)?(www.)?goodreads\.com\/author\/show\/\d+\..*$',
|
||||
'exampleValue' => 'https://www.goodreads.com/author/show/38550.Brandon_Sanderson'
|
||||
),
|
||||
'published_only' => array(
|
||||
'name' => 'Show published books only',
|
||||
'type' => 'checkbox',
|
||||
'required' => false,
|
||||
'title' => 'If left unchecked, this will return unpublished books as well',
|
||||
'defaultValue' => 'checked',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
private function collectAuthorBooks($url) {
|
||||
|
||||
$regex = '/goodreads\.com\/author\/show\/(\d+)/';
|
||||
|
||||
preg_match($regex, $url, $matches);
|
||||
|
||||
$authorId = $matches[1];
|
||||
|
||||
$authorListUrl = "https://www.goodreads.com/author/list/$authorId?sort=original_publication_year";
|
||||
|
||||
$html = getSimpleHTMLDOMCached($authorListUrl, self::CACHE_TIMEOUT);
|
||||
|
||||
foreach($html->find('tr[itemtype="http://schema.org/Book"]') as $row) {
|
||||
$dateSpan = $row->find('.uitext', 0)->plaintext;
|
||||
$date = null;
|
||||
|
||||
// If book is not yet published, ignore for now
|
||||
if(preg_match('/published\s+(\d{4})/', $dateSpan, $matches) === 1) {
|
||||
// Goodreads doesn't give us exact publication date here, only a year
|
||||
// We are skipping future dates anyway, so this is def published
|
||||
// but we can't pick a dynamic date either to keep clients from getting
|
||||
// confused. So we pick a guaranteed date of 1st-Jan instead.
|
||||
$date = $matches[1] . '-01-01';
|
||||
} else if ($this->getInput('published_only') !== 'checked') {
|
||||
// We can return unpublished books as well
|
||||
$date = date('Y-01-01');
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = defaultLinkTo($row, $this->getURI());
|
||||
|
||||
$item['title'] = $row->find('.bookTitle', 0)->plaintext;
|
||||
$item['uri'] = $row->find('.bookTitle', 0)->getAttribute('href');
|
||||
$item['author'] = $row->find('.authorName', 0)->plaintext;
|
||||
$item['content'] = '<a href="'
|
||||
. $row->find('.bookTitle', 0)->getAttribute('href')
|
||||
. '"><img src="'
|
||||
. $row->find('.bookCover', 0)->getAttribute('src')
|
||||
. '"></a>';
|
||||
$item['timestamp'] = $date;
|
||||
$item['enclosures'] = array(
|
||||
$row->find('.bookCover', 0)->getAttribute('src')
|
||||
);
|
||||
|
||||
$this->items[] = $item; // Add item to the list
|
||||
}
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
|
||||
switch ($this->queriedContext) {
|
||||
case self::CONTEXT_AUTHOR_BOOKS:
|
||||
$this->collectAuthorBooks($this->getInput('author_url'));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Invalid context', 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,25 +24,35 @@ class GoogleSearchBridge extends BridgeAbstract {
|
||||
));
|
||||
|
||||
public function collectData(){
|
||||
$html = '';
|
||||
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
$header = array('Accept-language: en-US');
|
||||
$html = getSimpleHTMLDOM($this->getURI(), $header)
|
||||
or returnServerError('No results for this query.');
|
||||
|
||||
$emIsRes = $html->find('div[id=res]', 0);
|
||||
|
||||
if(!is_null($emIsRes)) {
|
||||
foreach($emIsRes->find('div[class=g]') as $element) {
|
||||
|
||||
foreach($emIsRes->find('div[class~=g]') as $element) {
|
||||
$item = array();
|
||||
|
||||
$t = $element->find('a[href]', 0)->href;
|
||||
$item['uri'] = htmlspecialchars_decode($t);
|
||||
$item['title'] = $element->find('h3', 0)->plaintext;
|
||||
$item['content'] = $element->find('span[class=aCOpRe]', 0)->plaintext;
|
||||
$resultComponents = explode(' — ', $element->find('div[data-content-feature=1]', 0)->plaintext);
|
||||
$item['content'] = $resultComponents[1];
|
||||
|
||||
if(strpos($resultComponents[0], 'day') === true) {
|
||||
$daysago = explode(' ', $resultComponents[0])[0];
|
||||
$item['timestamp'] = date('d M Y', strtotime('-' . $daysago . ' days'));
|
||||
} else {
|
||||
$item['timestamp'] = $resultComponents[0];
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
usort($this->items, function($a, $b) {
|
||||
return $a['timestamp'] < $b['timestamp'];
|
||||
});
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
@@ -50,7 +60,7 @@ class GoogleSearchBridge extends BridgeAbstract {
|
||||
return self::URI
|
||||
. 'search?q='
|
||||
. urlencode($this->getInput('q'))
|
||||
. '&num=100&complete=0&tbs=qdr:y,sbd:1';
|
||||
. '&hl=en&num=100&complete=0&tbs=qdr:y,sbd:1';
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
|
@@ -1,4 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Uses the API as documented here:
|
||||
* https://haveibeenpwned.com/API/v3#AllBreaches
|
||||
*
|
||||
* Gets the latest breaches by the date of the breach or when it was added to
|
||||
* HIBP.
|
||||
* */
|
||||
class HaveIBeenPwnedBridge extends BridgeAbstract {
|
||||
const NAME = 'Have I Been Pwned (HIBP) Bridge';
|
||||
const URI = 'https://haveibeenpwned.com';
|
||||
@@ -21,52 +28,41 @@ class HaveIBeenPwnedBridge extends BridgeAbstract {
|
||||
'defaultValue' => 20,
|
||||
)
|
||||
));
|
||||
const API_URI = 'https://haveibeenpwned.com/api/v3';
|
||||
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
|
||||
private $breachDateRegex = '/Breach date: ([0-9]{1,2} [A-Z-a-z]+ [0-9]{4})/';
|
||||
private $dateAddedRegex = '/Date added to HIBP: ([0-9]{1,2} [A-Z-a-z]+ [0-9]{4})/';
|
||||
private $accountsRegex = '/Compromised accounts: ([0-9,]+)/';
|
||||
|
||||
private $breaches = array();
|
||||
|
||||
public function collectData() {
|
||||
|
||||
$html = getSimpleHTMLDOM(self::URI . '/PwnedWebsites');
|
||||
$data = json_decode(getContents(self::API_URI . '/breaches'), true);
|
||||
|
||||
$breaches = array();
|
||||
|
||||
foreach($html->find('div.row') as $breach) {
|
||||
foreach($data as $breach) {
|
||||
$item = array();
|
||||
|
||||
if ($breach->class != 'row') {
|
||||
continue;
|
||||
}
|
||||
$pwnCount = number_format($breach['PwnCount']);
|
||||
$item['title'] = $breach['Title'] . ' - '
|
||||
. $pwnCount . ' breached accounts';
|
||||
$item['dateAdded'] = $breach['AddedDate'];
|
||||
$item['breachDate'] = $breach['BreachDate'];
|
||||
$item['uri'] = self::URI . '/PwnedWebsites#' . $breach['Name'];
|
||||
|
||||
preg_match($this->breachDateRegex, $breach->find('p', 1)->plaintext, $breachDate)
|
||||
or returnServerError('Could not extract details');
|
||||
|
||||
preg_match($this->dateAddedRegex, $breach->find('p', 1)->plaintext, $dateAdded)
|
||||
or returnServerError('Could not extract details');
|
||||
|
||||
preg_match($this->accountsRegex, $breach->find('p', 1)->plaintext, $accounts)
|
||||
or returnServerError('Could not extract details');
|
||||
|
||||
$permalink = $breach->find('p', 1)->find('a', 0)->href;
|
||||
|
||||
// Remove permalink
|
||||
$breach->find('p', 1)->find('a', 0)->outertext = '';
|
||||
|
||||
$item['title'] = html_entity_decode($breach->find('h3', 0)->plaintext, ENT_QUOTES)
|
||||
. ' - ' . $accounts[1] . ' breached accounts';
|
||||
$item['dateAdded'] = strtotime($dateAdded[1]);
|
||||
$item['breachDate'] = strtotime($breachDate[1]);
|
||||
$item['uri'] = self::URI . '/PwnedWebsites' . $permalink;
|
||||
|
||||
$item['content'] = '<p>' . $breach->find('p', 0)->innertext . '</p>';
|
||||
$item['content'] = '<p>' . $breach['Description'] . '</p>';
|
||||
$item['content'] .= '<p>' . $this->breachType($breach) . '</p>';
|
||||
$item['content'] .= '<p>' . $breach->find('p', 1)->innertext . '</p>';
|
||||
|
||||
$breachDate = date('j F Y', strtotime($breach['BreachDate']));
|
||||
$addedDate = date('j F Y', strtotime($breach['AddedDate']));
|
||||
$compData = implode(', ', $breach['DataClasses']);
|
||||
|
||||
$item['content'] .= <<<EOD
|
||||
<p>
|
||||
<strong>Breach date:</strong> {$breachDate}<br>
|
||||
<strong>Date added to HIBP:</strong> {$addedDate}<br>
|
||||
<strong>Compromised accounts:</strong> {$pwnCount}<br>
|
||||
<strong>Compromised data:</strong> {$compData}<br>
|
||||
EOD;
|
||||
$item['uid'] = $breach['Name'];
|
||||
$this->breaches[] = $item;
|
||||
}
|
||||
|
||||
@@ -74,6 +70,27 @@ class HaveIBeenPwnedBridge extends BridgeAbstract {
|
||||
$this->createItems();
|
||||
}
|
||||
|
||||
private const BREACH_TYPES = array(
|
||||
'IsVerified' => array(
|
||||
false => 'Unverified breach, may be sourced from elsewhere'
|
||||
),
|
||||
'IsFabricated' => array(
|
||||
true => 'Fabricated breach, likely not legitimate'
|
||||
),
|
||||
'IsSensitive' => array(
|
||||
true => 'Sensitive breach, not publicly searchable'
|
||||
),
|
||||
'IsRetired' => array(
|
||||
true => 'Retired breach, removed from system'
|
||||
),
|
||||
'IsSpamList' => array(
|
||||
true => 'Spam list, used for spam marketing'
|
||||
),
|
||||
'IsMalware' => array(
|
||||
true => 'Malware breach'
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Extract data breach type(s)
|
||||
*/
|
||||
@@ -81,12 +98,10 @@ class HaveIBeenPwnedBridge extends BridgeAbstract {
|
||||
|
||||
$content = '';
|
||||
|
||||
if ($breach->find('h3 > i', 0)) {
|
||||
|
||||
foreach ($breach->find('h3 > i') as $i) {
|
||||
$content .= $i->title . '.<br>';
|
||||
foreach (self::BREACH_TYPES as $type => $message) {
|
||||
if (isset($message[$breach[$type]])) {
|
||||
$content .= $message[$breach[$type]] . '.<br>';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $content;
|
||||
@@ -127,6 +142,7 @@ class HaveIBeenPwnedBridge extends BridgeAbstract {
|
||||
$item['timestamp'] = $breach[$this->getInput('order')];
|
||||
$item['uri'] = $breach['uri'];
|
||||
$item['content'] = $breach['content'];
|
||||
$item['uid'] = $breach['uid'];
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/DealabsBridge.php');
|
||||
class HotUKDealsBridge extends PepperBridgeAbstract {
|
||||
|
||||
const NAME = 'HotUKDeals bridge';
|
||||
@@ -3253,7 +3252,7 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
|
||||
'name' => 'Discussion URL',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'title' => 'Discussion URL to monitor. Ex: https://www.hotukdeals.com/discussions/the-hukd-lego-thread-3599357',
|
||||
'title' => 'Discussion URL to monitor. Ex: https://www.hotukdeals.com/discussions/title-123',
|
||||
'exampleValue' => 'https://www.hotukdeals.com/discussions/the-hukd-lego-thread-3599357',
|
||||
),
|
||||
'only_with_url' => array(
|
||||
@@ -3306,7 +3305,7 @@ class HotUKDealsBridge extends PepperBridgeAbstract {
|
||||
'th'
|
||||
),
|
||||
'local-time-relative' => array(
|
||||
'Found ',
|
||||
'Posted ',
|
||||
'm',
|
||||
'h,',
|
||||
'day',
|
||||
|
@@ -160,6 +160,11 @@ class InstagramBridge extends BridgeAbstract {
|
||||
$mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
|
||||
}
|
||||
|
||||
$pattern = array('/\@([\w\.]+)/', '/#([\w\.]+)/');
|
||||
$replace = array(
|
||||
'<a href="https://www.instagram.com/$1">@$1</a>',
|
||||
'<a href="https://www.instagram.com/explore/tags/$1">#$1</a>');
|
||||
|
||||
switch($media->__typename) {
|
||||
case 'GraphSidecar':
|
||||
$data = $this->getInstagramSidecarData($item['uri'], $item['title'], $media, $textContent);
|
||||
@@ -169,7 +174,7 @@ class InstagramBridge extends BridgeAbstract {
|
||||
case 'GraphImage':
|
||||
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
|
||||
$item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
|
||||
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
|
||||
$item['content'] .= '</a><br><br>' . nl2br(preg_replace($pattern, $replace, htmlentities($textContent)));
|
||||
$item['enclosures'] = array($mediaURI);
|
||||
break;
|
||||
case 'GraphVideo':
|
||||
|
@@ -23,7 +23,8 @@ class InternetArchiveBridge extends BridgeAbstract {
|
||||
'Web Archives' => 'web-archive',
|
||||
),
|
||||
'defaultValue' => 'uploads',
|
||||
)
|
||||
),
|
||||
'limit' => self::LIMIT,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -72,7 +73,8 @@ class InternetArchiveBridge extends BridgeAbstract {
|
||||
if ($this->getInput('content') !== 'posts') {
|
||||
$detailsDivNumber = 0;
|
||||
|
||||
foreach ($html->find('div.results > div[data-id]') as $index => $result) {
|
||||
$results = $html->find('div.results > div[data-id]');
|
||||
foreach ($results as $index => $result) {
|
||||
$item = array();
|
||||
|
||||
if (in_array($result->class, $this->skipClasses)) {
|
||||
@@ -110,6 +112,11 @@ class InternetArchiveBridge extends BridgeAbstract {
|
||||
}
|
||||
|
||||
$detailsDivNumber++;
|
||||
|
||||
$limit = $this->getInput('limit') ?? 10;
|
||||
if (count($this->items) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +309,7 @@ EOD;
|
||||
|
||||
$items[] = $item;
|
||||
|
||||
if (count($items) >= 10) {
|
||||
if (count($items) >= $this->getInput('limit') ?? 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -12,17 +12,23 @@ class KhinsiderBridge extends BridgeAbstract
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
|
||||
$dates = $html->find('#EchoTopic h3');
|
||||
foreach ($dates as $date) {
|
||||
$dates = $html->find('.latestSoundtrackHeading');
|
||||
$tables = $html->find('.albumList');
|
||||
// $dates is empty
|
||||
foreach ($dates as $i => $date) {
|
||||
$item = array();
|
||||
$item['uri'] = self::URI;
|
||||
$item['timestamp'] = DateTime::createFromFormat('F jS, Y', $date->plaintext)->format('U');
|
||||
$item['timestamp'] = DateTime::createFromFormat('F jS, Y', $date->plaintext)->setTime(1, 1)->format('U');
|
||||
$item['title'] = sprintf('OST for %s', $date->plaintext);
|
||||
$item['author'] = 'Khinsider';
|
||||
$links = $date->next_sibling()->find('a');
|
||||
$trs = $tables[$i]->find('tr');
|
||||
$content = '<ul>';
|
||||
foreach ($links as $link) {
|
||||
$content .= sprintf('<li><a href="%s">%s</a></li>', $link->href, $link->plaintext);
|
||||
foreach ($trs as $tr) {
|
||||
$td = $tr->find('td', 1);
|
||||
if (null !== $td) {
|
||||
$link = $td->find('a', 0);
|
||||
$content .= sprintf('<li><a href="%s">%s</a></li>', $link->href, $link->plaintext);
|
||||
}
|
||||
}
|
||||
$content .= '</ul>';
|
||||
$item['content'] = $content;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('MoebooruBridge.php');
|
||||
|
||||
class KonachanBridge extends MoebooruBridge {
|
||||
|
||||
|
@@ -17,7 +17,8 @@ class KununuBridge extends BridgeAbstract {
|
||||
'Germany' => 'de',
|
||||
'Switzerland' => 'ch',
|
||||
'United States' => 'us'
|
||||
)
|
||||
),
|
||||
'exampleValue' => 'de',
|
||||
),
|
||||
'full' => array(
|
||||
'name' => 'Load full article',
|
||||
@@ -46,7 +47,7 @@ class KununuBridge extends BridgeAbstract {
|
||||
'company' => array(
|
||||
'name' => 'Company',
|
||||
'required' => true,
|
||||
'exampleValue' => 'kununu-us',
|
||||
'exampleValue' => 'adesso',
|
||||
'title' => 'Insert company name (i.e. Kununu US) or URI path (i.e. kununu-us)'
|
||||
)
|
||||
)
|
||||
@@ -72,7 +73,8 @@ class KununuBridge extends BridgeAbstract {
|
||||
break;
|
||||
}
|
||||
|
||||
return self::URI . $site . '/' . $company . '/' . $section . '?sort=update_time_desc';
|
||||
$url = sprintf('%s%s/%s/%s?sort=update_time_desc', self::URI, $site, $company, $section);
|
||||
return $url;
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
@@ -91,6 +93,9 @@ class KununuBridge extends BridgeAbstract {
|
||||
return 'https://www.kununu.com/favicon-196x196.png';
|
||||
}
|
||||
|
||||
/**
|
||||
* All css selectors need rework
|
||||
*/
|
||||
public function collectData(){
|
||||
$full = $this->getInput('full');
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
class LaCentraleBridge extends BridgeAbstract {
|
||||
|
||||
const MAINTAINER = 'jacknumber';
|
||||
@@ -414,9 +415,9 @@ class LaCentraleBridge extends BridgeAbstract {
|
||||
));
|
||||
|
||||
public function collectData(){
|
||||
// check data
|
||||
if(!empty($this->getInput('distance'))
|
||||
&& is_null($this->getInput('location'))) {
|
||||
&& is_null($this->getInput('location'))
|
||||
) {
|
||||
returnClientError('You need a place ("CP ou département") to search arround.');
|
||||
}
|
||||
|
||||
@@ -442,35 +443,31 @@ class LaCentraleBridge extends BridgeAbstract {
|
||||
'doors' => $this->getInput('doors'),
|
||||
'sortBy' => $this->getInput('sort')
|
||||
);
|
||||
$url = self::URI . 'listing?' . http_build_query($params);
|
||||
$url = sprintf('%slisting?%s', self::URI, http_build_query($params));
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
foreach($html->find('.linkAd') as $element) {
|
||||
$elements = $html->find('.adLineContainer');
|
||||
foreach($elements as $element) {
|
||||
|
||||
$item = array();
|
||||
$item['uri'] = trim(self::URI, '/') . $element->href;
|
||||
$item['title'] = $element->find('.brandModel', 0)->plaintext;
|
||||
$item['sellerType'] = $element->find('.typeSeller', 0)->plaintext;
|
||||
$item['uri'] = trim(self::URI, '/') . $element->find('div > a', 0)->href;
|
||||
$item['title'] = $element->find('.searchCard__makeModel', 0)->plaintext;
|
||||
$item['sellerType'] = $element->find('.searchCard__customer', 0)->plaintext;
|
||||
$item['author'] = $item['sellerType'];
|
||||
$item['version'] = $element->find('.version', 0)->plaintext;
|
||||
$item['price'] = $element->find('.fieldPrice', 0)->plaintext;
|
||||
$item['year'] = $element->find('.fieldYear', 0)->plaintext;
|
||||
$item['mileage'] = $element->find('.fieldMileage', 0)->plaintext;
|
||||
$item['departement'] = str_replace(',', '', $element->find('.dptCont', 0)->plaintext);
|
||||
$item['thumbnail'] = $element->find('.imgContent img', 0)->src;
|
||||
$item['enclosures'] = array($item['thumbnail']);
|
||||
$item['version'] = $element->find('.searchCard__version', 0)->plaintext;
|
||||
$item['price'] = $element->find('.searchCard__fieldPrice', 0)->plaintext;
|
||||
$item['year'] = $element->find('.searchCard__year', 0)->plaintext;
|
||||
$item['mileage'] = $element->find('.searchCard__mileage', 0)->plaintext;
|
||||
// The image is lazyloaded with ajax
|
||||
|
||||
$item['content'] = '
|
||||
<img src="' . $item['thumbnail'] . '">
|
||||
<br>Variation : ' . $item['version']
|
||||
. '<br>Prix : ' . $item['price']
|
||||
. '<br>Année : ' . $item['year']
|
||||
. '<br>Kilométrage : ' . $item['mileage']
|
||||
. '<br>Département : ' . $item['departement']
|
||||
. '<br>Type de vendeur : ' . $item['sellerType'];
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ class LegifranceJOBridge extends BridgeAbstract {
|
||||
|
||||
const MAINTAINER = 'Pierre Mazière';
|
||||
const NAME = 'Journal Officiel de la République Française';
|
||||
// This uri returns a snippet of js. Should probably be https://www.legifrance.gouv.fr/jorf/jo/
|
||||
const URI = 'https://www.legifrance.gouv.fr/affichJO.do';
|
||||
const DESCRIPTION = 'Returns the laws and decrees officially registered daily in France';
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('MoebooruBridge.php');
|
||||
|
||||
class LolibooruBridge extends MoebooruBridge {
|
||||
|
||||
|
@@ -15,7 +15,7 @@ class MangaDexBridge extends BridgeAbstract {
|
||||
'required' => true
|
||||
),
|
||||
'lang' => array(
|
||||
'name' => 'Chapter Languages',
|
||||
'name' => 'Chapter Languages (default=all)',
|
||||
'title' => 'comma-separated, two-letter language codes (example "en,jp")',
|
||||
'exampleValue' => 'en,jp',
|
||||
'required' => false
|
||||
@@ -32,7 +32,39 @@ class MangaDexBridge extends BridgeAbstract {
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Some chapters are inaccessible or only available on an external site. Include these?'
|
||||
)
|
||||
),
|
||||
'Search Chapters' => array(
|
||||
'chapter' => array(
|
||||
'name' => 'Chapter Number (default=all)',
|
||||
'title' => 'The example value finds the newest first chapters',
|
||||
'exampleValue' => 1,
|
||||
'required' => false
|
||||
),
|
||||
'groups' => array(
|
||||
'name' => 'Group UUID (default=all)',
|
||||
'title' => 'This can be found in the MangaDex Group Page URL',
|
||||
'exampleValue' => '00e03853-1b96-4f41-9542-c71b8692033b',
|
||||
'required' => false,
|
||||
),
|
||||
'uploader' => array(
|
||||
'name' => 'User UUID (default=all)',
|
||||
'title' => 'This can be found in the MangaDex User Page URL',
|
||||
'exampleValue' => 'd2ae45e0-b5e2-4e7f-a688-17925c2d7d6b',
|
||||
'required' => false,
|
||||
),
|
||||
'external' => array(
|
||||
'name' => 'Allow external feed items',
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Some chapters are inaccessible or only available on an external site. Include these?'
|
||||
)
|
||||
)
|
||||
// Future Manga Contexts:
|
||||
// Manga List (by author or tags): https://api.mangadex.org/swagger.html#/Manga/get-search-manga
|
||||
// Random Manga: https://api.mangadex.org/swagger.html#/Manga/get-manga-random
|
||||
// Future Chapter Contexts:
|
||||
// User Lists https://api.mangadex.org/swagger.html#/Feed/get-list-id-feed
|
||||
//
|
||||
// https://api.mangadex.org/docs/get-covers/
|
||||
);
|
||||
|
||||
const TITLE_REGEX = '#title/(?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})#';
|
||||
@@ -70,10 +102,26 @@ class MangaDexBridge extends BridgeAbstract {
|
||||
$array_params['includes[]'] = array('manga', 'scanlation_group', 'user');
|
||||
$uri = self::API_ROOT . 'manga/' . $matches['uuid'] . '/feed';
|
||||
break;
|
||||
case 'Search Chapters':
|
||||
$params['chapter'] = $this->getInput('chapter');
|
||||
$params['groups[]'] = $this->getInput('groups');
|
||||
$params['uploader'] = $this->getInput('uploader');
|
||||
$params['order[updatedAt]'] = 'desc';
|
||||
if (!$this->getInput('external')) {
|
||||
$params['includeFutureUpdates'] = '0';
|
||||
}
|
||||
$array_params['includes[]'] = array('manga', 'scanlation_group', 'user');
|
||||
$uri = self::API_ROOT . 'chapter';
|
||||
break;
|
||||
default:
|
||||
returnServerError('Unimplemented Context (getAPI)');
|
||||
}
|
||||
|
||||
// Remove null keys
|
||||
$params = array_filter($params, function($v) {
|
||||
return !empty($v);
|
||||
});
|
||||
|
||||
$uri .= '?' . http_build_query($params);
|
||||
|
||||
// Arrays are passed as repeated keys to MangaDex
|
||||
@@ -83,13 +131,14 @@ class MangaDexBridge extends BridgeAbstract {
|
||||
}
|
||||
|
||||
return $uri;
|
||||
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
switch($this->queriedContext) {
|
||||
case 'Title Chapters':
|
||||
return $this->feedName . ' Chapters';
|
||||
case 'Search Chapters':
|
||||
return 'MangaDex Chapter Search';
|
||||
default:
|
||||
return parent::getName();
|
||||
}
|
||||
@@ -105,7 +154,7 @@ class MangaDexBridge extends BridgeAbstract {
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$api_uri = $this->getApi();
|
||||
$api_uri = $this->getAPI();
|
||||
$header = array(
|
||||
'Content-Type: application/json'
|
||||
);
|
||||
@@ -120,6 +169,9 @@ class MangaDexBridge extends BridgeAbstract {
|
||||
case 'Title Chapters':
|
||||
$this->getChapters($content);
|
||||
break;
|
||||
case 'Search Chapters':
|
||||
$this->getChapters($content);
|
||||
break;
|
||||
default:
|
||||
returnServerError('Unimplemented Context (collectData)');
|
||||
}
|
||||
@@ -131,8 +183,15 @@ class MangaDexBridge extends BridgeAbstract {
|
||||
$item['uid'] = $chapter['id'];
|
||||
$item['uri'] = self::URI . 'chapter/' . $chapter['id'];
|
||||
|
||||
// Preceding space accounts for Manga title added later
|
||||
$item['title'] = ' Chapter ' . $chapter['attributes']['chapter'];
|
||||
// External chapter
|
||||
if (!$this->getInput('external') && $chapter['attributes']['pages'] == 0)
|
||||
continue;
|
||||
|
||||
$item['title'] = '';
|
||||
if (isset($chapter['attributes']['volume']))
|
||||
$item['title'] .= 'Volume ' . $chapter['attributes']['volume'] . ' ';
|
||||
if (isset($chapter['attributes']['chapter']))
|
||||
$item['title'] .= 'Chapter ' . $chapter['attributes']['chapter'];
|
||||
if (!empty($chapter['attributes']['title'])) {
|
||||
$item['title'] .= ' - ' . $chapter['attributes']['title'];
|
||||
}
|
||||
@@ -148,10 +207,10 @@ class MangaDexBridge extends BridgeAbstract {
|
||||
$groups[] = $rel['attributes']['name'];
|
||||
break;
|
||||
case 'manga':
|
||||
if (empty($this->feedName)) {
|
||||
if (empty($this->feedName))
|
||||
$this->feedName = reset($rel['attributes']['title']);
|
||||
}
|
||||
$item['title'] = reset($rel['attributes']['title']) . $item['title'];
|
||||
if ($this->queriedContext !== 'Title Chapters')
|
||||
$item['title'] = reset($rel['attributes']['title']) . ' ' . $item['title'];
|
||||
break;
|
||||
case 'user':
|
||||
if (isset($item['author'])) {
|
||||
|
@@ -17,12 +17,14 @@ class MarktplaatsBridge extends BridgeAbstract {
|
||||
'name' => 'zipcode',
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
'exampleValue' => '1013AA',
|
||||
'title' => 'Zip code for location limited searches',
|
||||
),
|
||||
'd' => array(
|
||||
'name' => 'distance',
|
||||
'type' => 'number',
|
||||
'required' => false,
|
||||
'exampleValue' => '100000',
|
||||
'title' => 'The distance in meters from the zipcode',
|
||||
),
|
||||
'f' => array(
|
||||
@@ -77,7 +79,7 @@ class MarktplaatsBridge extends BridgeAbstract {
|
||||
}
|
||||
}
|
||||
$url = 'https://www.marktplaats.nl/lrp/api/search?query=' . urlencode($this->getInput('q')) . $query;
|
||||
$jsonString = getSimpleHTMLDOM($url, 900);
|
||||
$jsonString = getSimpleHTMLDOM($url);
|
||||
$jsonObj = json_decode($jsonString);
|
||||
foreach($jsonObj->listings as $listing) {
|
||||
if(!$excludeGlobal || $listing->location->distanceMeters >= 0) {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('Shimmie2Bridge.php');
|
||||
|
||||
class MilbooruBridge extends Shimmie2Bridge {
|
||||
|
||||
|
@@ -20,6 +20,8 @@ class MsnMondeBridge extends BridgeAbstract {
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
$limit = 0;
|
||||
|
||||
// TODO: fix why articles is empty
|
||||
foreach($html->find('.smalla') as $article) {
|
||||
if($limit < 10) {
|
||||
$item = array();
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('GelbooruBridge.php');
|
||||
|
||||
class MspabooruBridge extends GelbooruBridge {
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/DealabsBridge.php');
|
||||
class MydealsBridge extends PepperBridgeAbstract {
|
||||
|
||||
const NAME = 'Mydeals bridge';
|
||||
@@ -12,7 +11,7 @@ class MydealsBridge extends PepperBridgeAbstract {
|
||||
'q' => array(
|
||||
'name' => 'Stichworten',
|
||||
'type' => 'text',
|
||||
'exampleValue' => 'watch',
|
||||
'exampleValue' => 'lamp',
|
||||
'required' => true
|
||||
),
|
||||
'hide_expired' => array(
|
||||
@@ -2002,8 +2001,8 @@ class MydealsBridge extends PepperBridgeAbstract {
|
||||
'name' => 'URL der Diskussion',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'title' => 'URL-Diskussion zu überwachen: https://www.mydealz.de/diskussion/title-123',
|
||||
'exampleValue' => '://www.mydealz.de/diskussion/title-123',
|
||||
'title' => 'URL-Diskussion zu überwachen: https://www.mydealz.de/diskussion/title-123',
|
||||
'exampleValue' => 'https://www.mydealz.de/diskussion/anleitung-wie-schreibe-ich-einen-deal-1658317',
|
||||
),
|
||||
'only_with_url' => array(
|
||||
'name' => 'Kommentare ohne URL ausschließen',
|
||||
|
@@ -8,28 +8,36 @@ class N26Bridge extends BridgeAbstract
|
||||
const CACHE_TIMEOUT = 1800;
|
||||
const DESCRIPTION = 'Returns recent blog posts from N26.';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$limit = 5;
|
||||
$url = 'https://n26.com/en-eu/blog/all';
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
$articles = $html->find('div[class="bl bm"]');
|
||||
|
||||
foreach($articles as $article) {
|
||||
$item = array();
|
||||
|
||||
$itemUrl = self::URI . $article->find('a', 1)->href;
|
||||
$item['uri'] = $itemUrl;
|
||||
|
||||
$item['title'] = $article->find('a', 1)->plaintext;
|
||||
|
||||
$fullArticle = getSimpleHTMLDOM($item['uri']);
|
||||
|
||||
$createdAt = $fullArticle->find('time', 0);
|
||||
$item['timestamp'] = strtotime($createdAt->plaintext);
|
||||
|
||||
$this->items[] = $item;
|
||||
if (count($this->items) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://n26.com/favicon.ico';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI . '/en-eu/blog-archive');
|
||||
|
||||
foreach($html->find('div[class="ag ah ai aj bs bt dx ea fo gx ie if ih ii ij ik s"]') as $article) {
|
||||
$item = array();
|
||||
|
||||
$item['uri'] = self::URI . $article->find('h2 a', 0)->href;
|
||||
$item['title'] = $article->find('h2 a', 0)->plaintext;
|
||||
|
||||
$fullArticle = getSimpleHTMLDOM($item['uri']);
|
||||
|
||||
$dateElement = $fullArticle->find('time', 0);
|
||||
$item['timestamp'] = strtotime($dateElement->plaintext);
|
||||
$item['content'] = $fullArticle->find('div[class="af ag ah ai an"]', 1)->innertext;
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
<?php
|
||||
|
||||
class NFLRUSBridge extends BridgeAbstract {
|
||||
|
||||
const NAME = 'NFLRUS';
|
||||
@@ -6,52 +7,19 @@ class NFLRUSBridge extends BridgeAbstract {
|
||||
const DESCRIPTION = 'Returns the recent articles published on nflrus.ru';
|
||||
const MAINTAINER = 'Maxim Shpak';
|
||||
|
||||
private function getEnglishMonth($month) {
|
||||
$months = array(
|
||||
'Января' => 'January',
|
||||
'Февраля' => 'February',
|
||||
'Марта' => 'March',
|
||||
'Апреля' => 'April',
|
||||
'Мая' => 'May',
|
||||
'Июня' => 'June',
|
||||
'Июля' => 'July',
|
||||
'Августа' => 'August',
|
||||
'Сентября' => 'September',
|
||||
'Октября' => 'October',
|
||||
'Ноября' => 'November',
|
||||
'Декабря' => 'December',
|
||||
);
|
||||
|
||||
if (isset($months[$month])) {
|
||||
return $months[$month];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function extractArticleTimestamp($article) {
|
||||
$time = $article->find('time', 0);
|
||||
if($time) {
|
||||
$timestring = trim($time->plaintext);
|
||||
$parts = explode(' ', $timestring);
|
||||
$month = $this->getEnglishMonth($parts[1]);
|
||||
if ($month) {
|
||||
$timestring = $parts[0] . ' ' . $month . ' ' . $parts[2];
|
||||
return strtotime($timestring);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
foreach($html->find('article') as $article) {
|
||||
$articles = $html->find('.big-post_content-col');
|
||||
|
||||
foreach($articles as $article) {
|
||||
$item = array();
|
||||
$item['uri'] = $article->find('.b-article__title a', 0)->href;
|
||||
$item['title'] = $article->find('.b-article__title a', 0)->plaintext;
|
||||
$item['author'] = $article->find('.link-author', 0)->plaintext;
|
||||
$item['timestamp'] = $this->extractArticleTimestamp($article);
|
||||
|
||||
$url = $article->find('.big-post_title.card-title a', 0);
|
||||
|
||||
$item['uri'] = $url->href;
|
||||
$item['title'] = $url->plaintext;
|
||||
$item['content'] = $article->find('div', 0)->innertext;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
class NeuviemeArtBridge extends FeedExpander {
|
||||
|
||||
const MAINTAINER = 'ORelio';
|
||||
const NAME = '9ème Art Bridge';
|
||||
const URI = 'http://www.9emeart.fr/';
|
||||
const DESCRIPTION = 'Returns the newest articles.';
|
||||
|
||||
protected function parseItem($item){
|
||||
$item = parent::parseItem($item);
|
||||
|
||||
$article_html = getSimpleHTMLDOMCached($item['uri']);
|
||||
if(!$article_html) {
|
||||
$item['content'] = 'Could not request 9eme Art: ' . $item['uri'];
|
||||
return $item;
|
||||
}
|
||||
|
||||
$article_image = '';
|
||||
foreach ($article_html->find('img.img_full') as $img) {
|
||||
if ($img->alt == $item['title']) {
|
||||
$article_image = self::URI . $img->src;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$article_content = '';
|
||||
if ($article_image) {
|
||||
$article_content = '<p><img src="' . $article_image . '" /></p>';
|
||||
}
|
||||
$article_content .= str_replace(
|
||||
'src="/', 'src="' . self::URI,
|
||||
$article_html->find('div.newsGenerique_con', 0)->innertext
|
||||
);
|
||||
$article_content = stripWithDelimiters($article_content, '<script', '</script>');
|
||||
$article_content = stripWithDelimiters($article_content, '<style', '</style>');
|
||||
$article_content = stripWithDelimiters($article_content, '<link', '>');
|
||||
|
||||
$item['content'] = $article_content;
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function collectData(){
|
||||
$feedUrl = self::URI . '9emeart.rss';
|
||||
$this->collectExpandableDatas($feedUrl);
|
||||
}
|
||||
}
|
@@ -55,7 +55,8 @@ class NextInpactBridge extends FeedExpander {
|
||||
'Hide Brief' => '1',
|
||||
'Only Brief' => '2'
|
||||
)
|
||||
)
|
||||
),
|
||||
'limit' => self::LIMIT,
|
||||
));
|
||||
|
||||
public function collectData(){
|
||||
@@ -80,7 +81,9 @@ class NextInpactBridge extends FeedExpander {
|
||||
$feed = 'params';
|
||||
}
|
||||
|
||||
$this->collectExpandableDatas($base_uri . 'rss/' . $feed . '.xml' . $args);
|
||||
$url = sprintf('%srss/%s.xml%s', $base_uri, $feed, $args);
|
||||
$limit = $this->getInput('limit') ?? 10;
|
||||
$this->collectExpandableDatas($url, $limit);
|
||||
}
|
||||
|
||||
protected function parseItem($newsItem){
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
ini_set('max_execution_time', '300');
|
||||
|
||||
class NordbayernBridge extends BridgeAbstract {
|
||||
|
||||
const MAINTAINER = 'schabi.org';
|
||||
@@ -47,82 +47,91 @@ class NordbayernBridge extends BridgeAbstract {
|
||||
)
|
||||
));
|
||||
|
||||
private function getValidImage($picture) {
|
||||
$img = $picture->find('img', 0);
|
||||
if ($img) {
|
||||
$imgUrl = $img->src;
|
||||
if(!preg_match('#/logo-.*\.png#', $imgUrl)) {
|
||||
return '<br><img src="' . $imgUrl . '">';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function getUseFullContent($rawContent) {
|
||||
$content = '';
|
||||
foreach($rawContent->children as $element) {
|
||||
if($element->tag === 'p' || $element->tag === 'h3') {
|
||||
if(($element->tag === 'p' || $element->tag === 'h3') &&
|
||||
$element->class !== 'article__teaser') {
|
||||
$content .= $element;
|
||||
}
|
||||
if($element->tag === 'main') {
|
||||
} else if($element->tag === 'main') {
|
||||
$content .= self::getUseFullContent($element->find('article', 0));
|
||||
}
|
||||
if($element->tag === 'header') {
|
||||
} else if($element->tag === 'header') {
|
||||
$content .= self::getUseFullContent($element);
|
||||
} else if($element->tag === 'div' &&
|
||||
!str_contains($element->class, 'article__infobox') &&
|
||||
!str_contains($element->class, 'authorinfo')) {
|
||||
$content .= self::getUseFullContent($element);
|
||||
} else if($element->tag === 'section' &&
|
||||
(str_contains($element->class, 'article__richtext') ||
|
||||
str_contains($element->class, 'article__context'))) {
|
||||
$content .= self::getUseFullContent($element);
|
||||
} else if($element->tag === 'picture') {
|
||||
$content .= self::getValidImage($element);
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function getValidImages($pictures) {
|
||||
$images = array();
|
||||
if(!empty($pictures)) {
|
||||
for($i = 0; $i < count($pictures); $i++) {
|
||||
$imgUrl = $pictures[$i]->find('img', 0)->src;
|
||||
if(strcmp($imgUrl, 'https://www.nordbayern.de/img/nb/logo-vnp.png') !== 0) {
|
||||
array_push($images, $imgUrl);
|
||||
}
|
||||
}
|
||||
private function getTeaser($content) {
|
||||
$teaser = $content->find('p[class=article__teaser]', 0);
|
||||
if($teaser === null) {
|
||||
return '';
|
||||
}
|
||||
return $images;
|
||||
$teaser = $teaser->plaintext;
|
||||
$teaser = preg_replace('/[ ]{2,}/', ' ', $teaser);
|
||||
$teaser = '<p class="article__teaser">' . $teaser . '</p>';
|
||||
return $teaser;
|
||||
}
|
||||
|
||||
private function handleArticle($link) {
|
||||
$item = array();
|
||||
$article = getSimpleHTMLDOM($link);
|
||||
defaultLinkTo($article, self::URI);
|
||||
|
||||
$content = $article->find('article[id=article]', 0);
|
||||
$item['uri'] = $link;
|
||||
$item['author'] = $article->find('[class=article__author extrabold]', 0)->plaintext;
|
||||
$item['timestamp'] = strtotime(str_replace('Uhr', '', $article->find('[class=article__release]', 0)->plaintext));
|
||||
if ($article->find('h2', 0) == null) {
|
||||
|
||||
$author = $article->find('.article__author', 1);
|
||||
if ($author !== null) {
|
||||
$item['author'] = trim($author->plaintext);
|
||||
}
|
||||
|
||||
$createdAt = $article->find('[class=article__release]', 0);
|
||||
if ($createdAt) {
|
||||
$item['timestamp'] = strtotime(str_replace('Uhr', '', $createdAt->plaintext));
|
||||
}
|
||||
|
||||
if ($article->find('h2', 0) === null) {
|
||||
$item['title'] = $article->find('h3', 0)->innertext;
|
||||
} else {
|
||||
$item['title'] = $article->find('h2', 0)->innertext;
|
||||
}
|
||||
$item['content'] = '';
|
||||
|
||||
//first get images from content
|
||||
$pictures = $article->find('picture');
|
||||
$images = self::getValidImages($pictures);
|
||||
if(!empty($images)) {
|
||||
// If there is an author info block
|
||||
// the first immage will be the portrait of the author
|
||||
// and not the article banner. The banner in this
|
||||
// case will be the second image.
|
||||
// Also skip first image, as its always NN logo.
|
||||
if ($article->find('a[id="openAuthor"]', 0) == null) {
|
||||
$bannerUrl = isset($images[1]) ? $images[1] : null;
|
||||
} else {
|
||||
$bannerUrl = isset($images[2]) ? $images[2] : null;
|
||||
}
|
||||
|
||||
$item['content'] .= '<img src="' . $bannerUrl . '">';
|
||||
}
|
||||
|
||||
if ($article->find('section[class*=article__richtext]', 0) == null) {
|
||||
if ($article->find('section[class*=article__richtext]', 0) === null) {
|
||||
$content = $article->find('div[class*=modul__teaser]', 0)
|
||||
->find('p', 0);
|
||||
$item['content'] .= $content;
|
||||
} else {
|
||||
$content = $article->find('section[class*=article__richtext]', 0)
|
||||
->find('div', 0)->find('div', 0);
|
||||
$content = $article->find('article', 0);
|
||||
// change order of article teaser in order to show it on top
|
||||
// of the title image. If we didn't do this some rss programs
|
||||
// would show the subtitle of the title image as teaser instead
|
||||
// of the actuall article teaser.
|
||||
$item['content'] .= self::getTeaser($content);
|
||||
$item['content'] .= self::getUseFullContent($content);
|
||||
}
|
||||
|
||||
for($i = 1; $i < count($images); $i++) {
|
||||
$item['content'] .= '<img src="' . $images[$i] . '">';
|
||||
}
|
||||
|
||||
// exclude police reports if desired
|
||||
if($this->getInput('policeReports') ||
|
||||
!str_contains($item['content'], 'Hier geht es zu allen aktuellen Polizeimeldungen.')) {
|
||||
@@ -135,17 +144,19 @@ class NordbayernBridge extends BridgeAbstract {
|
||||
private function handleNewsblock($listSite) {
|
||||
$main = $listSite->find('main', 0);
|
||||
foreach($main->find('article') as $article) {
|
||||
self::handleArticle(self::URI . $article->find('a', 0)->href);
|
||||
$url = $article->find('a', 0)->href;
|
||||
$url = urljoin(self::URI, $url);
|
||||
self::handleArticle($url);
|
||||
}
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$item = array();
|
||||
$region = $this->getInput('region');
|
||||
if($region === 'rothenburg-o-d-t') {
|
||||
$region = 'rothenburg-ob-der-tauber';
|
||||
}
|
||||
$listSite = getSimpleHTMLDOM(self::URI . '/region/' . $region);
|
||||
$url = self::URI . '/region/' . $region;
|
||||
$listSite = getSimpleHTMLDOM($url);
|
||||
|
||||
self::handleNewsblock($listSite);
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ class NotAlwaysBridge extends BridgeAbstract {
|
||||
'type' => 'list',
|
||||
'name' => 'Filter',
|
||||
'values' => array(
|
||||
'All' => 'all',
|
||||
'All' => '',
|
||||
'Right' => 'right',
|
||||
'Working' => 'working',
|
||||
'Romantic' => 'romantic',
|
||||
|
95
bridges/NpciBridge.php
Normal file
95
bridges/NpciBridge.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
class NpciBridge extends BridgeAbstract {
|
||||
|
||||
const MAINTAINER = 'captn3m0';
|
||||
const NAME = 'NCPI Circulars';
|
||||
const URI = 'https://npci.org.in';
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
const DESCRIPTION = 'Returns circulars from National Payments Corporation of India)';
|
||||
|
||||
const URL_SUFFIX = [
|
||||
'cts' => 'circulars',
|
||||
'upi' => 'circular',
|
||||
'rupay' => 'circulars',
|
||||
'nach' => 'circulars',
|
||||
'imps' => 'circular',
|
||||
'netc-fastag' => 'circulars',
|
||||
'99' => 'circular',
|
||||
'nfs' => 'circulars',
|
||||
'aeps' => 'circulars',
|
||||
'bhim-aadhaar' => 'circular',
|
||||
'e-rupi' => 'circular',
|
||||
'Bharat QR' => 'circulars',
|
||||
'bharat-billpay' => 'circulars',
|
||||
];
|
||||
|
||||
const PARAMETERS = [[
|
||||
'product' => [
|
||||
'name' => 'product',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'CTS' => 'cts',
|
||||
'UPI' => 'upi',
|
||||
'RuPay' => 'rupay',
|
||||
'NACH' => 'nach',
|
||||
'IMPS' => 'imps',
|
||||
'NETC FASTag' => 'netc-fastag',
|
||||
'*99#' => '99',
|
||||
'NFS' => 'nfs',
|
||||
'AePS' => 'aeps',
|
||||
'BHIM Aadhaar' => 'bhim-aadhaar',
|
||||
'e-RUPI' => 'e-rupi',
|
||||
'Bharat BillPay' => 'bharat-billpay'
|
||||
]
|
||||
]
|
||||
]];
|
||||
|
||||
public function getName() {
|
||||
$product = $this->getInput('product');
|
||||
if ($product) {
|
||||
$productNameMap = array_flip(self::PARAMETERS[0]['product']['values']);
|
||||
$productName = $productNameMap[$product];
|
||||
return "NPCI Circulars: $productName";
|
||||
}
|
||||
|
||||
return 'NPCI Circulars';
|
||||
}
|
||||
|
||||
public function getURI(){
|
||||
$product = $this->getInput('product');
|
||||
return $product ? sprintf('%s/what-we-do/%s/%s', self::URI, $product, self::URL_SUFFIX[$product]) : self::URI;
|
||||
}
|
||||
|
||||
public function collectData(){
|
||||
$html = getSimpleHTMLDOMCached($this->getURI());
|
||||
$year = date('Y');
|
||||
$elements = $html->find("div[id=year$year] .pdf-item");
|
||||
|
||||
foreach($elements as $element) {
|
||||
$title = $element->find('p', 0)->innertext;
|
||||
|
||||
$link = $element->find('a', 0);
|
||||
|
||||
$uri = null;
|
||||
|
||||
if ($link) {
|
||||
$pdfLink = $link->getAttribute('href');
|
||||
$uri = self::URI . str_replace(' ', '+', $pdfLink);
|
||||
}
|
||||
|
||||
$item = [
|
||||
'uri' => $uri,
|
||||
'title' => $title,
|
||||
'content' => $title ,
|
||||
'uid' => sha1($pdfLink),
|
||||
'enclosures' => [
|
||||
$uri
|
||||
]
|
||||
];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
$this->items = array_slice($this->items, 0, 15);
|
||||
}
|
||||
}
|
@@ -130,7 +130,9 @@ class OpenlyBridge extends BridgeAbstract {
|
||||
$this->feedTitle = $html->find('a.tooltipitem', 0)->plaintext;
|
||||
}
|
||||
|
||||
foreach($html->find('div.item') as $div) {
|
||||
$items = $html->find('div.item');
|
||||
$limit = 5;
|
||||
foreach(array_slice($items, 0, $limit) as $div) {
|
||||
$this->items[] = $this->getArticle($div->find('a', 0)->href);
|
||||
|
||||
if (count($this->items) >= $this->itemLimit) {
|
||||
|
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
class OsmAndBlogBridge extends BridgeAbstract {
|
||||
const NAME = 'OsmAnd Blog';
|
||||
const URI = 'https://osmand.net/';
|
||||
const DESCRIPTION = 'Get the latest news from OsmAnd.net';
|
||||
const MAINTAINER = 'fulmeek';
|
||||
|
||||
public function collectData() {
|
||||
$html = getSimpleHTMLDOM(self::URI . 'blog');
|
||||
|
||||
foreach($html->find('div.article') as $element) {
|
||||
$item = array();
|
||||
|
||||
$objTitle = $element->find('h1', 0);
|
||||
if (!$objTitle)
|
||||
$objTitle = $element->find('h2', 0);
|
||||
if (!$objTitle)
|
||||
$objTitle = $element->find('h3', 0);
|
||||
if ($objTitle)
|
||||
$item['title'] = $objTitle->plaintext;
|
||||
|
||||
$objDate = $element->find('meta[pubdate]', 0);
|
||||
if ($objDate) {
|
||||
$item['timestamp'] = strtotime($objDate->pubdate);
|
||||
} else {
|
||||
$objDate = $element->find('.date', 0);
|
||||
if ($objDate)
|
||||
$item['timestamp'] = strtotime($objDate->plaintext);
|
||||
}
|
||||
|
||||
$this->cleanupContent($element, $objTitle, $objDate, $element->find('.date', 0));
|
||||
$item['content'] = $element->innertext;
|
||||
|
||||
$objLink = $html->find('.articlelinklist a', 0);
|
||||
if ($objLink) {
|
||||
$item['uri'] = $this->filterURL($objLink->href);
|
||||
} else {
|
||||
$item['uri'] = 'urn:sha1:' . hash('sha1', $item['content']);
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function filterURL($url) {
|
||||
if (strpos($url, '://') === false)
|
||||
return self::URI . ltrim($url, '/');
|
||||
return $url;
|
||||
}
|
||||
|
||||
private function cleanupContent($content, ...$removeItems) {
|
||||
foreach ($removeItems as $obj) {
|
||||
if ($obj) $obj->outertext = '';
|
||||
}
|
||||
foreach ($content->find('img') as $obj) {
|
||||
$obj->src = $this->filterURL($obj->src);
|
||||
}
|
||||
foreach ($content->find('a') as $obj) {
|
||||
$obj->href = $this->filterURL($obj->href);
|
||||
$obj->target = '_blank';
|
||||
}
|
||||
}
|
||||
}
|
34
bridges/PCGWNewsBridge.php
Normal file
34
bridges/PCGWNewsBridge.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
class PCGWNewsBridge extends FeedExpander {
|
||||
const MAINTAINER = 'somini';
|
||||
const NAME = 'PCGamingWiki News';
|
||||
const BASE_URI = 'https://www.pcgamingwiki.com';
|
||||
const URI = self::BASE_URI . '/wiki/PCGamingWiki:News';
|
||||
const DESCRIPTION = 'PCGW News Feed';
|
||||
|
||||
public function getIcon() {
|
||||
return 'https://static.pcgamingwiki.com/favicons/pcgamingwiki.png';
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
$now = strtotime('now');
|
||||
|
||||
foreach($html->find('.mw-parser-output .news_li') as $element) {
|
||||
$item = array();
|
||||
|
||||
$date_string = $element->find('b', 0)->innertext;
|
||||
$date = strtotime($date_string);
|
||||
if ($date > $now) {
|
||||
$date = strtotime($date_string . ' - 1 year');
|
||||
}
|
||||
$item['title'] = self::NAME . ' for ' . date('Y-m-d', $date);
|
||||
$item['content'] = $element;
|
||||
$item['uri'] = $this->getURI();
|
||||
$item['timestamp'] = $date;
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
80
bridges/ParlerBridge.php
Normal file
80
bridges/ParlerBridge.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
final class ParlerBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Parler.com bridge';
|
||||
const URI = 'https://parler.com';
|
||||
const DESCRIPTION = 'Fetches the latest posts from a parler user';
|
||||
const MAINTAINER = 'dvikan';
|
||||
const CACHE_TIMEOUT = 60 * 15; // 15m
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'user' => [
|
||||
'name' => 'User',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'exampleValue' => 'NigelFarage',
|
||||
],
|
||||
'limit' => self::LIMIT,
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$user = trim($this->getInput('user'));
|
||||
|
||||
if (preg_match('#^https?://parler\.com/(\w+)#i', $user, $m)) {
|
||||
$user = $m[1];
|
||||
}
|
||||
|
||||
$posts = $this->fetchParlerProfileFeed($user);
|
||||
|
||||
foreach ($posts as $post) {
|
||||
// For some reason, the post data is placed inside primary attribute
|
||||
$primary = $post->primary;
|
||||
|
||||
$item = [
|
||||
'title' => mb_substr($primary->body, 0, 100),
|
||||
'uri' => sprintf('https://parler.com/feed/%s', $primary->uuid),
|
||||
'author' => $primary->username,
|
||||
'uid' => $primary->uuid,
|
||||
'content' => nl2br($primary->full_body),
|
||||
];
|
||||
|
||||
$date = DateTimeImmutable::createFromFormat('m/d/YH:i A', $primary->date_str . $primary->time_str);
|
||||
if ($date) {
|
||||
$item['timestamp'] = $date->getTimestamp();
|
||||
} else {
|
||||
Debug::log(sprintf('Unable to parse data from Parler.com: "%s"', $date));
|
||||
}
|
||||
|
||||
if (isset($primary->image)) {
|
||||
$item['enclosures'][] = $primary->image;
|
||||
$item['content'] .= sprintf('<img loading="lazy" src="%s">', $primary->image);
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchParlerProfileFeed(string $user): array
|
||||
{
|
||||
$json = getContents('https://parler.com/open-api/profile-feed.php', [], [
|
||||
CURLOPT_POSTFIELDS => http_build_query([
|
||||
'user' => $user,
|
||||
'page' => '1',
|
||||
]),
|
||||
]);
|
||||
$response = json_decode($json);
|
||||
if ($response === false) {
|
||||
throw new \Exception('Unable to decode json from Parler');
|
||||
}
|
||||
if ($response->status !== 'ok') {
|
||||
throw new \Exception('Did not get OK from Parler');
|
||||
}
|
||||
if ($response->data === []) {
|
||||
throw new \Exception('Unknown Parler username');
|
||||
}
|
||||
return $response->data->posts;
|
||||
}
|
||||
}
|
@@ -28,7 +28,9 @@ class ParuVenduImmoBridge extends BridgeAbstract {
|
||||
public function collectData(){
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
foreach($html->find('div.annonce a') as $element) {
|
||||
$elements = $html->find('#bloc_liste > div.ergov3-annonce a');
|
||||
|
||||
foreach($elements as $element) {
|
||||
|
||||
if(!$element->title) {
|
||||
continue;
|
||||
@@ -41,10 +43,19 @@ class ParuVenduImmoBridge extends BridgeAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
$desc = $element->find('span.desc')[0]->innertext;
|
||||
$desc = str_replace("voir l'annonce", '', $desc);
|
||||
$description = $element->find('p', 0);
|
||||
if ($description) {
|
||||
$desc = str_replace("voir l'annonce", '', $description->innertext);
|
||||
} else {
|
||||
$desc = '';
|
||||
}
|
||||
|
||||
$price = $element->find('span.price')[0]->innertext;
|
||||
$priceElement = $element->find('div.ergov3-priceannonce', 0);
|
||||
if ($priceElement) {
|
||||
$price = $priceElement->innertext;
|
||||
} else {
|
||||
$price = '';
|
||||
}
|
||||
|
||||
list($href) = explode('#', $element->href);
|
||||
|
||||
|
@@ -7,11 +7,18 @@ class PcGamerBridge extends BridgeAbstract
|
||||
updates and news on all your favorite PC gaming franchises.';
|
||||
const MAINTAINER = 'IceWreck, mdemoss';
|
||||
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'limit' => self::LIMIT,
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached($this->getURI(), 300);
|
||||
$stories = $html->find('a.article-link');
|
||||
foreach ($stories as $element) {
|
||||
$limit = $this->getInput('limit') ?? 10;
|
||||
foreach (array_slice($stories, 0, $limit) as $element) {
|
||||
$item = array();
|
||||
$item['uri'] = $element->href;
|
||||
$articleHtml = getSimpleHTMLDOMCached($item['uri']);
|
||||
|
682
bridges/PepperBridgeAbstract.php
Normal file
682
bridges/PepperBridgeAbstract.php
Normal file
@@ -0,0 +1,682 @@
|
||||
<?php
|
||||
|
||||
class PepperBridgeAbstract extends BridgeAbstract {
|
||||
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
|
||||
public function collectData(){
|
||||
switch($this->queriedContext) {
|
||||
case $this->i8n('context-keyword'):
|
||||
return $this->collectDataKeywords();
|
||||
break;
|
||||
case $this->i8n('context-group'):
|
||||
return $this->collectDataGroup();
|
||||
break;
|
||||
case $this->i8n('context-talk'):
|
||||
return $this->collectDataTalk();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Deal data from the choosen group in the choosed order
|
||||
*/
|
||||
protected function collectDataGroup()
|
||||
{
|
||||
$url = $this->getGroupURI();
|
||||
$this->collectDeals($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Deal data from the choosen keywords and parameters
|
||||
*/
|
||||
protected function collectDataKeywords()
|
||||
{
|
||||
/* Even if the original website uses POST with the search page, GET works too */
|
||||
$url = $this->getSearchURI();
|
||||
$this->collectDeals($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Deal data using the given URL
|
||||
*/
|
||||
protected function collectDeals($url){
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
$list = $html->find('article[id]');
|
||||
|
||||
// Deal Image Link CSS Selector
|
||||
$selectorImageLink = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'cept-thread-image-link',
|
||||
'imgFrame',
|
||||
'imgFrame--noBorder',
|
||||
'thread-listImgCell',
|
||||
)
|
||||
);
|
||||
|
||||
// Deal Link CSS Selector
|
||||
$selectorLink = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'cept-tt',
|
||||
'thread-link',
|
||||
'linkPlain',
|
||||
)
|
||||
);
|
||||
|
||||
// Deal Hotness CSS Selector
|
||||
$selectorHot = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'cept-vote-box',
|
||||
'vote-box'
|
||||
)
|
||||
);
|
||||
|
||||
// Deal Description CSS Selector
|
||||
$selectorDescription = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'overflow--wrap-break'
|
||||
)
|
||||
);
|
||||
|
||||
// Deal Date CSS Selector
|
||||
$selectorDate = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'size--all-s',
|
||||
'flex',
|
||||
'boxAlign-jc--all-fe'
|
||||
)
|
||||
);
|
||||
|
||||
// If there is no results, we don't parse the content because it display some random deals
|
||||
$noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
|
||||
if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
|
||||
$this->items = array();
|
||||
} else {
|
||||
foreach ($list as $deal) {
|
||||
$item = array();
|
||||
$item['uri'] = $this->getDealURI($deal);
|
||||
$item['title'] = $this->getTitle($deal);
|
||||
$item['author'] = $deal->find('span.thread-username', 0)->plaintext;
|
||||
|
||||
$item['content'] = '<table><tr><td><a href="'
|
||||
. $item['uri']
|
||||
. '"><img src="'
|
||||
. $this->getImage($deal)
|
||||
. '"/></td><td>'
|
||||
. $this->getHTMLTitle($item)
|
||||
. $this->getPrice($deal)
|
||||
. $this->getDiscount($deal)
|
||||
. $this->getShipsFrom($deal)
|
||||
. $this->getShippingCost($deal)
|
||||
. $this->getSource($deal)
|
||||
. $deal->find('div[class*=' . $selectorDescription . ']', 0)->innertext
|
||||
. '</td><td>'
|
||||
. $deal->find('div[class*=' . $selectorHot . ']', 0)
|
||||
->find('span', 1)->outertext
|
||||
. '</td></table>';
|
||||
|
||||
// Check if a clock icon is displayed on the deal
|
||||
$clocks = $deal->find('svg[class*=icon--clock]');
|
||||
if($clocks !== null && count($clocks) > 0) {
|
||||
// Get the last clock, corresponding to the deal posting date
|
||||
$clock = end($clocks);
|
||||
|
||||
// Find the text corresponding to the clock
|
||||
$spanDateDiv = $clock->parent()->find('span[class=hide--toW3]', 0);
|
||||
$itemDate = $spanDateDiv->plaintext;
|
||||
// In case of a Local deal, there is no date, but we can use
|
||||
// this case for other reason (like date not in the last field)
|
||||
if ($this->contains($itemDate, $this->i8n('localdeal'))) {
|
||||
$item['timestamp'] = time();
|
||||
} else if ($this->contains($itemDate, $this->i8n('relative-date-indicator'))) {
|
||||
$item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
|
||||
} else {
|
||||
$item['timestamp'] = $this->parseDate($itemDate);
|
||||
}
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Talk lastest comments
|
||||
*/
|
||||
protected function collectDataTalk(){
|
||||
$threadURL = $this->getInput('url');
|
||||
$onlyWithUrl = $this->getInput('only_with_url');
|
||||
|
||||
// Get Thread ID from url passed in parameter
|
||||
$threadSearch = preg_match('/-([0-9]{1,20})$/', $threadURL, $matches);
|
||||
|
||||
// Show an error message if we can't find the thread ID in the URL sent by the user
|
||||
if($threadSearch !== 1) {
|
||||
returnClientError($this->i8n('thread-error'));
|
||||
}
|
||||
$threadID = $matches[1];
|
||||
|
||||
$url = $this->i8n('bridge-uri') . 'graphql';
|
||||
|
||||
// Get Cookies header to do the query
|
||||
$cookies = $this->getCookies($url);
|
||||
|
||||
// GraphQL String
|
||||
// This was extracted from https://www.dealabs.com/assets/js/modern/common_211b99.js
|
||||
// This string was extracted during a Website visit, and minified using this neat tool :
|
||||
// https://codepen.io/dangodev/pen/Baoqmoy
|
||||
$graphqlString = <<<'HEREDOC'
|
||||
query comments($filter:CommentFilter!,$limit:Int,$page:Int){comments(filter:$filter,limit:$limit,page:$page){
|
||||
items{...commentFields}pagination{...paginationFields}}}fragment commentFields on Comment{commentId threadId url
|
||||
preparedHtmlContent user{...userMediumAvatarFields...userNameFields...userPersonaFields bestBadge{...badgeFields}}
|
||||
reactionCounts{type count}deletable currentUserReaction{type}reported reportable source status createdAt updatedAt
|
||||
ignored popular deletedBy{username}notes{content createdAt user{username}}lastEdit{reason timeAgo userId}}fragment
|
||||
userMediumAvatarFields on User{userId isDeletedOrPendingDeletion imageUrls(slot:"default",variations:
|
||||
["user_small_avatar"])}fragment userNameFields on User{userId username isUserProfileHidden isDeletedOrPendingDeletion}
|
||||
fragment userPersonaFields on User{persona{type text}}fragment badgeFields on Badge{badgeId level{...badgeLevelFields}}
|
||||
fragment badgeLevelFields on BadgeLevel{key name description}fragment paginationFields on Pagination{count current last
|
||||
next previous size order}
|
||||
HEREDOC;
|
||||
|
||||
// Construct the JSON object to send to the Website
|
||||
$queryArray = array (
|
||||
'query' => $graphqlString,
|
||||
'variables' => array (
|
||||
'filter' => array (
|
||||
'threadId' => array (
|
||||
'eq' => $threadID,
|
||||
),
|
||||
'order' => array (
|
||||
'direction' => 'Descending',
|
||||
),
|
||||
|
||||
),
|
||||
'page' => 1,
|
||||
),
|
||||
);
|
||||
$queryJSON = json_encode($queryArray);
|
||||
|
||||
// HTTP headers
|
||||
$header = array(
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json, text/plain, */*',
|
||||
'X-Pepper-Txn: threads.show',
|
||||
'X-Request-Type: application/vnd.pepper.v1+json',
|
||||
'X-Requested-With: XMLHttpRequest',
|
||||
$cookies,
|
||||
);
|
||||
// CURL Options
|
||||
$opts = array(
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => $queryJSON
|
||||
);
|
||||
$json = getContents($url, $header, $opts);
|
||||
$objects = json_decode($json);
|
||||
foreach($objects->data->comments->items as $comment) {
|
||||
$item = array();
|
||||
$item['uri'] = $comment->url;
|
||||
$item['title'] = $comment->user->username . ' - ' . $comment->createdAt;
|
||||
$item['author'] = $comment->user->username;
|
||||
$item['content'] = $comment->preparedHtmlContent;
|
||||
$item['uid'] = $comment->commentId;
|
||||
// Timestamp handling needs a new parsing function
|
||||
if($onlyWithUrl == true) {
|
||||
// Count Links and Quote Links
|
||||
$content = str_get_html($item['content']);
|
||||
$countLinks = count($content->find('a[href]'));
|
||||
$countQuoteLinks = count($content->find('a[href][class=userHtml-quote-source]'));
|
||||
// Only add element if there are Links ans more links tant Quote links
|
||||
if($countLinks > 0 && $countLinks > $countQuoteLinks) {
|
||||
$this->items[] = $item;
|
||||
}
|
||||
} else {
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the cookies obtained from the URL
|
||||
* @return array the array containing the cookies set by the URL
|
||||
*/
|
||||
private function getCookies($url)
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
// get headers too with this line
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
$result = curl_exec($ch);
|
||||
// get cookie
|
||||
// multi-cookie variant contributed by @Combuster in comments
|
||||
preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $matches);
|
||||
$cookies = array();
|
||||
foreach($matches[1] as $item) {
|
||||
parse_str($item, $cookie);
|
||||
$cookies = array_merge($cookies, $cookie);
|
||||
}
|
||||
$header = 'Cookie: ';
|
||||
foreach($cookies as $name => $content) {
|
||||
$header .= $name . '=' . $content . '; ';
|
||||
}
|
||||
return $header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the string $str contains any of the string of the array $arr
|
||||
* @return boolean true if the string matched anything otherwise false
|
||||
*/
|
||||
private function contains($str, array $arr)
|
||||
{
|
||||
foreach ($arr as $a) {
|
||||
if (stripos($str, $a) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Price from a Deal if it exists
|
||||
* @return string String of the deal price
|
||||
*/
|
||||
private function getPrice($deal)
|
||||
{
|
||||
if ($deal->find(
|
||||
'span[class*=thread-price]', 0) != null) {
|
||||
return '<div>' . $this->i8n('price') . ' : '
|
||||
. $deal->find(
|
||||
'span[class*=thread-price]', 0
|
||||
)->plaintext
|
||||
. '</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Title from a Deal if it exists
|
||||
* @return string String of the deal title
|
||||
*/
|
||||
private function getTitle($deal)
|
||||
{
|
||||
|
||||
$titleRoot = $deal->find('div[class*=threadGrid-title]', 0);
|
||||
$titleA = $titleRoot->find('a[class*=thread-link]', 0);
|
||||
$titleFirstChild = $titleRoot->first_child();
|
||||
if($titleA !== null) {
|
||||
$title = $titleA->plaintext;
|
||||
} else {
|
||||
// In some case, expired deals have a different format
|
||||
$title = $titleRoot->find('span', 0)->plaintext;
|
||||
}
|
||||
|
||||
return $title;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Title from a Talk if it exists
|
||||
* @return string String of the Talk title
|
||||
*/
|
||||
private function getTalkTitle()
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached($this->getInput('url'));
|
||||
$title = $html->find('h1[class=thread-title]', 0)->plaintext;
|
||||
return $title;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML Title code from an item
|
||||
* @return string String of the deal title
|
||||
*/
|
||||
private function getHTMLTitle($item)
|
||||
{
|
||||
if($item['uri'] == '') {
|
||||
$html = '<h2>' . $item['title'] . '</h2>';
|
||||
} else {
|
||||
$html = '<h2><a href="' . $item['uri'] . '">'
|
||||
. $item['title'] . '</a></h2>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URI from a Deal if it exists
|
||||
* @return string String of the deal URI
|
||||
*/
|
||||
private function getDealURI($deal)
|
||||
{
|
||||
|
||||
$uriA = $deal->find('div[class*=threadGrid-title]', 0)->find('a[class*=thread-link]', 0);
|
||||
if($uriA === null) {
|
||||
$uri = '';
|
||||
} else {
|
||||
$uri = $uriA->href;
|
||||
}
|
||||
|
||||
return $uri;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Shipping costs from a Deal if it exists
|
||||
* @return string String of the deal shipping Cost
|
||||
*/
|
||||
private function getShippingCost($deal)
|
||||
{
|
||||
if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0) != null) {
|
||||
if ($deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1) != null) {
|
||||
return '<div>' . $this->i8n('shipping') . ' : '
|
||||
. $deal->find('span[class*=space--ml-2 size--all-s overflow--wrap-off]', 0)->children(1)->innertext
|
||||
. '</div>';
|
||||
} else {
|
||||
return '<div>' . $this->i8n('shipping') . ' : '
|
||||
. $deal->find('span[class*=text--color-greyShade flex--inline]', 0)->innertext
|
||||
. '</div>';
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source of a Deal if it exists
|
||||
* @return string String of the deal source
|
||||
*/
|
||||
private function getSource($deal)
|
||||
{
|
||||
if ($deal->find('a[class*=text--color-greyShade]', 0) != null) {
|
||||
return '<div>' . $this->i8n('origin') . ' : '
|
||||
. $deal->find('a[class*=text--color-greyShade]', 0)->outertext
|
||||
. '</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original Price and discout from a Deal if it exists
|
||||
* @return string String of the deal original price and discount
|
||||
*/
|
||||
private function getDiscount($deal)
|
||||
{
|
||||
if ($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
|
||||
$discountHtml = $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0);
|
||||
if ($discountHtml != null) {
|
||||
$discount = $discountHtml->plaintext;
|
||||
} else {
|
||||
$discount = '';
|
||||
}
|
||||
return '<div>' . $this->i8n('discount') . ' : <span style="text-decoration: line-through;">'
|
||||
. $deal->find(
|
||||
'span[class*=mute--text text--lineThrough]', 0
|
||||
)->plaintext
|
||||
. '</span> '
|
||||
. $discount
|
||||
. '</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Picture URL from a Deal if it exists
|
||||
* @return string String of the deal Picture URL
|
||||
*/
|
||||
private function getImage($deal)
|
||||
{
|
||||
$selectorLazy = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'thread-image',
|
||||
'width--all-auto',
|
||||
'height--all-auto',
|
||||
'imgFrame-img',
|
||||
'img--dummy',
|
||||
'js-lazy-img'
|
||||
)
|
||||
);
|
||||
|
||||
$selectorPlain = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'thread-image',
|
||||
'width--all-auto',
|
||||
'height--all-auto',
|
||||
'imgFrame-img',
|
||||
)
|
||||
);
|
||||
if ($deal->find('img[class=' . $selectorLazy . ']', 0) != null) {
|
||||
return json_decode(
|
||||
html_entity_decode(
|
||||
$deal->find('img[class=' . $selectorLazy . ']', 0)
|
||||
->getAttribute('data-lazy-img')))->{'src'};
|
||||
} else {
|
||||
return $deal->find('img[class*=' . $selectorPlain . ']', 0 )->src;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the originating country from a Deal if it exists
|
||||
* @return string String of the deal originating country
|
||||
*/
|
||||
private function getShipsFrom($deal)
|
||||
{
|
||||
$selector = implode(
|
||||
' ', /* Notice this is a space! */
|
||||
array(
|
||||
'hide--toW2',
|
||||
'metaRibbon',
|
||||
)
|
||||
);
|
||||
if ($deal->find('span[class*=' . $selector . ']', 0) != null) {
|
||||
return '<div>'
|
||||
. $deal->find('span[class*=' . $selector . ']', 0)->children(2)->plaintext
|
||||
. '</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a local date into a timestamp
|
||||
* @return int timestamp of the input date
|
||||
*/
|
||||
private function parseDate($string)
|
||||
{
|
||||
$month_local = $this->i8n('local-months');
|
||||
$month_en = array(
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
);
|
||||
|
||||
// A date can be prfixed with some words, we remove theme
|
||||
$string = $this->removeDatePrefixes($string);
|
||||
// We translate the local months name in the english one
|
||||
$date_str = trim(str_replace($month_local, $month_en, $string));
|
||||
|
||||
// If the date does not contain any year, we add the current year
|
||||
if (!preg_match('/[0-9]{4}/', $string)) {
|
||||
$date_str .= ' ' . date('Y');
|
||||
}
|
||||
|
||||
// Add the Hour and minutes
|
||||
$date_str .= ' 00:00';
|
||||
$date = DateTime::createFromFormat('j F Y H:i', $date_str);
|
||||
// In some case, the date is not recognized : as a workaround the actual date is taken
|
||||
if($date === false) {
|
||||
$date = new DateTime();
|
||||
}
|
||||
return $date->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the prefix of a date if it has one
|
||||
* @return the date without prefiux
|
||||
*/
|
||||
private function removeDatePrefixes($string)
|
||||
{
|
||||
$string = str_replace($this->i8n('date-prefixes'), array(), $string);
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the suffix of a relative date if it has one
|
||||
* @return the relative date without suffixes
|
||||
*/
|
||||
private function removeRelativeDateSuffixes($string)
|
||||
{
|
||||
if (count($this->i8n('relative-date-ignore-suffix')) > 0) {
|
||||
$string = preg_replace($this->i8n('relative-date-ignore-suffix'), '', $string);
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a relative local date into a timestamp
|
||||
* @return int timestamp of the input date
|
||||
*/
|
||||
private function relativeDateToTimestamp($str) {
|
||||
$date = new DateTime();
|
||||
|
||||
// In case of update date, replace it by the regular relative date first word
|
||||
$str = str_replace($this->i8n('relative-date-alt-prefixes'), $this->i8n('local-time-relative')[0], $str);
|
||||
|
||||
$str = $this->removeRelativeDateSuffixes($str);
|
||||
|
||||
$search = $this->i8n('local-time-relative');
|
||||
|
||||
$replace = array(
|
||||
'-',
|
||||
'minute',
|
||||
'hour',
|
||||
'day',
|
||||
'month',
|
||||
'year',
|
||||
''
|
||||
);
|
||||
$date->modify(str_replace($search, $replace, $str));
|
||||
return $date->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed title according to the parameters
|
||||
* @return string the RSS feed Tiyle
|
||||
*/
|
||||
public function getName(){
|
||||
switch($this->queriedContext) {
|
||||
case $this->i8n('context-keyword'):
|
||||
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-keyword') . ' : ' . $this->getInput('q');
|
||||
break;
|
||||
case $this->i8n('context-group'):
|
||||
$values = $this->getParameters()[$this->i8n('context-group')]['group']['values'];
|
||||
$group = array_search($this->getInput('group'), $values);
|
||||
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-group') . ' : ' . $group;
|
||||
break;
|
||||
case $this->i8n('context-talk'):
|
||||
return $this->i8n('bridge-name') . ' - ' . $this->i8n('title-talk') . ' : ' . $this->getTalkTitle();
|
||||
break;
|
||||
default: // Return default value
|
||||
return static::NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed URI according to the parameters
|
||||
* @return string the RSS feed Title
|
||||
*/
|
||||
public function getURI(){
|
||||
switch($this->queriedContext) {
|
||||
case $this->i8n('context-keyword'):
|
||||
return $this->getSearchURI();
|
||||
break;
|
||||
case $this->i8n('context-group'):
|
||||
return $this->getGroupURI();
|
||||
break;
|
||||
case $this->i8n('context-talk'):
|
||||
return $this->getTalkURI();
|
||||
break;
|
||||
default: // Return default value
|
||||
return static::URI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed URI for a keyword Feed
|
||||
* @return string the RSS feed URI
|
||||
*/
|
||||
private function getSearchURI(){
|
||||
$q = $this->getInput('q');
|
||||
$hide_expired = $this->getInput('hide_expired');
|
||||
$hide_local = $this->getInput('hide_local');
|
||||
$priceFrom = $this->getInput('priceFrom');
|
||||
$priceTo = $this->getInput('priceTo');
|
||||
$url = $this->i8n('bridge-uri')
|
||||
. 'search/advanced?q='
|
||||
. urlencode($q)
|
||||
. '&hide_expired=' . $hide_expired
|
||||
. '&hide_local=' . $hide_local
|
||||
. '&priceFrom=' . $priceFrom
|
||||
. '&priceTo=' . $priceTo
|
||||
/* Some default parameters
|
||||
* search_fields : Search in Titres & Descriptions & Codes
|
||||
* sort_by : Sort the search by new deals
|
||||
* time_frame : Search will not be on a limited timeframe
|
||||
*/
|
||||
. '&search_fields[]=1&search_fields[]=2&search_fields[]=3&sort_by=new&time_frame=0';
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed URI for a group Feed
|
||||
* @return string the RSS feed URI
|
||||
*/
|
||||
private function getGroupURI(){
|
||||
$group = $this->getInput('group');
|
||||
$order = $this->getInput('order');
|
||||
|
||||
$url = $this->i8n('bridge-uri')
|
||||
. $this->i8n('uri-group') . $group . $order;
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS Feed URI for a Talk Feed
|
||||
* @return string the RSS feed URI
|
||||
*/
|
||||
private function getTalkURI(){
|
||||
$url = $this->getInput('url');
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is some "localisation" function that returns the needed content using
|
||||
* the "$lang" class variable in the local class
|
||||
* @return various the local content needed
|
||||
*/
|
||||
protected function i8n($key)
|
||||
{
|
||||
if (array_key_exists($key, $this->lang)) {
|
||||
return $this->lang[$key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
69
bridges/PicalaBridge.php
Normal file
69
bridges/PicalaBridge.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
class PicalaBridge extends BridgeAbstract {
|
||||
const TYPES = array(
|
||||
'Actualités' => 'actualites',
|
||||
'Économie' => 'economie',
|
||||
'Tests' => 'tests',
|
||||
'Pratique' => 'pratique',
|
||||
);
|
||||
const NAME = 'Picala Bridge';
|
||||
const URI = 'https://www.picala.fr';
|
||||
const DESCRIPTION = 'Dernière nouvelles du média indépendant sur le vélo électrique';
|
||||
const MAINTAINER = 'Chouchen';
|
||||
const PARAMETERS = array(
|
||||
array(
|
||||
'type' => array(
|
||||
'name' => 'Type',
|
||||
'type' => 'list',
|
||||
'values' => self::TYPES,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
public function getURI() {
|
||||
if(!is_null($this->getInput('type'))) {
|
||||
return sprintf('%s/%s', static::URI, $this->getInput('type'));
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function getIcon() {
|
||||
return 'https://picala-static.s3.amazonaws.com/static/img/favicon/favicon-32x32.png';
|
||||
}
|
||||
|
||||
public function getDescription() {
|
||||
if(!is_null($this->getInput('type'))) {
|
||||
return sprintf('%s - %s', static::DESCRIPTION, array_search($this->getInput('type'), self::TYPES));
|
||||
}
|
||||
|
||||
return parent::getDescription();
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
if(!is_null($this->getInput('type'))) {
|
||||
return sprintf('%s - %s', static::NAME, array_search($this->getInput('type'), self::TYPES));
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$fullhtml = getSimpleHTMLDOM($this->getURI());
|
||||
foreach($fullhtml->find('.list-container-category a') as $article) {
|
||||
$srcsets = explode(',', $article->find('img', 0)->getAttribute('srcset'));
|
||||
$image = explode(' ', trim(array_shift($srcsets)))[0];
|
||||
|
||||
$item = array();
|
||||
$item['uri'] = self::URI . $article->href;
|
||||
$item['title'] = $article->find('h2', 0)->plaintext;
|
||||
$item['content'] = sprintf(
|
||||
'<img src="%s" /><br>%s',
|
||||
$image,
|
||||
$article->find('.teaser__text', 0)->plaintext
|
||||
);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@@ -42,35 +42,49 @@ class PicukiBridge extends BridgeAbstract
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
foreach ($html->find('.box-photos .box-photo') as $element) {
|
||||
|
||||
// check if item is an ad.
|
||||
// skip ad items
|
||||
if (in_array('adv', explode(' ', $element->class))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = array();
|
||||
$url = urljoin(self::URI, $element->find('a', 0)->href);
|
||||
|
||||
$author = trim($element->find('.user-nickname', 0)->plaintext);
|
||||
|
||||
$date = date_create();
|
||||
$relative_date = str_replace(' ago', '', $element->find('.time', 0)->plaintext);
|
||||
date_sub($date, date_interval_create_from_date_string($relative_date));
|
||||
$item['timestamp'] = date_format($date, 'r');
|
||||
$relativeDate = str_replace(' ago', '', $element->find('.time', 0)->plaintext);
|
||||
date_sub($date, date_interval_create_from_date_string($relativeDate));
|
||||
|
||||
$item['uri'] = urljoin(self::URI, $element->find('a', 0)->href);
|
||||
$description = trim($element->find('.photo-description', 0)->plaintext);
|
||||
|
||||
$item['title'] = $element->find('.photo-description', 0)->plaintext;
|
||||
$isVideo = (bool) $element->find('.video-icon', 0);
|
||||
$videoNote = $isVideo ? '<p><i>(video)</i></p>' : '';
|
||||
|
||||
$is_video = (bool) $element->find('.video-icon', 0);
|
||||
$item['content'] = ($is_video) ? '(video) ' : '';
|
||||
$item['content'] .= $element->find('.photo', 0)->outertext;
|
||||
$imageUrl = $element->find('.post-image', 0)->src;
|
||||
|
||||
$item['enclosures'] = array(
|
||||
// just add `.jpg` extension to get the correct mime type. All Instagram posts are JPG
|
||||
urljoin(self::URI, $element->find('.post-image', 0)->src . '.jpg')
|
||||
);
|
||||
// the last path segment needs to be encoded, because it contains special characters like + or |
|
||||
$imageUrlParts = explode('/', $imageUrl);
|
||||
$imageUrlParts[count($imageUrlParts) - 1] = urlencode($imageUrlParts[count($imageUrlParts) - 1]);
|
||||
$imageUrl = implode('/', $imageUrlParts);
|
||||
|
||||
$item['thumbnail'] = urljoin(self::URI, $element->find('.post-image', 0)->src);
|
||||
// add fake file extension for it to be recognized as image/jpeg instead of application/octet-stream
|
||||
$imageUrl = $imageUrl . '#.jpg';
|
||||
|
||||
$this->items[] = $item;
|
||||
$this->items[] = array(
|
||||
'uri' => $url,
|
||||
'author' => $author,
|
||||
'timestamp' => date_format($date, 'r'),
|
||||
'title' => strlen($description) > 60 ? mb_substr($description, 0, 57) . '...' : $description,
|
||||
'thumbnail' => $imageUrl,
|
||||
'enclosures' => [$imageUrl],
|
||||
'content' => <<<HTML
|
||||
<a href="{$url}">
|
||||
<img loading="lazy" src="{$imageUrl}" />
|
||||
</a>
|
||||
{$videoNote}
|
||||
<p>{$description}<p>
|
||||
HTML
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ class PillowfortBridge extends BridgeAbstract {
|
||||
'name' => 'Username',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'exampleValue' => 'vaxis2',
|
||||
'exampleValue' => 'Staff'
|
||||
),
|
||||
'noava' => array(
|
||||
'name' => 'Hide avatar',
|
||||
@@ -39,6 +39,29 @@ class PillowfortBridge extends BridgeAbstract {
|
||||
)
|
||||
));
|
||||
|
||||
/**
|
||||
* The Pillowfort bridge.
|
||||
*
|
||||
* Pillowfort pages are dynamically generated from a json file
|
||||
* which holds the last 20 or so posts from the given user.
|
||||
* This bridge uses that json file and HTML/CSS similar
|
||||
* to the Twitter bridge for formatting.
|
||||
*/
|
||||
public function collectData() {
|
||||
$jsonSite = getContents($this->getJSONURI());
|
||||
|
||||
$jsonFile = json_decode($jsonSite, true);
|
||||
$posts = $jsonFile['posts'];
|
||||
|
||||
foreach($posts as $post) {
|
||||
$item = $this->getItemFromPost($post);
|
||||
|
||||
//empty when 'noreblogs' is checked and current post is a reblog.
|
||||
if(!empty($item))
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
$name = $this -> getUsername();
|
||||
if($name != '')
|
||||
@@ -56,7 +79,7 @@ class PillowfortBridge extends BridgeAbstract {
|
||||
}
|
||||
|
||||
protected function getJSONURI() {
|
||||
return $this -> getURI() . '/json';
|
||||
return $this -> getURI() . '/json/?p=1';
|
||||
}
|
||||
|
||||
protected function getUsername() {
|
||||
@@ -196,27 +219,4 @@ EOD;
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Pillowfort bridge.
|
||||
*
|
||||
* Pillowfort pages are dynamically generated from a json file
|
||||
* which holds the last 20 or so posts from the given user.
|
||||
* This bridge uses that json file and HTML/CSS similar
|
||||
* to the Twitter bridge for formatting.
|
||||
*/
|
||||
public function collectData() {
|
||||
$jsonSite = getContents($this -> getJSONURI());
|
||||
|
||||
$jsonFile = json_decode($jsonSite, true);
|
||||
$posts = $jsonFile['posts'];
|
||||
|
||||
foreach($posts as $post) {
|
||||
$item = $this->getItemFromPost($post);
|
||||
|
||||
//empty when 'noreblogs' is checked and current post is a reblog.
|
||||
if(!empty($item))
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,82 +1,190 @@
|
||||
<?php
|
||||
class PixivBridge extends BridgeAbstract {
|
||||
|
||||
// Good resource on API return values (Ex: illustType):
|
||||
// https://hackage.haskell.org/package/pixiv-0.1.0/docs/Web-Pixiv-Types.html
|
||||
const MAINTAINER = 'Yaman Qalieh';
|
||||
const NAME = 'Pixiv Bridge';
|
||||
const URI = 'https://www.pixiv.net/';
|
||||
const DESCRIPTION = 'Returns the tag search from pixiv.net';
|
||||
const CACHE_TIMEOUT = 21600; // 6h
|
||||
|
||||
|
||||
const PARAMETERS = array( array(
|
||||
'mode' => array(
|
||||
'name' => 'Post Type',
|
||||
'type' => 'list',
|
||||
'values' => array('Illustration' => 'illustrations/',
|
||||
'Manga' => 'manga/',
|
||||
'Novel' => 'novels/')
|
||||
const PARAMETERS = array(
|
||||
'global' => array(
|
||||
'posts' => array(
|
||||
'name' => 'Post Limit',
|
||||
'type' => 'number',
|
||||
'defaultValue' => '10'
|
||||
),
|
||||
'fullsize' => array(
|
||||
'name' => 'Full-size Image',
|
||||
'type' => 'checkbox'
|
||||
),
|
||||
'mode' => array(
|
||||
'name' => 'Post Type',
|
||||
'type' => 'list',
|
||||
'values' => array('All Works' => 'all',
|
||||
'Illustrations' => 'illustrations/',
|
||||
'Manga' => 'manga/',
|
||||
'Novels' => 'novels/')
|
||||
),
|
||||
),
|
||||
'tag' => array(
|
||||
'name' => 'Query to search',
|
||||
'exampleValue' => 'オリジナル',
|
||||
'required' => true
|
||||
// Backwards compatibility: Original bridge only had tags
|
||||
'' => array(
|
||||
'tag' => array(
|
||||
'name' => 'Query to search',
|
||||
'exampleValue' => 'オリジナル',
|
||||
'required' => true
|
||||
)
|
||||
),
|
||||
'posts' => array(
|
||||
'name' => 'Post Limit',
|
||||
'type' => 'number',
|
||||
'defaultValue' => '10'
|
||||
),
|
||||
'fullsize' => array(
|
||||
'name' => 'Full-size Image',
|
||||
'type' => 'checkbox'
|
||||
'User' => array(
|
||||
'userid' => array(
|
||||
'name' => 'User ID from profile URL',
|
||||
'exampleValue' => '11',
|
||||
'required' => true
|
||||
)
|
||||
)
|
||||
));
|
||||
);
|
||||
|
||||
// maps from URLs to json keys by context
|
||||
const JSON_KEY_MAP = array(
|
||||
'illustrations/' => 'illust',
|
||||
'manga/' => 'manga',
|
||||
'novels/' => 'novel'
|
||||
);
|
||||
const WORK_LINK_MAP = array(
|
||||
'illustrations/' => 'artworks/',
|
||||
'manga/' => 'artworks/',
|
||||
'novels/' => 'novel/show.php?id='
|
||||
'' => array(
|
||||
'illustrations/' => 'illust',
|
||||
'manga/' => 'manga',
|
||||
'novels/' => 'novel'
|
||||
),
|
||||
'User' => array(
|
||||
'illustrations/' => 'illusts',
|
||||
'manga/' => 'manga',
|
||||
'novels/' => 'novels'
|
||||
)
|
||||
);
|
||||
|
||||
// Hold the username for getName()
|
||||
private $username = null;
|
||||
|
||||
public function getName() {
|
||||
switch($this->queriedContext) {
|
||||
// Tags context
|
||||
case '':
|
||||
$context = 'Tag';
|
||||
$query = $this->getInput('tag');
|
||||
break;
|
||||
case 'User':
|
||||
$context = 'User';
|
||||
$query = $this->username ?? $this->getInput('userid');
|
||||
break;
|
||||
default:
|
||||
return parent::getName();
|
||||
}
|
||||
$mode = array_search($this->getInput('mode'),
|
||||
self::PARAMETERS['global']['mode']['values']);
|
||||
return "Pixiv ${mode} from ${context} ${query}";
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
switch($this->queriedContext) {
|
||||
// Tags context
|
||||
case '':
|
||||
$uri = static::URI . 'tags/' . urlencode($this->getInput('tag') ?? '');
|
||||
break;
|
||||
case 'User':
|
||||
$uri = static::URI . 'users/' . $this->getInput('userid');
|
||||
break;
|
||||
default:
|
||||
return parent::getURI();
|
||||
}
|
||||
if ($this->getInput('mode') != 'all') {
|
||||
$uri = $uri . '/' . $this->getInput('mode');
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
private function getSearchURI($mode) {
|
||||
switch($this->queriedContext) {
|
||||
// Tags context
|
||||
case '':
|
||||
$query = urlencode($this->getInput('tag'));
|
||||
$uri = static::URI . 'ajax/search/top/' . $query;
|
||||
break;
|
||||
case 'User':
|
||||
$uri = static::URI . 'ajax/user/' . $this->getInput('userid')
|
||||
. '/profile/top';
|
||||
break;
|
||||
default:
|
||||
returnClientError('Invalid Context');
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
private function getDataFromJSON($json, $json_key) {
|
||||
$json = $json['body'][$json_key];
|
||||
// Tags context contains subkey
|
||||
if ($this->queriedContext == '') {
|
||||
$json = $json['data'];
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
|
||||
private function collectWorksArray() {
|
||||
$content = getContents($this->getSearchURI($this->getInput('mode')));
|
||||
$content = json_decode($content, true);
|
||||
if ($this->getInput('mode') == 'all') {
|
||||
$total = array();
|
||||
foreach(self::JSON_KEY_MAP[$this->queriedContext] as $mode => $json_key) {
|
||||
$current = $this->getDataFromJSON($content, $json_key);
|
||||
$total = array_merge($total, $current);
|
||||
}
|
||||
$content = $total;
|
||||
} else {
|
||||
$json_key = self::JSON_KEY_MAP[$this->queriedContext][$this->getInput('mode')];
|
||||
$content = $this->getDataFromJSON($content, $json_key);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function collectData() {
|
||||
$content = getContents($this->getSearchURI());
|
||||
$content = json_decode($content, true);
|
||||
|
||||
$key = self::JSON_KEY_MAP[$this->getInput('mode')];
|
||||
$count = 0;
|
||||
foreach($content['body'][$key]['data'] as $result) {
|
||||
$count++;
|
||||
if ($count > $this->getInput('posts')) {
|
||||
break;
|
||||
}
|
||||
$content = $this->collectWorksArray();
|
||||
|
||||
$content = array_filter($content, function($v, $k) {
|
||||
return !array_key_exists('isAdContainer', $v);
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
// Sort by updateDate to get newest works
|
||||
usort($content, function($a, $b) {
|
||||
return $b['updateDate'] <=> $a['updateDate'];
|
||||
});
|
||||
$content = array_slice($content, 0, $this->getInput('posts'));
|
||||
|
||||
foreach($content as $result) {
|
||||
// Store username for getName()
|
||||
if (!$this->username)
|
||||
$this->username = $result['userName'];
|
||||
|
||||
$item = array();
|
||||
$item['id'] = $result['id'];
|
||||
$item['uri'] = static::URI . self::WORK_LINK_MAP[$this->getInput('mode')] . $result['id'];
|
||||
$item['uid'] = $result['id'];
|
||||
$subpath = array_key_exists('illustType', $result) ? 'artworks/' : 'novel/show.php?id=';
|
||||
$item['uri'] = static::URI . $subpath . $result['id'];
|
||||
$item['title'] = $result['title'];
|
||||
$item['author'] = $result['userName'];
|
||||
$item['timestamp'] = $result['updateDate'];
|
||||
$item['content'] = "<img src='" . $this->cacheImage($result['url'], $item['id']) . "' />";
|
||||
$item['categories'] = $result['tags'];
|
||||
$cached_image = $this->cacheImage($result['url'], $result['id'],
|
||||
array_key_exists('illustType', $result));
|
||||
$item['content'] = "<img src='" . $cached_image . "' />";
|
||||
|
||||
// Additional content items
|
||||
if (array_key_exists('pageCount', $result)) {
|
||||
$item['content'] .= '<br>Page Count: ' . $result['pageCount'];
|
||||
} else {
|
||||
$item['content'] .= '<br>Word Count: ' . $result['wordCount'];
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function getSearchURI() {
|
||||
$query = urlencode($this->getInput('tag'));
|
||||
|
||||
$uri = static::URI . 'ajax/search/' . $this->getInput('mode')
|
||||
. $query . '?word=' . $query . '&order=date_d&mode=all&p=1';
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
private function cacheImage($url, $illustId) {
|
||||
private function cacheImage($url, $illustId, $isImage) {
|
||||
$illustId = preg_replace('/[^0-9]/', '', $illustId);
|
||||
$thumbnailurl = $url;
|
||||
|
||||
@@ -93,7 +201,7 @@ class PixivBridge extends BridgeAbstract {
|
||||
if(!is_file($path)) {
|
||||
|
||||
// Get fullsize URL
|
||||
if (!$this->getInput('mode') !== 'novels/' && $this->getInput('fullsize')) {
|
||||
if ($isImage && $this->getInput('fullsize')) {
|
||||
$ajax_uri = static::URI . 'ajax/illust/' . $illustId;
|
||||
$imagejson = json_decode(getContents($ajax_uri), true);
|
||||
$url = $imagejson['body']['urls']['original'];
|
||||
|
@@ -56,8 +56,10 @@ class RadioMelodieBridge extends BridgeAbstract {
|
||||
|
||||
// Handle date to timestamp
|
||||
$dateHTML = $article->find('div[class=author]', 0)->children(1)->plaintext;
|
||||
preg_match('/([a-z]{4,10}[ ]{1,2}[0-9]{1,2} [\p{L}]{4,10} [0-9]{4} à [0-9]{2}:[0-9]{2})/mus', $dateHTML, $matches);
|
||||
|
||||
preg_match('/([a-z]{4,10}[ ]{1,2}[0-9]{1,2} [\p{L}]{3,10} [0-9]{4} à [0-9]{2}:[0-9]{2})/mus', $dateHTML, $matches);
|
||||
$dateText = $matches[1];
|
||||
|
||||
$timestamp = $this->parseDate($dateText);
|
||||
|
||||
$item['enclosures'] = array_merge($picture, $audio);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
class RoadAndTrackBridge extends BridgeAbstract {
|
||||
const MAINTAINER = 'teromene';
|
||||
const NAME = 'Road And Track Bridge';
|
||||
@@ -10,17 +11,15 @@ class RoadAndTrackBridge extends BridgeAbstract {
|
||||
|
||||
$page = getSimpleHTMLDOM(self::URI);
|
||||
|
||||
//Process the first element
|
||||
$firstArticleLink = $page->find('.custom-promo-title', 0)->href;
|
||||
$this->items[] = $this->fetchArticle($firstArticleLink);
|
||||
$limit = 5;
|
||||
|
||||
$limit = 19;
|
||||
foreach($page->find('.full-item-title') as $article) {
|
||||
foreach($page->find('a.enk2x9t2') as $article) {
|
||||
$this->items[] = $this->fetchArticle($article->href);
|
||||
$limit -= 1;
|
||||
if($limit == 0) break;
|
||||
}
|
||||
|
||||
if (count($this->items) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function fixImages($content) {
|
||||
@@ -36,7 +35,6 @@ class RoadAndTrackBridge extends BridgeAbstract {
|
||||
}
|
||||
|
||||
return $enclosures;
|
||||
|
||||
}
|
||||
|
||||
private function fetchArticle($articleLink) {
|
||||
@@ -45,13 +43,19 @@ class RoadAndTrackBridge extends BridgeAbstract {
|
||||
$article = getSimpleHTMLDOM($articleLink);
|
||||
$item = array();
|
||||
|
||||
$item['title'] = $article->find('.content-hed', 0)->innertext;
|
||||
$title = $article->find('.content-hed', 0);
|
||||
if ($title) {
|
||||
$item['title'] = $title->innertext;
|
||||
}
|
||||
|
||||
$item['author'] = $article->find('.byline-name', 0)->innertext;
|
||||
$item['timestamp'] = strtotime($article->find('.content-info-date', 0)->getAttribute('datetime'));
|
||||
|
||||
$content = $article->find('.content-container', 0);
|
||||
if($content->find('.content-rail', 0) !== null)
|
||||
if($content->find('.content-rail', 0) !== null) {
|
||||
$content->find('.content-rail', 0)->innertext = '';
|
||||
}
|
||||
|
||||
$enclosures = $this->fixImages($content);
|
||||
|
||||
$item['enclosures'] = $enclosures;
|
||||
|
@@ -7,20 +7,107 @@ class RobinhoodSnacksBridge extends BridgeAbstract {
|
||||
const CACHE_TIMEOUT = 86400; // 24h
|
||||
const DESCRIPTION = 'Returns newsletters from Robinhood Snacks';
|
||||
|
||||
// Work around 403 by pretending to be a legit browser
|
||||
const FAKE_HEADERS = array(
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0',
|
||||
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||
'Accept-Language: es-ES,en-US;q=0.7,en;q=0.3',
|
||||
'Accept-Encoding: gzip, deflate, br',
|
||||
'Connection: keep-alive',
|
||||
'Upgrade-Insecure-Requests: 1',
|
||||
'Sec-Fetch-Dest: document',
|
||||
'Sec-Fetch-Mode: navigate',
|
||||
'Sec-Fetch-Site: none',
|
||||
'Sec-Fetch-User: ?1',
|
||||
'Pragma: no-cache',
|
||||
'Cache-Control: no-cache',
|
||||
'TE: trailers'
|
||||
);
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
$html = getSimpleHTMLDOM(self::URI, self::FAKE_HEADERS);
|
||||
$html = defaultLinkTo($html, $this->getURI());
|
||||
|
||||
foreach ($html->find('#root > div > div > div > div > div > a') as $element) {
|
||||
$elements = $html->find('#__next > div > div > div > div > a');
|
||||
|
||||
foreach ($elements as $element) {
|
||||
if ($element->href === 'https://snacks.robinhood.com/newsletters/page/2/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = $element->find('div > div', 2);
|
||||
|
||||
// Remove element that is not parsed (span with weekly tag)
|
||||
$unwanted_selector = 'span';
|
||||
foreach($content->find($unwanted_selector) as $found) {
|
||||
$found->outertext = '';
|
||||
}
|
||||
|
||||
$title = $content->find('div', 0)->innertext;
|
||||
$timestamp = strtotime($content->find('div', 1)->innertext);
|
||||
$uri = $element->href;
|
||||
|
||||
$this->items[] = array(
|
||||
'uri' => $element->href,
|
||||
'title' => $element->find('div > div', 3)->plaintext,
|
||||
'content' => $element->find('div > div', 4)->plaintext,
|
||||
'uri' => $uri,
|
||||
'title' => $title,
|
||||
'timestamp' => $timestamp,
|
||||
'content' => self::getArticleContent($uri)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getArticleContent($uri)
|
||||
{
|
||||
$article_html = getSimpleHTMLDOMCached($uri, self::CACHE_TIMEOUT, self::FAKE_HEADERS);
|
||||
if(!$article_html) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$content = $article_html->find('#__next > div > div > div > span', 0);
|
||||
$content->removeChild($content->find('div', 0));
|
||||
$content->removeChild($content->find('h1', 0));
|
||||
$content->removeChild($content->find('img', 1));
|
||||
|
||||
// Remove elements that are not part of article content
|
||||
$unwanted_selector = 'style';
|
||||
foreach($content->find($unwanted_selector) as $found) {
|
||||
$found->outertext = '';
|
||||
}
|
||||
|
||||
// Images cleanup
|
||||
$already_displayed_pictures = array();
|
||||
foreach($content->find('img') as $found) {
|
||||
// Skip loader images
|
||||
if (str_contains($found->src, 'data:image/gif;base64')) {
|
||||
$found->outertext = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip multiple images with same src
|
||||
// and remove duplicated image description
|
||||
if (in_array($found->src, $already_displayed_pictures)) {
|
||||
$found->parent->parent->parent->outertext = '';
|
||||
$found->parent->parent->parent->nextSibling()->nextSibling()->outertext = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove srcset attribute
|
||||
$found->removeAttribute('srcset');
|
||||
|
||||
// If relative img, fix path
|
||||
if (str_starts_with($found->src, '/_next')) {
|
||||
$found->setAttribute('src', 'https://snacks.robinhood.com' . $found->getAttribute('src'));
|
||||
}
|
||||
|
||||
$already_displayed_pictures[] = $found->src;
|
||||
}
|
||||
|
||||
$content_text = $content->innertext;
|
||||
|
||||
// Remove noscript tag to display images
|
||||
$content_text = str_replace('<noscript>', '', $content_text);
|
||||
|
||||
return $content_text;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('GelbooruBridge.php');
|
||||
|
||||
class Rule34Bridge extends GelbooruBridge {
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('Shimmie2Bridge.php');
|
||||
|
||||
class Rule34pahealBridge extends Shimmie2Bridge {
|
||||
|
||||
|
91
bridges/RutubeBridge.php
Normal file
91
bridges/RutubeBridge.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
class RutubeBridge extends BridgeAbstract {
|
||||
|
||||
const NAME = 'Rutube';
|
||||
const URI = 'https://rutube.ru';
|
||||
const MAINTAINER = 'em92';
|
||||
const DESCRIPTION = 'Выводит ленту видео';
|
||||
|
||||
const PARAMETERS = array(
|
||||
'По каналу' => array(
|
||||
'c' => array(
|
||||
'name' => 'ИД канала',
|
||||
'exampleValue' => 1342940, // Мятежник Джек
|
||||
'type' => 'number',
|
||||
'required' => true
|
||||
),
|
||||
),
|
||||
'По плейлисту' => array(
|
||||
'p' => array(
|
||||
'name' => 'ИД плейлиста',
|
||||
'exampleValue' => 83641, // QRUSH
|
||||
'type' => 'number',
|
||||
'required' => true
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
protected $title;
|
||||
|
||||
public function getURI() {
|
||||
if ($this->getInput('c')) {
|
||||
return self::URI . '/channel/' . strval($this->getInput('c')) . '/videos/';
|
||||
} else if ($this->getInput('p')) {
|
||||
return self::URI . '/plst/' . strval($this->getInput('p')) . '/';
|
||||
} else {
|
||||
return parent::getURI();
|
||||
}
|
||||
}
|
||||
|
||||
public function getIcon() {
|
||||
return 'https://static.rutube.ru/static/favicon.ico';
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
if (is_null($this->title)) {
|
||||
return parent::getName();
|
||||
} else {
|
||||
return $this->title . ' - ' . parent::getName();
|
||||
}
|
||||
}
|
||||
|
||||
private function getJSONData($html) {
|
||||
$jsonDataRegex = '/window.reduxState = (.*?);/';
|
||||
preg_match($jsonDataRegex, $html, $matches) or returnServerError('Could not find reduxState');
|
||||
return json_decode(str_replace('\x', '\\\x', $matches[1]));
|
||||
}
|
||||
|
||||
public function collectData(){
|
||||
$link = $this->getURI();
|
||||
|
||||
$html = getContents($link);
|
||||
$reduxState = $this->getJSONData($html);
|
||||
$videos = [];
|
||||
if ($this->getInput('c')) {
|
||||
$videos = $reduxState->userChannel->videos->results;
|
||||
$this->title = $reduxState->userChannel->info->name;
|
||||
} else if ($this->getInput('p')) {
|
||||
$videos = $reduxState->playlist->data->results;
|
||||
$this->title = $reduxState->playlist->title;
|
||||
}
|
||||
|
||||
foreach($videos as $video) {
|
||||
$item = new FeedItem();
|
||||
$item->setTitle($video->title);
|
||||
$item->setURI($video->video_url);
|
||||
$content = '<a href="' . $item->getURI() . '">';
|
||||
$content .= '<img src="' . $video->thumbnail_url . '" />';
|
||||
$content .= '</a><br/>';
|
||||
$content .= nl2br(
|
||||
// Converting links in plaintext
|
||||
// Copied from https://stackoverflow.com/a/12590772
|
||||
preg_replace(
|
||||
'$(https?://[a-z0-9_./?=&#-]+)(?![^<>]*>)$i', ' <a href="$1" target="_blank">$1</a> ',
|
||||
$video->description . ' '
|
||||
)
|
||||
);
|
||||
$item->setContent($content);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('GelbooruBridge.php');
|
||||
|
||||
class SafebooruBridge extends GelbooruBridge {
|
||||
|
||||
|
@@ -12,7 +12,7 @@ class SeznamZpravyBridge extends BridgeAbstract {
|
||||
'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'
|
||||
'exampleValue' => 'radek-nohl-1'
|
||||
),
|
||||
)
|
||||
);
|
||||
@@ -33,55 +33,89 @@ class SeznamZpravyBridge extends BridgeAbstract {
|
||||
$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]'
|
||||
'articleList' => 'ul.ogm-document-timeline-page li article[data-dot=mol-timeline-item]',
|
||||
'articleTitle' => 'a[data-dot=mol-article-card-title]',
|
||||
'articleDM' => 'span.mol-formatted-date__date',
|
||||
'articleTime' => 'span.mol-formatted-date__time',
|
||||
'articleContent' => 'div[data-dot=ogm-article-content]',
|
||||
'articleImage' => 'div[data-dot=ogm-main-media] img',
|
||||
'articleParagraphs' => 'div[data-dot=mol-paragraph]'
|
||||
);
|
||||
|
||||
$html = getSimpleHTMLDOMCached($url . $this->getInput('author'), $ONE_DAY);
|
||||
$main_breadcrumbs = $html->find($selectors['breadcrumbs'], 0);
|
||||
$author = $main_breadcrumbs->last_child()->plaintext
|
||||
or returnServerError('Could not get author on: ' . $this->getURI());
|
||||
$mainBreadcrumbs = $html->find($selectors['breadcrumbs'], 0)
|
||||
or returnServerError('Could not get breadcrumbs for: ' . $this->getURI());
|
||||
|
||||
$author = $mainBreadcrumbs->last_child()->plaintext
|
||||
or returnServerError('Could not get author for: ' . $this->getURI());
|
||||
|
||||
$this->feedName = $author . ' - Seznam Zprávy';
|
||||
|
||||
$articles = $html->find($selectors['article_list'])
|
||||
or returnServerError('Could not find articles on: ' . $this->getURI());
|
||||
$articles = $html->find($selectors['articleList'])
|
||||
or returnServerError('Could not find articles for: ' . $this->getURI());
|
||||
|
||||
foreach ($articles as $article) {
|
||||
$title_link = $article->find($selectors['article_title'], 0)
|
||||
or returnServerError('Could not find title on: ' . $this->getURI());
|
||||
// Get article URL
|
||||
$titleLink = $article->find($selectors['articleTitle'], 0)
|
||||
or returnServerError('Could not find title for: ' . $this->getURI());
|
||||
$articleURL = $titleLink->href;
|
||||
|
||||
$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);
|
||||
$articleContentHTML = getSimpleHTMLDOMCached($articleURL, $ONE_DAY);
|
||||
|
||||
$breadcrumbs_e = $article_content_html->find($selectors['breadcrumbs'], 0);
|
||||
$breadcrumbs = $breadcrumbs_e->children();
|
||||
$num_breadcrumbs = count($breadcrumbs);
|
||||
// Article header image
|
||||
$articleImageElem = $articleContentHTML->find($selectors['articleImage'], 0);
|
||||
|
||||
// Article text content
|
||||
$contentElem = $articleContentHTML->find($selectors['articleContent'], 0)
|
||||
or returnServerError('Could not get article content for: ' . $articleURL);
|
||||
$contentParagraphs = $contentElem->find($selectors['articleParagraphs'])
|
||||
or returnServerError('Could not find paragraphs for: ' . $articleURL);
|
||||
|
||||
// If the article has an image, put that image at the start
|
||||
$contentInitialValue = isset($articleImageElem) ? $articleImageElem->outertext : '';
|
||||
$contentText = array_reduce($contentParagraphs, function($s, $elem) {
|
||||
return $s . $elem->innertext;
|
||||
}, $contentInitialValue);
|
||||
|
||||
// Article categories
|
||||
$breadcrumbsElem = $articleContentHTML->find($selectors['breadcrumbs'], 0)
|
||||
or returnServerError('Could not find breadcrumbs for: ' . $articleURL);
|
||||
$breadcrumbs = $breadcrumbsElem->children();
|
||||
$numBreadcrumbs = count($breadcrumbs);
|
||||
$categories = array();
|
||||
foreach ($breadcrumbs as $cat) {
|
||||
if (--$num_breadcrumbs <= 0) {
|
||||
if (--$numBreadcrumbs <= 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;
|
||||
// Article date & time
|
||||
$articleTimeElem = $article->find($selectors['articleTime'], 0)
|
||||
or returnServerError('Could not find article time for: ' . $articleURL);
|
||||
$articleTime = $articleTimeElem->plaintext;
|
||||
|
||||
$articleDMElem = $article->find($selectors['articleDM'], 0);
|
||||
if (isset($articleDMElem)) {
|
||||
$articleDMText = $articleDMElem->plaintext;
|
||||
} else {
|
||||
// If there is no date but only a time, the article was published today
|
||||
$articleDMText = date('d.m.');
|
||||
}
|
||||
$articleDMY = preg_replace('/[^0-9\.]/', '', $articleDMText) . date('Y');
|
||||
|
||||
// Add article to items, potentially with header image as enclosure
|
||||
$item = array(
|
||||
'title' => $title_link->plaintext,
|
||||
'uri' => $title_link->href,
|
||||
'timestamp' => strtotime($article_dmy . ' ' . $article_time),
|
||||
'title' => $titleLink->plaintext,
|
||||
'uri' => $titleLink->href,
|
||||
'timestamp' => strtotime($articleDMY . ' ' . $articleTime),
|
||||
'author' => $author,
|
||||
'content' => $content_text,
|
||||
'content' => $contentText,
|
||||
'categories' => $categories
|
||||
);
|
||||
if (isset($articleImageElem)) {
|
||||
$item['enclosures'] = array('https:' . $articleImageElem->src);
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
break;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('DanbooruBridge.php');
|
||||
|
||||
class Shimmie2Bridge extends DanbooruBridge {
|
||||
|
||||
|
176
bridges/SlusheBridge.php
Normal file
176
bridges/SlusheBridge.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
class SlusheBridge extends BridgeAbstract {
|
||||
|
||||
const MAINTAINER = 'quickwick';
|
||||
const NAME = 'Slushe';
|
||||
const URI = 'https://slushe.com';
|
||||
const DESCRIPTION = 'Returns latest posts from Slushe';
|
||||
|
||||
const PARAMETERS = array(
|
||||
'Artist' => array(
|
||||
'artist_name' => array(
|
||||
'name' => 'Artist name',
|
||||
'required' => true,
|
||||
'exampleValue' => 'lexx228',
|
||||
'title' => 'Enter an artist name'
|
||||
)
|
||||
),
|
||||
'Category' => array(
|
||||
'category' => array(
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'defaultValue' => 'Safe for Work',
|
||||
'title' => 'Choose a category',
|
||||
'values' => array(
|
||||
'2D' => '29',
|
||||
'3DX' => '58',
|
||||
'Animation' => '60',
|
||||
'Anime Fan Art' => '46',
|
||||
'BDSM' => '47',
|
||||
'Big Butt' => '73',
|
||||
'Big Dick' => '52',
|
||||
'Bit Tits' => '49',
|
||||
'Bisexual' => '69',
|
||||
'Comic' => '51',
|
||||
'Couple' => '3',
|
||||
'Dickgirl/Futanari' => '56',
|
||||
'Feet' => '75',
|
||||
'Game Fan Art' => '63',
|
||||
'Gay' => '36',
|
||||
'GIF' => '42',
|
||||
'Group Sex/ Orgy' => '62',
|
||||
'Lesbian' => '67',
|
||||
'Mature' => '72',
|
||||
'Misc. Fan Art' => '68',
|
||||
'Monster' => '64',
|
||||
'Pin-Up' => '28',
|
||||
'Safe for Work' => '71',
|
||||
'SFM' => '70',
|
||||
'Solo' => '66',
|
||||
'Threesome' => '38',
|
||||
'TV & Film Fan Art' => '34',
|
||||
'Western Fan Art' => '33'
|
||||
)
|
||||
)
|
||||
),
|
||||
'Search' => array(
|
||||
'search_term' => array(
|
||||
'name' => 'Search term(s)',
|
||||
'required' => true,
|
||||
'exampleValue' => 'pole dance',
|
||||
'title' => 'Enter one or more search terms, separated by spaces'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function getName(){
|
||||
switch($this->queriedContext) {
|
||||
case 'Artist':
|
||||
return 'Slushe Artist: ' . $this->getInput('artist_name');
|
||||
break;
|
||||
case 'Category':
|
||||
return 'Slushe Category: ' . $this->getInput('category');
|
||||
break;
|
||||
case 'Search':
|
||||
return 'Slushe Search: ' . $this->getInput('search_term');
|
||||
break;
|
||||
default:
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
||||
|
||||
public function collectData(){
|
||||
switch($this->queriedContext) {
|
||||
case 'Artist':
|
||||
$uri = self::URI . '/' . $this->getInput('artist_name');
|
||||
break;
|
||||
case 'Category':
|
||||
$uri = self::URI . '/search/posts/channels?niche=' .
|
||||
$this->getInput('category');
|
||||
break;
|
||||
case 'Search':
|
||||
$uri = self::URI . '/search/posts/' . $this->getInput('search_term') .
|
||||
'?s=1';
|
||||
break;
|
||||
}
|
||||
|
||||
$headers = array(
|
||||
'Authority : slushe.com',
|
||||
'Cookie: age-verify=1;',
|
||||
'sec-ch-ua: "Chromium";v="100", " Not A;Brand";v="99"',
|
||||
'sec-ch-ua-mobile: ?0',
|
||||
'sec-ch-ua-platform: "Windows"',
|
||||
'sec-fetch-dest: document',
|
||||
'sec-fetch-mode: navigate',
|
||||
'sec-fetch-site: same-origin',
|
||||
'sec-fetch-user: ?1',
|
||||
'upgrade-insecure-requests: 1'
|
||||
);
|
||||
// Add user-agent string to headers with implode, due to line length limit
|
||||
$user_agent_string = [
|
||||
'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/',
|
||||
'537.36(KHTML, like Gecko) Chrome/100.0.4896.147 Safari/537.36'
|
||||
];
|
||||
$headers[] = implode('', $user_agent_string);
|
||||
|
||||
$html = getSimpleHTMLDOM($uri, $headers);
|
||||
|
||||
//Debug::log($html);
|
||||
//Debug::log($html->find('div.blog-item')[0]);
|
||||
|
||||
//Loop on each entry
|
||||
foreach($html->find('div.blog-item') as $element) {
|
||||
//Debug::log($element);
|
||||
|
||||
$title = $element->find('h3.title', 0)->first_child()->innertext;
|
||||
$article_uri = $element->find('h3.title', 0)->first_child()->href;
|
||||
$timestamp = $element->find('div.publication-date', 0)->innertext;
|
||||
$author = $element->find('div.artist', 0)->
|
||||
first_child()->first_child()->innertext;
|
||||
|
||||
// Create & populate item
|
||||
$item = array();
|
||||
$item['uri'] = $article_uri;
|
||||
$item['id'] = $item['uri'];
|
||||
$item['timestamp'] = $timestamp;
|
||||
$item['title'] = $title;
|
||||
$item['author'] = $author;
|
||||
|
||||
$media_html = '';
|
||||
|
||||
// Look for image thumbnails
|
||||
$media_uris = $element->find('div.thumb', 0);
|
||||
if (isset($media_uris)) {
|
||||
// Add gallery image count, if it exists
|
||||
$gallery_count = $media_uris->find('span.count', 0);
|
||||
if (isset($gallery_count)) {
|
||||
$media_html .= '<p>Gallery count: ' .
|
||||
$gallery_count->first_child()->innertext . '</p>';
|
||||
}
|
||||
// Add image thumbnail(s)
|
||||
foreach($media_uris->find('img') as $media_uri) {
|
||||
$media_html .= '<a href="' . $article_uri . '">' . $media_uri . '</a>';
|
||||
//Debug::log('Adding to enclosures: ' . str_replace(' ', '%20', $media_uri->src));
|
||||
$item['enclosures'][] = str_replace(' ', '%20', $media_uri->src);
|
||||
}
|
||||
}
|
||||
|
||||
// Look for video thumbnails
|
||||
$media_uris = $element->find('div.thumb-holder', 0);
|
||||
// Add video thumbnail(s)
|
||||
if (isset($media_uris)) {
|
||||
foreach($media_uris->find('img') as $media_uri) {
|
||||
$media_html .= '<p>Video:</p><a href="' .
|
||||
$article_uri . '">' . $media_uri . '</a>';
|
||||
//Debug::log('Adding to enclosures: ' . $media_uri->src);
|
||||
$item['enclosures'][] = $media_uri->src;
|
||||
}
|
||||
}
|
||||
$item['content'] = $media_html;
|
||||
|
||||
if(isset($item['title'])) {
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once('GelbooruBridge.php');
|
||||
|
||||
class TbibBridge extends GelbooruBridge {
|
||||
|
||||
|
@@ -67,7 +67,13 @@ class TorrentGalaxyBridge extends BridgeAbstract {
|
||||
$item = array();
|
||||
$item['uri'] = self::URI . $identity->href;
|
||||
$item['title'] = $identity->plaintext;
|
||||
$item['timestamp'] = DateTime::createFromFormat('d/m/y H:i', $creadate)->format('U');
|
||||
|
||||
// todo: parse date strings such as '1Hr ago' etc.
|
||||
$createdAt = DateTime::createFromFormat('d/m/y H:i', $creadate);
|
||||
if ($createdAt) {
|
||||
$item['timestamp'] = $createdAt->format('U');
|
||||
}
|
||||
|
||||
$item['author'] = $authorid->plaintext;
|
||||
$item['content'] = <<<HTML
|
||||
<h1>{$identity->plaintext}</h1>
|
||||
|
@@ -610,8 +610,8 @@ EOD;
|
||||
|
||||
try {
|
||||
$result = getContents($uri, $this->authHeaders, array(), true);
|
||||
} catch (UnexpectedResponseException $e) {
|
||||
switch ($e->getResponseCode()) {
|
||||
} catch (HttpException $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
case 403:
|
||||
if ($retries) {
|
||||
@@ -621,8 +621,8 @@ EOD;
|
||||
continue 2;
|
||||
}
|
||||
default:
|
||||
$code = $e->getResponseCode();
|
||||
$data = $e->getResponseBody();
|
||||
$code = $e->getCode();
|
||||
$data = $e->getMessage();
|
||||
returnServerError(<<<EOD
|
||||
Failed to make api call: $api
|
||||
HTTP Status: $code
|
||||
|
62
bridges/TwitterEngineeringBridge.php
Normal file
62
bridges/TwitterEngineeringBridge.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
class TwitterEngineeringBridge extends FeedExpander {
|
||||
|
||||
const MAINTAINER = 'corenting';
|
||||
const NAME = 'Twitter Engineering Blog';
|
||||
const URI = 'https://blog.twitter.com/engineering/';
|
||||
const DESCRIPTION = 'Returns the newest articles.';
|
||||
const CACHE_TIMEOUT = 21600; // 6h
|
||||
|
||||
protected function parseItem($item){
|
||||
$item = parent::parseItem($item);
|
||||
|
||||
$article_html = getSimpleHTMLDOMCached($item['uri']);
|
||||
if(!$article_html) {
|
||||
$item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
|
||||
return $item;
|
||||
}
|
||||
$article_html = defaultLinkTo($article_html, $this->getURI());
|
||||
|
||||
$article_body = $article_html->find('div.column.column-6', 0);
|
||||
|
||||
// Remove elements that are not part of article content
|
||||
$unwanted_selector = 'div.bl02-blog-post-text-masthead, div.tweet-error-text, div.bl13-tweet-template';
|
||||
foreach($article_body->find($unwanted_selector) as $found) {
|
||||
$found->outertext = '';
|
||||
}
|
||||
|
||||
// Set src for images
|
||||
foreach($article_body->find('img') as $found) {
|
||||
$found->setAttribute('src', $found->getAttribute('data-src'));
|
||||
}
|
||||
|
||||
$item['content'] = $article_body;
|
||||
$item['timestamp'] = strtotime($article_html->find('span.b02-blog-post-no-masthead__date', 0)->innertext);
|
||||
$item['categories'] = self::getCategoriesFromTags($article_html);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function getCategoriesFromTags($article_html){
|
||||
$tags_list_items = array($article_html->find('.post__tags > ul > li'));
|
||||
$categories = array();
|
||||
|
||||
foreach($tags_list_items as $tag_list_item) {
|
||||
foreach($tag_list_item as $tag) {
|
||||
$categories[] = trim($tag->plaintext);
|
||||
}
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
public function collectData(){
|
||||
$feed = static::URI . 'en_us/blog.rss';
|
||||
$this->collectExpandableDatas($feed);
|
||||
}
|
||||
|
||||
public function getName(){
|
||||
// Else the original feed returns "English (US)" as the title
|
||||
return 'Twitter Engineering Blog';
|
||||
}
|
||||
}
|
@@ -124,8 +124,8 @@ EOD
|
||||
)
|
||||
);
|
||||
|
||||
private $apiToken = null;
|
||||
private $authHeaders = array();
|
||||
// $Item variable needs to be accessible from multiple functions without passing
|
||||
private $item = array();
|
||||
|
||||
public function getName() {
|
||||
switch($this->queriedContext) {
|
||||
@@ -167,9 +167,9 @@ EOD
|
||||
$onlyMediaTweets = $this->getInput('imgonly');
|
||||
|
||||
// Read API token from config.ini.php, put into Header
|
||||
$this->apiToken = $this->getOption('twitterv2apitoken');
|
||||
$this->authHeaders = array(
|
||||
'authorization: Bearer ' . $this->apiToken,
|
||||
$apiToken = $this->getOption('twitterv2apitoken');
|
||||
$authHeaders = array(
|
||||
'authorization: Bearer ' . $apiToken,
|
||||
);
|
||||
|
||||
// Try to get all tweets
|
||||
@@ -180,7 +180,7 @@ EOD
|
||||
'user.fields' => 'pinned_tweet_id,profile_image_url'
|
||||
);
|
||||
$user = $this->makeApiCall('/users/by/username/'
|
||||
. $this->getInput('u'), $params);
|
||||
. $this->getInput('u'), $authHeaders, $params);
|
||||
|
||||
if(isset($user->errors)) {
|
||||
Debug::log('User JSON: ' . json_encode($user));
|
||||
@@ -209,7 +209,7 @@ EOD
|
||||
|
||||
// Get the tweets
|
||||
$data = $this->makeApiCall('/users/' . $user->data->id
|
||||
. '/tweets', $params);
|
||||
. '/tweets', $authHeaders, $params);
|
||||
break;
|
||||
|
||||
case 'By keyword or hashtag':
|
||||
@@ -231,7 +231,7 @@ EOD
|
||||
$params['query'] = $params['query'] . ' -is:retweet';
|
||||
}
|
||||
|
||||
$data = $this->makeApiCall('/tweets/search/recent', $params);
|
||||
$data = $this->makeApiCall('/tweets/search/recent', $authHeaders, $params);
|
||||
break;
|
||||
|
||||
case 'By list ID':
|
||||
@@ -246,7 +246,7 @@ EOD
|
||||
);
|
||||
|
||||
$data = $this->makeApiCall('/lists/' . $this->getInput('listid') .
|
||||
'/tweets', $params);
|
||||
'/tweets', $authHeaders, $params);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -291,13 +291,13 @@ EOD
|
||||
$retweetedData = null;
|
||||
$retweetedMedia = null;
|
||||
$retweetedUsers = null;
|
||||
if(!$hideImages && !$hideRetweets && isset($includesTweets)) {
|
||||
if(!$hideImages && isset($includesTweets)) {
|
||||
// There has to be a better PHP way to extract the tweet Ids?
|
||||
$includesTweetsIds = array();
|
||||
foreach($includesTweets as $includesTweet) {
|
||||
$includesTweetsIds[] = $includesTweet->id;
|
||||
}
|
||||
//Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds));
|
||||
Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds));
|
||||
|
||||
// Set default params for API query
|
||||
$params = array(
|
||||
@@ -309,7 +309,7 @@ EOD
|
||||
);
|
||||
|
||||
// Get the retweeted tweets
|
||||
$retweetedData = $this->makeApiCall('/tweets', $params);
|
||||
$retweetedData = $this->makeApiCall('/tweets', $authHeaders, $params);
|
||||
|
||||
// Extract retweets Media data into array
|
||||
isset($retweetedData->includes->media) ? $retweetedMedia
|
||||
@@ -329,15 +329,19 @@ EOD
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if Retweet or Reply
|
||||
$retweetTypes = array('retweeted', 'quoted');
|
||||
// Check if tweet is Retweet, Quote or Reply
|
||||
$isRetweet = false;
|
||||
$isReply = false;
|
||||
$isQuote = false;
|
||||
|
||||
if(isset($tweet->referenced_tweets)) {
|
||||
if(in_array($tweet->referenced_tweets[0]->type, $retweetTypes)) {
|
||||
$isRetweet = true;
|
||||
} elseif ($tweet->referenced_tweets[0]->type === 'replied_to') {
|
||||
$isReply = true;
|
||||
switch($tweet->referenced_tweets[0]->type) {
|
||||
case 'retweeted':
|
||||
$isRetweet = true; break;
|
||||
case 'quoted':
|
||||
$isQuote = true; break;
|
||||
case 'replied_to':
|
||||
$isReply = true; break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,20 +351,38 @@ EOD
|
||||
continue;
|
||||
}
|
||||
|
||||
$cleanedTweet = $tweet->text;
|
||||
$cleanedTweet = nl2br($tweet->text);
|
||||
//Debug::log('cleanedTweet: ' . $cleanedTweet);
|
||||
|
||||
// Perform filtering (skip tweets that don't contain desired word, if provided)
|
||||
// Perform optional keyword filtering (only keep tweet if keyword is found)
|
||||
if (! empty($tweetFilter)) {
|
||||
if(stripos($cleanedTweet, $this->getInput('filter')) === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize empty array to hold eventual HTML output
|
||||
$item = array();
|
||||
// Initialize empty array to hold feed item values
|
||||
$this->item = array();
|
||||
|
||||
// Start setting values needed for HTML output
|
||||
// Start getting and setting values needed for HTML output
|
||||
$quotedTweet = null;
|
||||
$cleanedQuotedTweet = null;
|
||||
$quotedUser = null;
|
||||
if ($isQuote) {
|
||||
Debug::log('Tweet is quote');
|
||||
foreach($includesTweets as $includesTweet) {
|
||||
if($includesTweet->id === $tweet->referenced_tweets[0]->id) {
|
||||
$quotedTweet = $includesTweet;
|
||||
$cleanedQuotedTweet = nl2br($quotedTweet->text);
|
||||
//Debug::log('Found quoted tweet');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$quotedUser = $this->getTweetUser($quotedTweet, $retweetedUsers, $includesUsers);
|
||||
}
|
||||
if($isRetweet || is_null($user)) {
|
||||
Debug::log('Tweet is retweet, or $user is null');
|
||||
// Replace tweet object with original retweeted object
|
||||
if($isRetweet) {
|
||||
foreach($includesTweets as $includesTweet) {
|
||||
@@ -377,73 +399,45 @@ EOD
|
||||
}
|
||||
|
||||
// Get user object for retweeted tweet
|
||||
$originalUser = new stdClass(); // make the linters stop complaining
|
||||
if(isset($retweetedUsers)) {
|
||||
foreach($retweetedUsers as $retweetedUser) {
|
||||
if($retweetedUser->id === $tweet->author_id) {
|
||||
$originalUser = $retweetedUser;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(isset($includesUsers)) {
|
||||
foreach($includesUsers as $includesUser) {
|
||||
if($includesUser->id === $tweet->author_id) {
|
||||
$originalUser = $includesUser;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$originalUser = $this->getTweetUser($tweet, $retweetedUsers, $includesUsers);
|
||||
|
||||
$item['username'] = $originalUser->username;
|
||||
$item['fullname'] = $originalUser->name;
|
||||
$this->item['username'] = $originalUser->username;
|
||||
$this->item['fullname'] = $originalUser->name;
|
||||
if(isset($originalUser->profile_image_url)) {
|
||||
$item['avatar'] = $originalUser->profile_image_url;
|
||||
$this->item['avatar'] = $originalUser->profile_image_url;
|
||||
} else{
|
||||
$item['avatar'] = null;
|
||||
$this->item['avatar'] = null;
|
||||
}
|
||||
} else{
|
||||
$item['username'] = $user->data->username;
|
||||
$item['fullname'] = $user->data->name;
|
||||
$item['avatar'] = $user->data->profile_image_url;
|
||||
$this->item['username'] = $user->data->username;
|
||||
$this->item['fullname'] = $user->data->name;
|
||||
$this->item['avatar'] = $user->data->profile_image_url;
|
||||
}
|
||||
$item['id'] = $tweet->id;
|
||||
$item['timestamp'] = $tweet->created_at;
|
||||
$item['uri']
|
||||
= self::URI . $item['username'] . '/status/' . $item['id'];
|
||||
$item['author'] = ($isRetweet ? 'RT: ' : '' )
|
||||
. $item['fullname']
|
||||
$this->item['id'] = $tweet->id;
|
||||
$this->item['timestamp'] = $tweet->created_at;
|
||||
$this->item['uri']
|
||||
= self::URI . $this->item['username'] . '/status/' . $this->item['id'];
|
||||
$this->item['author'] = ($isRetweet ? 'RT: ' : '' )
|
||||
. $this->item['fullname']
|
||||
. ' (@'
|
||||
. $item['username'] . ')';
|
||||
. $this->item['username'] . ')';
|
||||
|
||||
// Skip non-media tweet (if selected)
|
||||
// (Optional) Skip non-media tweet
|
||||
// This check must wait until after retweets are identified
|
||||
if ($onlyMediaTweets && !isset($tweet->attachments->media_keys)) {
|
||||
continue;
|
||||
if ($onlyMediaTweets && !isset($tweet->attachments->media_keys) &&
|
||||
(($isQuote && !isset($quotedTweet->attachments->media_keys)) || !$isQuote)) {
|
||||
// There is no media in current tweet or quoted tweet, skip to next
|
||||
continue;
|
||||
}
|
||||
|
||||
// Search for and replace URLs in Tweet text
|
||||
$foundUrls = false;
|
||||
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, $cleanedTweet, $url)) {
|
||||
$cleanedTweet = preg_replace($reg_ex,
|
||||
"<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
|
||||
$cleanedTweet);
|
||||
}
|
||||
$cleanedTweet = $this->replaceTweetURLs($tweet, $cleanedTweet);
|
||||
if (isset($cleanedQuotedTweet)) {
|
||||
Debug::log('Replacing URLs in Quoted Tweet text');
|
||||
$cleanedQuotedTweet = $this->replaceTweetURLs($quotedTweet, $cleanedQuotedTweet);
|
||||
}
|
||||
|
||||
// generate the title
|
||||
// Generate Title text
|
||||
if ($idAsTitle) {
|
||||
$titleText = $tweet->id;
|
||||
} else{
|
||||
@@ -456,117 +450,67 @@ EOD
|
||||
$titleText = 'R: ' . $titleText;
|
||||
}
|
||||
|
||||
$item['title'] = $titleText;
|
||||
$this->item['title'] = $titleText;
|
||||
|
||||
// Add avatar
|
||||
// Generate Avatar HTML block
|
||||
$picture_html = '';
|
||||
if(!$hideProfilePic && isset($item['avatar'])) {
|
||||
if(!$hideProfilePic && isset($this->item['avatar'])) {
|
||||
$picture_html = <<<EOD
|
||||
<a href="https://twitter.com/{$item['username']}">
|
||||
<a href="https://twitter.com/{$this->item['username']}">
|
||||
<img
|
||||
style="align:top; width:75px; border:1px solid black;"
|
||||
alt="{$item['username']}"
|
||||
src="{$item['avatar']}"
|
||||
title="{$item['fullname']}" />
|
||||
style="margin-right: 10px; margin-bottom: 10px;"
|
||||
alt="{$this->item['username']}"
|
||||
src="{$this->item['avatar']}"
|
||||
title="{$this->item['fullname']}" />
|
||||
</a>
|
||||
EOD;
|
||||
}
|
||||
|
||||
// Get images
|
||||
// Generate media HTML block
|
||||
$media_html = '';
|
||||
if(!$hideImages && isset($tweet->attachments->media_keys)) {
|
||||
|
||||
// Match media_keys in tweet to media list from, put matches
|
||||
// into new array
|
||||
$tweetMedia = array();
|
||||
// Start by checking the original list of tweet Media includes
|
||||
if(isset($includesMedia)) {
|
||||
foreach($includesMedia as $includesMedium) {
|
||||
if(in_array ($includesMedium->media_key,
|
||||
$tweet->attachments->media_keys)) {
|
||||
$tweetMedia[] = $includesMedium;
|
||||
}
|
||||
}
|
||||
$quoted_media_html = '';
|
||||
if(!$hideImages) {
|
||||
if (isset($tweet->attachments->media_keys)) {
|
||||
Debug::log('Generating HTML for tweet media');
|
||||
$media_html = $this->createTweetMediaHTML($tweet, $includesMedia, $retweetedMedia);
|
||||
}
|
||||
// If no matches found, check the retweet Media includes
|
||||
if(empty($tweetMedia) && isset($retweetedMedia)) {
|
||||
foreach($retweetedMedia as $retweetedMedium) {
|
||||
if(in_array ($retweetedMedium->media_key,
|
||||
$tweet->attachments->media_keys)) {
|
||||
$tweetMedia[] = $retweetedMedium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach($tweetMedia as $media) {
|
||||
switch($media->type) {
|
||||
case 'photo':
|
||||
if ($this->getInput('noimgscaling')) {
|
||||
$image = $media->url;
|
||||
$display_image = $media->url;
|
||||
} else{
|
||||
$image = $media->url . '?name=orig';
|
||||
$display_image = $media->url . '?name=thumb';
|
||||
}
|
||||
// add enclosures
|
||||
$item['enclosures'][] = $image;
|
||||
|
||||
$media_html .= <<<EOD
|
||||
<a href="{$image}">
|
||||
<img
|
||||
style="align:top; max-width:558px; border:1px solid black;"
|
||||
referrerpolicy="no-referrer"
|
||||
src="{$display_image}" />
|
||||
</a>
|
||||
EOD;
|
||||
break;
|
||||
case 'video':
|
||||
// To Do: Is there a way to easily match this
|
||||
// to a URL for a link?
|
||||
$display_image = $media->preview_image_url;
|
||||
|
||||
$media_html .= <<<EOD
|
||||
<img
|
||||
style="align:top; max-width:558px; border:1px solid black;"
|
||||
referrerpolicy="no-referrer"
|
||||
src="{$display_image}" />
|
||||
EOD;
|
||||
break;
|
||||
case 'animated_gif':
|
||||
// To Do: Is there a way to easily match this to a
|
||||
// URL for a link?
|
||||
$display_image = $media->preview_image_url;
|
||||
|
||||
$media_html .= <<<EOD
|
||||
<img
|
||||
style="align:top; max-width:558px; border:1px solid black;"
|
||||
referrerpolicy="no-referrer"
|
||||
src="{$display_image}" />
|
||||
EOD;
|
||||
break;
|
||||
default:
|
||||
Debug::log('Missing support for media type: '
|
||||
. $media->type);
|
||||
}
|
||||
if (isset($quotedTweet->attachments->media_keys)) {
|
||||
Debug::log('Generating HTML for quoted tweet media');
|
||||
$quoted_media_html = $this->createTweetMediaHTML($quotedTweet, $includesMedia, $retweetedMedia);
|
||||
}
|
||||
}
|
||||
|
||||
$item['content'] = <<<EOD
|
||||
<div style="display: inline-block; vertical-align: top;">
|
||||
// Generate the HTML for Item content
|
||||
$this->item['content'] = <<<EOD
|
||||
<div style="float: left;">
|
||||
{$picture_html}
|
||||
</div>
|
||||
<div style="display: inline-block; vertical-align: top;">
|
||||
<blockquote>{$cleanedTweet}</blockquote>
|
||||
</div>
|
||||
<div style="display: block; vertical-align: top;">
|
||||
<blockquote>{$media_html}</blockquote>
|
||||
<div style="display: table;">
|
||||
{$cleanedTweet}
|
||||
</div>
|
||||
<div style="display: block; margin-top: 16px;">
|
||||
{$media_html}
|
||||
EOD;
|
||||
|
||||
$item['content'] = htmlspecialchars_decode($item['content'], ENT_QUOTES);
|
||||
// Add Quoted Tweet HTML, if relevant
|
||||
if (isset($quotedTweet)) {
|
||||
$quotedTweetURI = self::URI . $quotedUser->username . '/status/' . $quotedTweet->id;
|
||||
$quote_html = <<<QUOTE
|
||||
<div style="display: table; border-style: solid; border-width: 1px;
|
||||
border-radius: 5px; padding: 5px;">
|
||||
<p><b>$quotedUser->name</b> @$quotedUser->username ·
|
||||
<a href="$quotedTweetURI">$quotedTweet->created_at</a></p>
|
||||
$cleanedQuotedTweet
|
||||
$quoted_media_html
|
||||
</div>
|
||||
QUOTE;
|
||||
$this->item['content'] .= $quote_html;
|
||||
}
|
||||
|
||||
// put out item
|
||||
$this->items[] = $item;
|
||||
$this->item['content'] = htmlspecialchars_decode($this->item['content'], ENT_QUOTES);
|
||||
|
||||
// Add current Item to Items array
|
||||
$this->items[] = $this->item;
|
||||
}
|
||||
|
||||
// Sort all tweets in array by date
|
||||
@@ -583,10 +527,160 @@ EOD;
|
||||
* @param $params array additional URI parmaeters
|
||||
* @return object json data
|
||||
*/
|
||||
private function makeApiCall($api, $params) {
|
||||
private function makeApiCall($api, $authHeaders, $params) {
|
||||
$uri = self::API_URI . $api . '?' . http_build_query($params);
|
||||
$result = getContents($uri, $this->authHeaders, array(), false);
|
||||
$result = getContents($uri, $authHeaders, array(), false);
|
||||
$data = json_decode($result);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change format of URLs in tweet text
|
||||
* @param $tweetObject object current Tweet JSON
|
||||
* @param $tweetText string current Tweet text
|
||||
* @return string modified tweet text
|
||||
*/
|
||||
private function replaceTweetURLs($tweetObject, $tweetText) {
|
||||
$foundUrls = false;
|
||||
// Rewrite URL links, based on URL list in tweet object
|
||||
if(isset($tweetObject->entities->urls)) {
|
||||
foreach($tweetObject->entities->urls as $url) {
|
||||
$tweetText = str_replace($url->url,
|
||||
'<a href="' . $url->expanded_url
|
||||
. '">' . $url->display_url . '</a>',
|
||||
$tweetText);
|
||||
}
|
||||
$foundUrls = true;
|
||||
}
|
||||
// Regex fallback for rewriting URL links. Should never trigger?
|
||||
if($foundUrls === false) {
|
||||
$reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
|
||||
if(preg_match($reg_ex, $tweetText, $url)) {
|
||||
$tweetText = preg_replace($reg_ex,
|
||||
"<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
|
||||
$tweetText);
|
||||
}
|
||||
}
|
||||
// Fix back-to-back URLs by adding a <br>
|
||||
$reg_ex = '/\/a>\s*<a/';
|
||||
$tweetText = preg_replace($reg_ex, '/a><br><a', $tweetText);
|
||||
|
||||
return $tweetText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find User object for Retweeted/Quoted tweet
|
||||
* @param $tweetObject object current Tweet JSON
|
||||
* @param $retweetedUsers
|
||||
* @param $includesUsers
|
||||
* @return object found User
|
||||
*/
|
||||
private function getTweetUser($tweetObject, $retweetedUsers, $includesUsers) {
|
||||
$originalUser = new stdClass(); // make the linters stop complaining
|
||||
if(isset($retweetedUsers)) {
|
||||
Debug::log('Searching for tweet author_id in $retweetedUsers');
|
||||
foreach($retweetedUsers as $retweetedUser) {
|
||||
if($retweetedUser->id === $tweetObject->author_id) {
|
||||
$matchedUser = $retweetedUser;
|
||||
Debug::log('Found author_id match in $retweetedUsers');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!isset($matchedUser->username) && isset($includesUsers)) {
|
||||
Debug::log('Searching for tweet author_id in $includesUsers');
|
||||
foreach($includesUsers as $includesUser) {
|
||||
if($includesUser->id === $tweetObject->author_id) {
|
||||
$matchedUser = $includesUser;
|
||||
Debug::log('Found author_id match in $includesUsers');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $matchedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for embedded media
|
||||
* @param $tweetObject object current Tweet JSON
|
||||
* @param $includesMedia
|
||||
* @param $retweetedMedia
|
||||
* @return string modified tweet text
|
||||
*/
|
||||
private function createTweetMediaHTML($tweetObject, $includesMedia, $retweetedMedia){
|
||||
$media_html = '';
|
||||
// Match media_keys in tweet to media list from, put matches into new array
|
||||
$tweetMedia = array();
|
||||
// Start by checking the original list of tweet Media includes
|
||||
if(isset($includesMedia)) {
|
||||
Debug::log('Searching for media_key in $includesMedia');
|
||||
foreach($includesMedia as $includesMedium) {
|
||||
if(in_array ($includesMedium->media_key,
|
||||
$tweetObject->attachments->media_keys)) {
|
||||
Debug::log('Found media_key in $includesMedia');
|
||||
$tweetMedia[] = $includesMedium;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no matches found, check the retweet Media includes
|
||||
if(empty($tweetMedia) && isset($retweetedMedia)) {
|
||||
Debug::log('Searching for media_key in $retweetedMedia');
|
||||
foreach($retweetedMedia as $retweetedMedium) {
|
||||
if(in_array ($retweetedMedium->media_key,
|
||||
$tweetObject->attachments->media_keys)) {
|
||||
Debug::log('Found media_key in $retweetedMedia');
|
||||
$tweetMedia[] = $retweetedMedium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach($tweetMedia as $media) {
|
||||
switch($media->type) {
|
||||
case 'photo':
|
||||
if ($this->getInput('noimgscaling')) {
|
||||
$image = $media->url;
|
||||
$display_image = $media->url;
|
||||
} else{
|
||||
$image = $media->url . '?name=orig';
|
||||
$display_image = $media->url;
|
||||
}
|
||||
// add enclosures
|
||||
$this->item['enclosures'][] = $image;
|
||||
|
||||
$media_html .= <<<EOD
|
||||
<a href="{$image}">
|
||||
<img
|
||||
referrerpolicy="no-referrer"
|
||||
src="{$display_image}" />
|
||||
</a>
|
||||
EOD;
|
||||
break;
|
||||
case 'video':
|
||||
// To Do: Is there a way to easily match this
|
||||
// to a direct Video URL?
|
||||
$display_image = $media->preview_image_url;
|
||||
|
||||
$media_html .= <<<EOD
|
||||
<p>Video:</p><a href="{$this->item['uri']}">
|
||||
<img referrerpolicy="no-referrer" src="{$display_image}" /></a>
|
||||
EOD;
|
||||
break;
|
||||
case 'animated_gif':
|
||||
// To Do: Is there a way to easily match this to a
|
||||
// direct animated Gif URL?
|
||||
$display_image = $media->preview_image_url;
|
||||
|
||||
$media_html .= <<<EOD
|
||||
<p>Animated Gif:</p><a href="{$this->item['uri']}">
|
||||
<img referrerpolicy="no-referrer" src="{$display_image}" /></a>
|
||||
EOD;
|
||||
break;
|
||||
default:
|
||||
Debug::log('Missing support for media type: '
|
||||
. $media->type);
|
||||
}
|
||||
}
|
||||
|
||||
return $media_html;
|
||||
}
|
||||
}
|
||||
|
187
bridges/UberNewsroomBridge.php
Normal file
187
bridges/UberNewsroomBridge.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
class UberNewsroomBridge extends BridgeAbstract {
|
||||
const NAME = 'Uber Newsroom Bridge';
|
||||
const URI = 'https://www.uber.com';
|
||||
const URI_API_DATA = 'https://newsroomapi.uber.com/wp-json/newsroom/v1/data?locale=';
|
||||
const URI_API_POST = 'https://newsroomapi.uber.com/wp-json/wp/v2/posts/';
|
||||
const DESCRIPTION = 'Returns news posts';
|
||||
const MAINTAINER = 'VerifiedJoseph';
|
||||
const PARAMETERS = array(array(
|
||||
'region' => array(
|
||||
'name' => 'Region',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'Africa' => array(
|
||||
'Egypt' => 'en-EG',
|
||||
'Ghana' => 'en-GH',
|
||||
'Kenya' => 'en-KE',
|
||||
'Nigeria' => 'en-NG',
|
||||
'South Africa' => 'en-ZA',
|
||||
'Tanzania' => 'en-TZ',
|
||||
'Uganda' => 'en-UG',
|
||||
),
|
||||
'Asia' => array(
|
||||
'Bangladesh' => 'en-BD',
|
||||
'Hong Kong' => 'en-HK',
|
||||
'India' => 'en-IN',
|
||||
'Japan' => 'ja-JP',
|
||||
'Korea' => 'en-KR',
|
||||
'Macau' => 'en-MO',
|
||||
'Sri Lanka' => 'en-LK',
|
||||
'Taiwan' => 'en-TW',
|
||||
),
|
||||
'Central America' => array(
|
||||
'Costa Rica' => 'es-CR',
|
||||
'Dominican Republic' => 'es-DO',
|
||||
'El Salvador' => 'es-SV',
|
||||
'Guatemala' => 'es-GT',
|
||||
'Honduras' => 'en-HN',
|
||||
'Mexico' => 'es-MX',
|
||||
'Nicaragua' => 'es-NI',
|
||||
'Panama' => 'es-PA',
|
||||
'Puerto Rico' => 'en-PR',
|
||||
),
|
||||
'Europe' => array(
|
||||
'Austria' => 'de-AT',
|
||||
'Azerbaijan' => 'az',
|
||||
'Belarus' => 'ru-BY',
|
||||
'Belgium' => 'en-BE',
|
||||
'Bulgaria' => 'en-BG',
|
||||
'Croatia' => 'hr',
|
||||
'Czech Republic' => 'cs-CZ',
|
||||
'Denmark' => 'en-DK',
|
||||
'Estonia' => 'en-EE',
|
||||
'Finland' => 'en-FI',
|
||||
'France' => 'en-FR',
|
||||
'Germany' => 'en-DE',
|
||||
'Greece' => 'en-GR',
|
||||
'Hungary' => 'en-HU',
|
||||
'Ireland' => 'en-IE',
|
||||
'Italy' => 'en-IT',
|
||||
'Kazakhstan' => 'ru-KZ',
|
||||
'Lithuania' => 'en-LT',
|
||||
'Netherlands' => 'en-NL',
|
||||
'Norway' => 'en-NO',
|
||||
'Poland' => 'pl',
|
||||
'Portugal' => 'en-PT',
|
||||
'Romania' => 'en-RO',
|
||||
'Russia' => 'ru',
|
||||
'Slovakia' => 'sk',
|
||||
'Spain' => 'es-ES',
|
||||
'Sweden' => 'en-SE',
|
||||
'Switzerland' => 'en-CH',
|
||||
'Turkey' => 'en-TR',
|
||||
'Ukraine' => 'uk-UA',
|
||||
'United Kingdom' => 'en-GB',
|
||||
),
|
||||
'Middle East' => array(
|
||||
'Bahrain' => 'en-BH',
|
||||
'Israel' => 'en-IL',
|
||||
'Jordan' => 'en-JO',
|
||||
'Lebanon' => 'en-LB',
|
||||
'Pakistan' => 'en-PK',
|
||||
'Qatar' => 'en-QA',
|
||||
'Saudi Arabia' => 'en-SA',
|
||||
'United Arab Emirates' => 'en-AE',
|
||||
),
|
||||
'North America' => array(
|
||||
'Canada' => 'en-CA',
|
||||
'United States' => 'en-US',
|
||||
),
|
||||
'Pacific' => array(
|
||||
'Australia' => 'en-AU',
|
||||
'New Zealand' => 'en-NZ',
|
||||
),
|
||||
'South America' => array(
|
||||
'Argentina' => 'es-AR',
|
||||
'Bolivia' => 'es-BO',
|
||||
'Brazil' => 'pt-BR',
|
||||
'Chile' => 'es-CL',
|
||||
'Colombia' => 'es-CO',
|
||||
'Ecuador' => 'es-EC',
|
||||
'Paraguay' => 'en-PY',
|
||||
'Peru' => 'es-PE',
|
||||
'Trinidad & Tobago' => 'en-TT',
|
||||
'Uruguay' => 'es-UY',
|
||||
'Venezuela' => 'en-VE',
|
||||
),
|
||||
),
|
||||
'defaultValue' => 'en-US',
|
||||
)
|
||||
));
|
||||
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
|
||||
public function collectData() {
|
||||
$json = getContents(self::URI_API_DATA . $this->getInput('region'));
|
||||
$data = json_decode($json);
|
||||
|
||||
foreach ($data->articles as $article) {
|
||||
$json = getContents(self::URI_API_POST . $article->id);
|
||||
$post = json_decode($json);
|
||||
|
||||
$item = array();
|
||||
$item['title'] = $post->title->rendered;
|
||||
$item['timestamp'] = $post->date;
|
||||
$item['uri'] = $post->link;
|
||||
$item['content'] = $this->formatContent($post->content->rendered);
|
||||
$item['enclosures'][] = $this->getImage($post->yoast_head);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
if (is_null($this->getInput('region')) === false && $this->getInput('region') !== 'all') {
|
||||
return self::URI . '/' . $this->getInput('region') . '/newsroom';
|
||||
}
|
||||
|
||||
return parent::getURI() . '/newsroom';
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
if (is_null($this->getInput('region')) === false) {
|
||||
return $this->getRegionName() . ' - Uber Newsroom';
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
private function getRegionName() {
|
||||
$parameters = $this->getParameters();
|
||||
|
||||
foreach ($parameters[0]['region']['values'] as $values) {
|
||||
foreach ($values as $name => $code) {
|
||||
|
||||
if ($code === $this->getInput('region')) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getImage($html) {
|
||||
$html = str_get_html($html);
|
||||
|
||||
if ($html->find('meta[property="og:image"]', 0)) {
|
||||
return $html->find('meta[property="og:image"]', 0)->content;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function formatContent($html) {
|
||||
$html = str_get_html($html);
|
||||
|
||||
foreach ($html->find('div.wp-video') as $div) {
|
||||
$div->style = '';
|
||||
}
|
||||
|
||||
foreach ($html->find('video') as $video) {
|
||||
$video->width = '100%';
|
||||
$video->height = '';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
@@ -17,7 +17,8 @@ class UnogsBridge extends BridgeAbstract {
|
||||
'What\'s New' => 'new last 7 days',
|
||||
'Expiring' => 'expiring'
|
||||
)
|
||||
)
|
||||
),
|
||||
'limit' => self::LIMIT,
|
||||
),
|
||||
'Global' => array(),
|
||||
'Country' => array(
|
||||
@@ -160,8 +161,17 @@ EOD;
|
||||
break;
|
||||
}
|
||||
|
||||
$api_url = self::URI . '/api/search?query=' . urlencode($feed)
|
||||
. ($country_code ? '&countrylist=' . $country_code : '') . '&limit=30';
|
||||
$limit = $this->getInput('limit') ?? 30;
|
||||
|
||||
// https://rapidapi.com/unogs/api/unogsng/details
|
||||
$api_url = sprintf(
|
||||
'%s/api/search?query=%s%s&limit=%s',
|
||||
self::URI,
|
||||
urlencode($feed),
|
||||
$country_code ? '&countrylist=' . $country_code : '',
|
||||
$limit
|
||||
);
|
||||
|
||||
$json_data = $this->getJSON($api_url);
|
||||
$movies = $json_data['results'];
|
||||
|
||||
|
@@ -103,7 +103,7 @@ class UnsplashBridge extends BridgeAbstract
|
||||
|
||||
public function getName()
|
||||
{
|
||||
$filteredUser = $this->getInput('u');
|
||||
$filteredUser = $this->getInput('u') ?? '';
|
||||
if (strlen($filteredUser) > 0) {
|
||||
return $filteredUser . ' - ' . self::NAME;
|
||||
} else {
|
||||
|
@@ -29,12 +29,12 @@ class UsbekEtRicaBridge extends BridgeAbstract {
|
||||
$fullarticle = $this->getInput('fullarticle');
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
$articles = $html->find('div.details');
|
||||
$articles = $html->find('article');
|
||||
|
||||
foreach($articles as $article) {
|
||||
$item = array();
|
||||
|
||||
$title = $article->find('div.card-title', 0);
|
||||
$title = $article->find('h2', 0);
|
||||
if($title) {
|
||||
$item['title'] = $title->plaintext;
|
||||
} else {
|
||||
@@ -47,7 +47,9 @@ class UsbekEtRicaBridge extends BridgeAbstract {
|
||||
$item['author'] = $author->plaintext;
|
||||
}
|
||||
|
||||
$uri = $article->find('a.read', 0)->href;
|
||||
$u = $article->find('a.card-img', 0);
|
||||
|
||||
$uri = $u->href;
|
||||
if(substr($uri, 0, 1) === 'h') { // absolute uri
|
||||
$item['uri'] = $uri;
|
||||
} else { // relative uri
|
||||
@@ -90,7 +92,7 @@ class UsbekEtRicaBridge extends BridgeAbstract {
|
||||
private function loadFullArticle($uri){
|
||||
$html = getSimpleHTMLDOMCached($uri);
|
||||
|
||||
$content = $html->find('section.main', 0);
|
||||
$content = $html->find('div.rich-text', 1);
|
||||
if($content) {
|
||||
return $this->replaceUriInHtmlElement($content);
|
||||
}
|
||||
|
68
bridges/UsenixBridge.php
Normal file
68
bridges/UsenixBridge.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class UsenixBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'USENIX';
|
||||
const URI = 'https://www.usenix.org/publications';
|
||||
const DESCRIPTION = 'Digital publications from USENIX (usenix.org)';
|
||||
const MAINTAINER = 'dvikan';
|
||||
const PARAMETERS = [
|
||||
'USENIX ;login:' => [
|
||||
],
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
if ($this->queriedContext === 'USENIX ;login:') {
|
||||
$this->collectLoginOnlineItems();
|
||||
return;
|
||||
}
|
||||
returnClientError('Illegal Context');
|
||||
}
|
||||
|
||||
private function collectLoginOnlineItems(): void
|
||||
{
|
||||
$url = 'https://www.usenix.org/publications/loginonline';
|
||||
$dom = getSimpleHTMLDOMCached($url);
|
||||
$items = $dom->find('div.view-content > div');
|
||||
|
||||
foreach ($items as $item) {
|
||||
$title = $item->find('.views-field-title > span', 0);
|
||||
$author = $item->find('.views-field-pseudo-author-list > span.field-content', 0);
|
||||
$relativeUrl = $item->find('.views-field-nothing-1 > span > a', 0);
|
||||
$uri = sprintf('https://www.usenix.org%s', $relativeUrl->href);
|
||||
// June 2, 2022
|
||||
$createdAt = $item->find('div.views-field-field-lv2-publication-date > div > span', 0);
|
||||
|
||||
$item = [
|
||||
'title' => $title->innertext,
|
||||
'author' => strstr($author->plaintext, ',', true) ?: $author->plaintext,
|
||||
'uri' => $uri,
|
||||
'timestamp' => $createdAt->innertext,
|
||||
];
|
||||
|
||||
$this->items[] = array_merge($item, $this->getItemContent($uri));
|
||||
}
|
||||
}
|
||||
|
||||
private function getItemContent(string $uri) : array
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached($uri);
|
||||
$content = $html->find('.paragraphs-items-full', 0)->innertext;
|
||||
$extra = $html->find('fieldset', 0);
|
||||
if (!empty($extra)) {
|
||||
$content .= $extra->innertext;
|
||||
}
|
||||
|
||||
$tags = [];
|
||||
foreach($html->find('.field-name-field-lv2-tags div.field-item') as $tag) {
|
||||
$tags[] = $tag->plaintext;
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
'categories' => $tags
|
||||
];
|
||||
}
|
||||
}
|
@@ -17,21 +17,24 @@ class ViadeoCompanyBridge extends BridgeAbstract {
|
||||
));
|
||||
|
||||
public function collectData(){
|
||||
$html = '';
|
||||
$link = self::URI . 'fr/company/' . $this->getInput('c');
|
||||
// Redirects to https://emploi.lefigaro.fr/recherche/entreprises
|
||||
$url = sprintf('%sfr/company/%s', self::URI, $this->getInput('c'));
|
||||
|
||||
$html = getSimpleHTMLDOM($link);
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
foreach($html->find('//*[@id="company-newsfeed"]/ul/li') as $element) {
|
||||
// TODO: Fix broken xpath selector
|
||||
$elements = $html->find('//*[@id="company-newsfeed"]/ul/li');
|
||||
|
||||
foreach($elements as $element) {
|
||||
$title = $element->find('p', 0)->innertext;
|
||||
if($title) {
|
||||
$item = array();
|
||||
$item['uri'] = $link;
|
||||
$item['title'] = mb_substr($element->find('p', 0)->innertext, 0, 100);
|
||||
$item['content'] = $element->find('p', 0)->innertext;;
|
||||
$this->items[] = $item;
|
||||
$i++;
|
||||
if(!$title) {
|
||||
continue;
|
||||
}
|
||||
$item = array();
|
||||
$item['uri'] = $url;
|
||||
$item['title'] = mb_substr($element->find('p', 0)->innertext, 0, 100);
|
||||
$item['content'] = $element->find('p', 0)->innertext;;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user