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

Compare commits

..

107 Commits

Author SHA1 Message Date
teromene
b0c7a62f74 [index] Bumped version to 2018-04-20 2018-04-20 17:15:25 +02:00
teromene
57b15a089e Added DiscogsBridge. Closes #615 2018-04-20 16:57:09 +02:00
teromene
4b7fbe4188 DansTonChatBridge: test before accessing plaintext 2018-04-19 21:00:18 +02:00
Teromene
2390fb58b3 Merge pull request #673 from GregThib/patch-1
DansTonChatBridge: Update to follow DTC website changes
2018-04-19 20:58:01 +02:00
Teromene
1e8d29f6ec Merge pull request #672 from em92/patch-3
[YoutubeBridge] Removed duration in titles on search mode
2018-04-19 20:56:34 +02:00
Eugene Molotov
644d13686c [YoutubeBridge] Removed duration in titles on search mode 2018-04-19 09:03:29 +05:00
teromene
aa0ff1c9b1 Added YGGTorrentBridge. 2018-04-18 21:57:27 +02:00
teromene
539d9f1f06 Add SupInfoBridge, fixes #668 2018-04-18 12:39:45 +02:00
teromene
5ece801ce7 Fix h* display size in HtmlFormat, and fix images being wider than the page. 2018-04-18 12:29:22 +02:00
GregThib
4dcea6d9c9 Update to follow DTC website changes
Now, entry title is optionnal and may be found in h3 HTML element.
Entry content is mandatory and may be found in div[class="item-content"] HTML element.

Moreover, the title may contain simple quotes (here, encoded) so the bridge have to decode first to apply format library function. In case we don't do that, the format function double encode the quote and something like ' could appear.
2018-04-18 12:00:00 +02:00
teromene
d69e2521f1 Removed T411 bridge. Website was closed nearly one year ago. 2018-04-18 11:44:54 +02:00
teromene
7927d73719 Rewrote DemonoidBridge. Fixes #626. 2018-04-17 15:25:02 +02:00
teromene
0620f30ae0 Changed the API key used for SoundCloud bridge. Should fix #599 2018-04-17 14:24:00 +02:00
teromene
795494cfce Added enclosures to InstagramBridge. 2018-04-16 19:34:21 +02:00
teromene
ba8542156c Remove usage of function file_get_contents. 2018-04-16 19:27:20 +02:00
Eugene Molotov
55f112e034 [VkBridge] Rewrited bridge code (#667)
* [VkBridge] Convert special HTML entities to characters in pageName

* [VkBridge] Generate feed item title

* [VkBridge] Remove double backslashes in feed item link

* [VkBridge] Unpin post if pinned

* [VkBridge] Mark reposted messages

* [VkBridge] Correct external link parsing

* [VkBridge] Added article parsing

* [VkBridge] Added video parsing

* [VkBridge] Added photo parsing

* [VkBridge] Added album link parsing

* [VkBridge] Added one more external link selector

* [VkBridge] Using array of link selectors to remove

* [VkBridge] Added document parsing

* [VkBridge] Added sign parsing

* [VkBridge] Fixed incorrect sorting with pinned item

* [VkBridge] More methods to parse documents

* [VkBridge] Save fallback if page name element not found

* [VkBridge] Using post signed as feed item author

* [VkBridge] Fixed document link

* [VkBridge] Coding policy fixes
2018-04-16 10:55:31 +01:00
Mitsukarenai
208fff801d [FDroid] minor fixes for Travis CI 2018-04-15 13:21:48 +02:00
Mitsukarenai
3c9860de43 [FDroid] new bridge 2018-04-15 13:13:10 +02:00
Adam Tygart
a16ec196c5 [NotAlways] Add a bridge for the NotAlways family of sites (#537)
NotAlways right found it necessary to remove their RSS feeds recently. This is a *simple* bridge to grab the ones on the front page. It allows you to filter the articles based on their classification (right, working, romantic, related, learning, friendly, hopeless, unfiltered, or all).
2018-04-15 12:02:37 +01:00
teromene
887fc7b037 Fix GoComics, website completely changed. Fixes #663 2018-04-14 18:15:44 +01:00
teromene
1bd4a40f71 Added GNOME Builder configuration to gitignore. 2018-04-14 18:15:13 +01:00
teromene
494169f959 Added bridge for Pixiv.
This bridge is slow, as caching of images is required (REFERER header required to access the full size images)
2018-04-14 16:19:35 +01:00
logmanoriginal
178177e787 [index] Push version to 2018-04-06 2018-04-06 22:45:33 +02:00
logmanoriginal
1cb83ccea3 [IPBBridge] Use limit for the number of items
The limit was used to specify the number of pages to return from a given
topic which resulted in the number of returned items variing between one
and however many entries are listed on one page.

This commit changes the implementation for the limit to keep loading more
pages until the specified limit is reached. Excessive elements are removed
in order to return the exact amount of items specified by the limit.

This behavior is closer to how other bridges are implemented and makes it
more natural to use without being too confusing. Existing queries must be
updated to account for the new limit.

References #657
2018-04-06 22:25:49 +02:00
sysadminstory
c899399569 [DealabsBridge] Follow the website changes (#660) 2018-04-06 21:25:41 +02:00
LogMANOriginal
0f93370e92 Merge pull request #654 from LogMANOriginal/cURL
Use cURL instead of file_get_contents
2018-04-06 20:49:58 +02:00
logmanoriginal
45c3dcb636 [VkBridge] Simplify header specification 2018-04-06 20:42:19 +02:00
logmanoriginal
ecfc220b10 [KernelBugTrackerBridge] Fix too many parameters requesting HTML DOM 2018-04-06 20:42:19 +02:00
logmanoriginal
4b3efed7ec [YoutubeBridge] Fix too many parameters when using HTML mode 2018-04-06 20:42:19 +02:00
logmanoriginal
bc28c5da8e [contents] Set CURLOPT_HTTPHEADER only if the provided array contains data 2018-04-06 20:42:19 +02:00
logmanoriginal
5bd9c1611d [contents] Limit cURL protocols to HTTP and HTTPS 2018-04-06 20:42:19 +02:00
logmanoriginal
6caca4946b bridges: Fix bridges with custom headers and options
This commit fixes bridges which called getContents, getSimpleHTMLDOM
or getSimpleHTMLDOMCached with custom settings.
2018-04-06 20:42:19 +02:00
logmanoriginal
ee78e7613f [contents] Replace file_get_contents by cURL
cURL is a powerful library specifically designed to connect to many
different types of servers with different types of protocols. For
more detailed information refer to the PHP cURL manual:

- http://php.net/manual/en/book.curl.php

Due to this change some parameters for the getContents function were
necessary (also applies to getSimpleHTMLDOM and getSimpleHTMLDOMCached):

> $use_include_path removed

  This parameter has never been used and doesn't even make sense in
  this context; If set to true file_get_contents would also search
  for files in the include_path (specified in php.ini).

> $context replaced by $header and $opts

  The $context parameter allowed for customization of the request in
  order to change how file_get_contents would acquire the data (i.e.
  using POST instead of GET, sending custom header, etc...)

  cURL also provides facilities to specify custom headers and change
  how it communicates to severs. cURL, however, is much more advanced.

  - $header is an optional parameter (empty by default). It receives
    an array of strings to send in the HTTP request header.

    See 'CURLOPT_HTTPHEADER':

    "An array of HTTP header fields to set, in the format
    array('Content-type: text/plain', 'Content-length: 100')"

    - php.net/manual/en/function.curl-setopt.php

  - $opts is an optional parameter (empty by default). It receives
    an array of options, where each option is a key-value-pair of
    a cURL option (CURLOPT_*) and it's associated parameter. This
    parameter accepts any of the CURLOPT_* settings.

    Example (sending POST instead of GET):

    $opts = array(
      CURLOPT_POST => 1,
      CURLOPT_POSTFIELDS => '&action=none'
    );
    $html = getContents($url, array(), $opts);

    Refer to the cURL setopt manual for more information:
    - php.net/manual/en/function.curl-setopt.php

> $offset and $maxlen removed

  These options were supported by file_get_contents, but there doesn't
  seem to be an equivalent in cURL. Since no caller uses them they are
  safe to remove.

Compressed data / Encoding

  By using cURL instead of file_get_contents RSS-Bridge no longer has
  to handle compressed data manually.

  See 'CURLOPT_ENCODING':

  "[...] Supported encodings are "identity", "deflate", and "gzip".
  If an empty string, "", is set, a header containing all supported
  encoding types is sent."

  - http://php.net/manual/en/function.curl-setopt.php

  Notice: By default all encoding types are accepted (""). This can
  be changed by setting a custom option via $opts.

    Example:

    $opts = array(CURLOPT_ENCODING => 'gzip');
    $html = getContents($url, array(), $opts);

Proxy

The proxy implementation should still work, but there doesn't seem
to be an equivalent for 'request_fulluri = true'. To my understanding
this isn't an issue because cURL knows how to handle proxy communication.
2018-04-06 20:42:19 +02:00
logmanoriginal
2df2623430 [index] Add 'curl' extension check 2018-04-06 20:42:19 +02:00
logmanoriginal
de5f850cdb [index] Fix indentation using tabs 2018-04-06 20:34:44 +02:00
teromene
ac6847045c Catch Errors in order to display a message in more cases. We also catch Exceptions to maintain compat with php 5.
Add check for simplexml extension.
2018-04-04 19:02:40 +01:00
logmanoriginal
df6da837dc [FacebookBridge] Return error if username starts with slash
Requesting a username with a leading slash would cause error 500
because the requested URI would contain two slashes in a row.

For example username "/test" would result in:
https://facebook.com//test

References #628
2018-03-23 21:23:30 +01:00
Eugene Molotov
41b7984a4e [YoutubeBridge] Playlist mode: faster feed generating if item count is less or equal to 15 (#648)
* [YoutubeBridge] Playlist mode: faster feed generating if item count is less or equal to 15
2018-03-19 12:41:52 +00:00
teromene
38c7e0272e Add hashtag support to InstagramBridge.
Fixes  #629
2018-03-19 12:29:24 +00:00
teromene
29c690dbcd Fix InstagramBridge, thanks to @pintassilgo comments.
Fixes #646
2018-03-19 12:17:42 +00:00
LogMANOriginal
8ba817478b Implement customizable cache timeout (#641)
* [BridgeAbstract] Implement customizable cache timeout

The customizable cache timeout is used instead of the default cache
timeout (CACHE_TIMEOUT) if specified by the caller.

* [index] Add new global parameter '_cache_timeout'

The _cache_timeout parameter is an optional parameter that can be
used to specify a custom cache timeout. This option is enabled by
default.

It can be disabled using the named constant 'CUSTOM_CACHE_TIMEOUT'
which supports two states:

> true: Enabled (default)
> false: Disabled

* [BridgeAbstract] Change scope of 'getCacheTimeout' to public

* [html] Add cache timeout parameter to all bridges

The timeout parameter only shows if CUSTOM_CACHE_TIMEOUT has been set
to true. The default value is automatically set to the value specified
in the bridge.

* [index] Disable custom cache timeout by default
2018-03-14 18:06:36 +01:00
Eugene Molotov
cacbe90102 [YoutubeBridge] Sort playlist items by publication date (#643) 2018-03-13 11:24:40 +00:00
Antoine Cadoret
cb91cd5d2f Fix SteamBridge (#637) (#639)
Fixes #639
2018-03-12 09:22:34 +00:00
sysadminstory
52dfa3fe76 [RadioMelodieBridge] Add new bridge (#640) 2018-03-11 15:38:07 +01:00
logmanoriginal
29a1c7ac09 [index.php] Add extension check for 'mbstring'
The mbstring extension is required by all formats in order to convert multi-
byte characters to UTF-8. This commit adds an extension check to throw an
error message if the extension is not enabled.
2018-03-07 19:11:47 +01:00
teromene
6eea51eeeb Fix SteamBridge.
Fixes #636
2018-03-07 10:24:33 +00:00
teromene
2149af0e74 Fix Pinterest bridge, remove the old JSON parsing, and return original sized image.
Fixes #632
2018-03-06 12:01:48 +00:00
teromene
142a647b7a Merge branch 'master' of github.com:RSS-Bridge/rss-bridge 2018-03-06 11:27:37 +00:00
teromene
6e916ddd35 Fix Arte7Bridge.
Fixes #633
2018-03-06 11:26:16 +00:00
Eugene Molotov
159b00145d [VkBridge] Setting feed title (#635)
* [VkBridge] Setting feed title
2018-03-05 09:46:15 +00:00
Mitsukarenai
26ce16baa2 [PlanetLibre] remove bridge (origin now has RSS) 2018-03-03 21:04:40 +01:00
sysadminstory
0622fe142b Dealabs : Added Groupes Feeds and Feed name is set according to parameters (#630)
* [DealabsBride] Added Groupes Feeds
2018-03-01 17:10:34 +00:00
logmanoriginal
4805b52d42 [YoutubeBridge] Fix typo 2018-02-16 22:35:00 +01:00
logmanoriginal
962617086e [YoutubeBridge] Remove superfluous div selectors 2018-02-16 22:31:47 +01:00
logmanoriginal
4f6277b6b5 [YoutubeBridge] Fix parsing author name breaks the bridge
The author name is parsed by searching a string within the entire
HTML document:

$author = $html->innertext;
$author = substr($author, strpos($author, '"author=') + 8);
$author = substr($author, 0, strpos($author, '\u0026'));

This solution will return big portions of the HTML document if
the strpos function returns zero (not found).

This commit replaces the previous implementation by searching for
a specific script tag and making use of the JSON data inside it.

References #580
2018-02-16 22:31:29 +01:00
logmanoriginal
5aaab9eb8c [YoutubeBridge] Skip unavailable videos 2018-02-16 22:11:03 +01:00
sysadminstory
ef402bb5c3 [DealabsBride] Fix for the new site (#595)
* [DealabsBride] Fix for the new site
2018-02-14 11:03:44 +00:00
LogMANOriginal
85ac9001d6 [IPBBridge] Add bridge (#564)
This bridge returns feeds for any URI that is compatible with the
IPB implementation (currently 4.x). Older versions might work, but
there is no guarantee.

Only forum and topic URIs are supported!

The bridge automatically checks if natural feeds are available (by
adding '.xml' to the URI). If so the feed is returned. Otherwise
the bridge will attempt to identify the content type and build a
feed accordingly.

Valid URIs are forums and topics. For forums the first page is
returned, for topics the last one. Elements are ordered such that
the latest entry is returned first (oldest-to-newest)

The optional parameter '&limit=' specifies how many pages should
be loaded (default: 1). Topics are loaded in reverse order.
=> Does not work with forums!

Images are provided as enclosures and scaled to a max-size of
400x400 pixels by default (Except for natural feeds).

The content is filtered before being returned:
- Unnecessary tags are removed (iframes, etc...)
- Styles for blockquotes are restored (grey background)

Closes #507
2018-02-13 21:46:33 +01:00
Mitsukarenai
7939bffcdd fix: TébéoBridge Travis cleanup 2018-02-11 19:08:19 +01:00
Mitsukarenai
bb58aa8e31 New bridge: Tébéo 2018-02-11 16:56:34 +01:00
Ruslan
1d35149191 Update VkBridge (#625) 2018-01-30 16:57:07 +00:00
Tameroski
be03764029 Fixing double quote issue at the end of URL (#623) 2018-01-23 11:27:45 +00:00
Matt DeMoss
a07874d468 Initial commit for Bloomberg bridge with top stories and search (#607)
* initial commit for Bloomberg bridge with top stories and search
2018-01-12 12:08:15 +00:00
Matt DeMoss
90d7ae8776 Fix twitter list filter test #613, fix and change getName() for lists. (#614) 2018-01-12 12:07:40 +00:00
Teromene
93e0562353 Merge pull request #610 from mdemoss/YouTubeTitle-#609
You tube title fix for #609
2018-01-11 12:09:38 +00:00
Teromene
4c5d547d9c Merge pull request #608 from mdemoss/PcGamerBridge
Pc gamer bridge
2018-01-11 12:08:10 +00:00
Teromene
9a3a64010f Merge pull request #620 from RSS-Bridge/teromene-patch-2
Update MixCloudBridge.php
2018-01-11 11:48:29 +00:00
Teromene
e59a6f4c9e Update MixCloudBridge.php
Fix whitespace at start of line
2018-01-11 11:44:51 +00:00
Teromene
1506e68587 Merge pull request #619 from RSS-Bridge/teromene-patch-1
Update .travis.yml
2018-01-11 11:44:37 +00:00
Teromene
671cba4f68 Update .travis.yml
Try to fix build failure
2018-01-11 11:41:25 +00:00
Teromene
374eb8f4bf Merge pull request #617 from adamchainz/patch-1
README - sort lists alphabetically
2018-01-10 14:05:05 +00:00
Adam Johnson
60f7a2b3e4 README - sort lists alphabetically
This makes them easier to scan and check "does rss-bridge support service X I'm interested in?" :)
2018-01-10 11:45:55 +00:00
Teromene
7744172c63 Merge pull request #616 from lalannev/patch-1
Update LegifranceJOBridge.php
2018-01-09 17:19:28 +00:00
lalannev
5a763aee8d Update LegifranceJOBridge.php 2018-01-09 14:57:17 +01:00
Matt DeMoss
c14b2c6905 address phpcs style errors 2017-12-28 20:20:24 -05:00
Matt DeMoss
0871376922 store feed name in new variable, switch getName on queriedContext, remove 'bridge' from name for feeds, fixes #609 2017-12-28 20:20:24 -05:00
Matt DeMoss
c5fe9a6dc0 mark places where a new variable is needed 2017-12-28 20:20:24 -05:00
Matt DeMoss
fbbcd02384 apply phpcbf for automatic style fixes 2017-12-24 16:45:56 -05:00
Matt DeMoss
d34987f9c1 PC Gamer bridge initial commit with most read stories 2017-12-24 16:40:59 -05:00
Teromene
9e0565c655 Merge pull request #604 from TwizzyDizzy/master
Fix double forward-slash in returned post URI leading to 404
2017-12-14 16:43:33 +00:00
Thomas Dalichow
443081c90b Fix double forward-slash in returned post URI leading to 404 2017-12-06 22:17:46 +01:00
Teromene
03fc09e3c6 Merge pull request #602 from TwizzyDizzy/master
Fake user agent as Mixcloud blocks certain User-Agents
2017-12-01 17:29:26 +00:00
Thomas Dalichow
45323c2b2f Fake user agent as Mixcloud blocks certain User-Agents 2017-12-01 17:28:57 +01:00
Teromene
67ee73782c Merge pull request #582 from sysadminstory/master
[DealabsBridge] Add new bridge
2017-10-18 10:53:46 +01:00
sysadminstory
2bb9a29ddc Delete usefull whitespace 2017-10-17 23:37:09 +02:00
sysadminstory
5cbd363597 Coding style fix
Fixed the bridge to follow the project coding style
2017-10-17 23:30:27 +02:00
Teromene
aa6ded0ea4 Merge pull request #593 from b1nj/master
Update saison AllocineFRBridge
2017-10-17 19:02:54 +01:00
sysadminstory
3c61dc2b57 Merge remote-tracking branch 'upstream/master' 2017-10-17 14:53:22 +02:00
B1nj
3e528ddccf Update saisons AllocineFRBridge 2017-10-16 22:24:49 -04:00
teromene
cba65d6d08 [Arte7Bridge] Fix Arte7 bridge, use the API 2017-10-12 18:12:31 +01:00
Teromene
8d418611a2 Merge pull request #589 from mickael-bertrand/patch-2
Updater torrent9 URI
2017-10-12 17:18:04 +01:00
teromene
98b0f0f8ba [Core] Verify the presence of the array keys before accessing them.
Fixes  #588
2017-10-12 17:14:34 +01:00
Teromene
6f66e6d9be Merge pull request #592 from ldidry/fix-gocomics
Update GoComicsBridge
2017-10-11 11:34:37 +01:00
Luc Didry
8b06299bad Update GoComicsBridge 2017-10-11 10:03:29 +02:00
MickaëlBERTRAND
5a99981827 Updater torrent9 URI 2017-10-08 19:21:10 +02:00
Teromene
e30ad3feb4 Add support for running rss-bridge from the CLI 2017-09-25 19:14:02 +02:00
logmanoriginal
77657a9154 [.travis.yml] Refactor script 2017-09-24 18:45:58 +02:00
logmanoriginal
3059b1ea80 [YoutubeBridge] Skip Ads
The search might return unrelated videos (Ads) that are inserted
between regular search results. This adds a check to skip Ads.

Closes #571
2017-09-24 17:25:47 +02:00
LogMANOriginal
4037c34393 [TwitterBridge] Add category for lists (#545)
This adds a new option to generate feeds from Twitter lists using
an optional filter (string comparison).
2017-09-24 16:59:45 +02:00
logmanoriginal
e671a2ad02 [.travis.yml] Fix configuration to work with Ubuntu Trusty
Travis CI upgraded the linux build environment from Ubuntu Precise
to Ubuntu Trusty with Trusty becoming the default build environment
as of August 2017:

  https://docs.travis-ci.com/user/reference/overview/

A bug in the configuration of the Ubuntu Trusty distro causes all
builds except nightly to fail. The PHP include_path is set to

  include_path='.:/home/travis/.phpenv/versions/5.6.31/share/pear'

instead of

  include_path='.:/home/travis/.phpenv/versions/5.6.31/lib/php/pear'

which causes phpcs to fail because it cannot resolve import paths.

This commit adds a hotfix to .travis.yml that circumvents the
issue by overwriting the include_path during initialization. This
hotfix should be removed once a solution is found.

This bug is tracked via
https://github.com/travis-ci/travis-ci/issues/8487
2017-09-23 20:48:28 +02:00
logmanoriginal
1ea091f215 [.travis.yml] Fix Tavis CI build error
Travis CI recently updated the default distribution from Ubuntu
Precise to Ubuntu Trusty, which causes all builds except nightly
to fail.

For unknown reasons phpcs is unable to locate "PHP/CodeSniffer/
autoload.php" causing it to fail with a fatal error

The root cause of the failure is unknown. We explicitly return to
the previous build system (Ubuntu Precise) for builds to work again.

See the migration guide for reference:
https://docs.travis-ci.com/user/precise-to-trusty-migration-guide/

Notice: Ubuntu Precise is retired as of September 2017 and will be
decommissioned in the near future:
https://docs.travis-ci.com/user/reference/overview/
2017-09-23 18:38:47 +02:00
logmanoriginal
87fa4ae3ac [.travis.yml] Fix warning channel pear.php.net has updated its protocol 2017-09-23 18:05:09 +02:00
sysadminstory
d7a1dca004 [DealabsBridge] Conform to coding policy
- If no there are no results, an explicit message is now returned
- Commas are now following the coding policy
- Lines are no longer more than 80 chars when possible
2017-09-19 02:08:22 +02:00
sysadminstory
fe48340327 [DealabsBridge] Add new bridge 2017-09-05 21:03:21 +02:00
logmanoriginal
b4c6aa41a7 [index] Return error if no format is specified when requesting a bridge 2017-08-28 20:45:06 +02:00
metaMMA
1696aee212 [DemonoidBridge] Add new bridge 2017-08-28 20:00:52 +02:00
metaMMA
585379d47a [ThePirateBayBridge] Add instructions
Added additional instructions for: 'username search' and 'category
search' next to instructions for 'keyword search'.

Changed variable name from underscore to camelCase.
2017-08-28 20:00:00 +02:00
42 changed files with 2361 additions and 497 deletions

5
.gitignore vendored
View File

@@ -231,4 +231,7 @@ DEBUG
######################
## VisualStudioCode ##
######################
.vscode/*
.vscode/*
#Builder
.buildconfig

View File

@@ -1,11 +1,9 @@
dist: trusty
sudo: false
language: php
php:
- '5.6'
- '7.0'
- hhvm
- nightly
install:
- pear channel-update pear.php.net
- pear install PHP_CodeSniffer
script:
@@ -14,6 +12,13 @@ script:
matrix:
fast_finish: true
include:
- php: 5.6
- php: 7.0
- php: hhvm
- php: nightly
allow_failures:
- php: hhvm
- php: nightly
- php: nightly

View File

@@ -7,23 +7,23 @@ rss-bridge is a PHP project capable of generating ATOM feeds for websites which
Supported sites/pages (main)
===
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
* `GoogleSearch` : Most recent results from Google Search
* `GooglePlus` : Most recent posts of user timeline
* `Twitter` : Return keyword/hashtag search or user timeline
* `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
* `YouTube` : YouTube user channel, playlist or search
* `Cryptome` : Returns the most recent documents from [Cryptome.org](http://cryptome.org/)
* `DansTonChat`: Most recent quotes from [danstonchat.com](http://danstonchat.com/)
* `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
* `Instagram`: Most recent photos from an Instagram user
* `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
* `Pinterest`: Most recent photos from user or search
* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://secouchermoinsbete.fr/)
* `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
* `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
* `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
* `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/)
* `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/)
* `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/)
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
* `GooglePlus` : Most recent posts of user timeline
* `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
* `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
* `Pinterest`: Most recent photos from user or search
* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://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
* `YouTube` : YouTube user channel, playlist or search
Plus [many other bridges](bridges/) to enable, thanks to the community
@@ -31,11 +31,11 @@ Output format
===
Output format can take several forms:
* `Atom` : ATOM Feed, for use in RSS/Feed readers
* `Mrss` : MRSS Feed, for use in RSS/Feed readers
* `Json` : Json, for consumption by other applications.
* `Html` : Simple html page.
* `Plaintext` : raw text (php object, as returned by print_r)
* `Atom` : ATOM Feed, for use in RSS/Feed readers
* `Html` : Simple html page.
* `Json` : Json, for consumption by other applications.
* `Mrss` : MRSS Feed, for use in RSS/Feed readers
* `Plaintext` : raw text (php object, as returned by print_r)
Screenshot
===

View File

@@ -26,7 +26,7 @@ class AllocineFRBridge extends BridgeAbstract {
switch($this->getInput('category')) {
case 'faux-raccord':
$uri = static::URI . 'video/programme-12284/saison-29841/';
$uri = static::URI . 'video/programme-12284/saison-32180/';
break;
case 'top-5':
$uri = static::URI . 'video/programme-12299/saison-29561/';
@@ -64,7 +64,7 @@ class AllocineFRBridge extends BridgeAbstract {
self::PARAMETERS[$this->queriedContext]['category']['values']
);
foreach($html->find('figure.media-meta-fig') as $element) {
foreach($html->find('.media-meta-list figure.media-meta-fig') as $element) {
$item = array();
$title = $element->find('div.titlebar h3.title a', 0);

View File

@@ -3,24 +3,28 @@ class Arte7Bridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Arte +7';
const URI = 'http://www.arte.tv/';
const URI = 'https://www.arte.tv/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns newest videos from ARTE +7';
const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
const PARAMETERS = array(
'Catégorie (Français)' => array(
'catfr' => array(
'type' => 'list',
'name' => 'Catégorie',
'values' => array(
'Toutes les vidéos (français)' => 'toutes-les-videos',
'Actu & société' => 'actu-société',
'Séries & fiction' => 'séries-fiction',
'Cinéma' => 'cinéma',
'Arts & spectacles classiques' => 'arts-spectacles-classiques',
'Culture pop' => 'culture-pop',
'Découverte' => 'découverte',
'Histoire' => 'histoire',
'Junior' => 'junior'
'Toutes les vidéos (français)' => null,
'Actu & société' => 'ACT',
'Séries & fiction' => 'SER',
'Cinéma' => 'CIN',
'Arts & spectacles classiques' => 'ARS',
'Culture pop' => 'CPO',
'Découverte' => 'DEC',
'Histoire' => 'HIST',
'Science' => 'SCI',
'Autre' => 'AUT'
)
)
),
@@ -29,15 +33,16 @@ class Arte7Bridge extends BridgeAbstract {
'type' => 'list',
'name' => 'Catégorie',
'values' => array(
'Alle Videos (deutsch)' => 'alle-videos',
'Aktuelles & Gesellschaft' => 'aktuelles-gesellschaft',
'Fernsehfilme & Serien' => 'fernsehfilme-serien',
'Kino' => 'kino',
'Kunst & Kultur' => 'kunst-kultur',
'Popkultur & Alternativ' => 'popkultur-alternativ',
'Entdeckung' => 'entdeckung',
'Geschichte' => 'geschichte',
'Junior' => 'junior'
'Alle Videos (deutsch)' => null,
'Aktuelles & Gesellschaft' => 'ACT',
'Fernsehfilme & Serien' => 'SER',
'Kino' => 'CIN',
'Kunst & Kultur' => 'ARS',
'Popkultur & Alternativ' => 'CPO',
'Entdeckung' => 'DEC',
'Geschichte' => 'HIST',
'Wissenschaft' => 'SCI',
'Sonstiges' => 'AUT'
)
)
)
@@ -55,44 +60,37 @@ class Arte7Bridge extends BridgeAbstract {
break;
}
$url = self::URI . 'guide/' . $lang . '/plus7/' . $category;
$input = getContents($url) or die('Could not request ARTE.');
$url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=10&language='
. $lang
. ($category != null ? '&category.code=' . $category : '');
if(strpos($input, 'categoryVideoSet') !== false) {
$input = explode('categoryVideoSet="', $input);
$input = explode('}}', $input[1]);
$input = $input[0] . '}}';
} else {
$input = explode('videoSet="', $input);
$input = explode('}]}', $input[1]);
$input = $input[0] . '}]}';
}
$header = array(
'Authorization: Bearer ' . self::API_TOKEN
);
$input_json = json_decode(html_entity_decode($input, ENT_QUOTES), true);
$input = getContents($url, $header) or die('Could not request ARTE.');
$input_json = json_decode($input, true);
foreach($input_json['videos'] as $element) {
$item = array();
$item['uri'] = str_replace("autoplay=1", "", $element['url']);
$item['uri'] = $element['url'];
$item['id'] = $element['id'];
$hack_broadcast_time = $element['rights_end'];
$hack_broadcast_time = strtok($hack_broadcast_time, 'T');
$hack_broadcast_time = strtok('T');
$item['timestamp'] = strtotime($element['scheduled_on'] . 'T' . $hack_broadcast_time);
$item['timestamp'] = strtotime($element['videoRightsBegin']);
$item['title'] = $element['title'];
if(!empty($element['subtitle']))
$item['title'] = $element['title'] . ' | ' . $element['subtitle'];
$item['duration'] = round((int)$element['duration'] / 60);
$item['content'] = $element['teaser']
$item['duration'] = round((int)$element['durationSeconds'] / 60);
$item['content'] = $element['teaserText']
. '<br><br>'
. $item['duration']
. 'min<br><a href="'
. $item['uri']
. '"><img src="'
. $element['thumbnail_url']
. $element['mainImage']['url']
. '" /></a>';
$this->items[] = $item;

View File

@@ -0,0 +1,65 @@
<?php
class BloombergBridge extends BridgeAbstract
{
const NAME = 'Bloomberg';
const URI = 'https://www.bloomberg.com/';
const DESCRIPTION = 'Trending stories from Bloomberg';
const MAINTAINER = 'mdemoss';
const PARAMETERS = array(
'Trending Stories' => array(),
'From Search' => array(
'q' => array(
'name' => 'Keyword',
'required' => true
)
)
);
public function getName()
{
switch($this->queriedContext) {
case 'Trending Stories':
return self::NAME . ' Trending Stories';
case 'From Search':
if (!is_null($this->getInput('q'))) {
return self::NAME . ' Search : ' . $this->getInput('q');
}
break;
}
return parent::getName();
}
public function collectData()
{
switch($this->queriedContext) {
case 'Trending Stories': // Get list of top new <article>s from the front page.
$html = getSimpleHTMLDOMCached($this->getURI(), 300);
$stories = $html->find('ul.top-news-v3__stories article.top-news-v3-story');
break;
case 'From Search': // Get list of <article> elements from search.
$html = getSimpleHTMLDOMCached(
$this->getURI() .
'search?sort=time:desc&page=1&query=' .
urlencode($this->getInput('q')), 300
);
$stories = $html->find('div.search-result-items article.search-result-story');
break;
}
foreach ($stories as $element) {
$item['uri'] = $element->find('h1 a', 0)->href;
if (preg_match('#^https://#i', $item['uri']) !== 1) {
$item['uri'] = $this->getURI() . $item['uri'];
}
$articleHtml = getSimpleHTMLDOMCached($item['uri']);
if (!$articleHtml) {
continue;
}
$item['title'] = $element->find('h1 a', 0)->plaintext;
$item['timestamp'] = strtotime($articleHtml->find('meta[name=iso-8601-publish-date],meta[name=date]', 0)->content);
$item['content'] = $articleHtml->find('meta[name=description]', 0)->content;
$this->items[] = $item;
}
}
}

View File

@@ -15,8 +15,13 @@ class DansTonChatBridge extends BridgeAbstract {
foreach($html->find('div.item') as $element) {
$item = array();
$item['uri'] = $element->find('a', 0)->href;
$item['title'] = 'DansTonChat ' . $element->find('a', 1)->plaintext;
$item['content'] = $element->find('a', 0)->innertext;
$titleContent = $element->find('h3 a', 0);
if($titleContent) {
$item['title'] = 'DansTonChat ' . html_entity_decode($titleContent->plaintext, ENT_QUOTES);
} else {
$item['title'] = 'DansTonChat';
}
$item['content'] = $element->find('div.item-content a', 0)->innertext;
$this->items[] = $item;
}
}

471
bridges/DealabsBridge.php Normal file
View File

@@ -0,0 +1,471 @@
<?php
class DealabsBridge extends BridgeAbstract {
const NAME = 'Dealabs search bridge';
const URI = 'https://www.dealabs.com/';
const DESCRIPTION = 'Return the Dealabs search result using keywords';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Recherche par Mot(s) clé(s)' => array (
'q' => array(
'name' => 'Mot(s) clé(s)',
'type' => 'text',
'required' => true
),
'hide_expired' => array(
'name' => 'Masquer les éléments expirés',
'type' => 'checkbox',
'required' => 'true'
),
'hide_local' => array(
'name' => 'Masquer les deals locaux',
'type' => 'checkbox',
'title' => 'Masquer les deals en magasins physiques',
'required' => 'true'
),
'priceFrom' => array(
'name' => 'Prix minimum',
'type' => 'text',
'title' => 'Prix mnimum en euros',
'required' => 'false',
'defaultValue' => ''
),
'priceTo' => array(
'name' => 'Prix maximum',
'type' => 'text',
'title' => 'Prix maximum en euros',
'required' => 'false',
'defaultValue' => ''
),
),
'Deals par groupe' => array(
'groupe' => array(
'name' => 'Groupe',
'type' => 'list',
'required' => 'true',
'title' => 'Groupe dont il faut afficher les deals',
'values' => array(
'Accessoires & gadgets' => 'accessoires-gadgets',
'Alimentation & boissons' => 'alimentation-boissons',
'Animaux' => 'animaux',
'Applis & logiciels' => 'applis-logiciels',
'Consoles & jeux vidéo' => 'consoles-jeux-video',
'Culture & divertissement' => 'culture-divertissement',
'Gratuit' => 'gratuit',
'Image, son & vidéo' => 'image-son-video',
'Informatique' => 'informatique',
'Jeux & jouets' => 'jeux-jouets',
'Maison & jardin' => 'maison-jardin',
'Mode & accessoires' => 'mode-accessoires',
'Santé & cosmétiques' => 'hygiene-sante-cosmetiques',
'Services divers' => 'services-divers',
'Sports & plein air' => 'sports-plein-air',
'Téléphonie' => 'telephonie',
'Voyages & sorties' => 'voyages-sorties-restaurants'
)
),
'ordre' => array(
'name' => 'Trier par',
'type' => 'list',
'required' => 'true',
'title' => 'Ordre de tri des deals',
'values' => array(
'Du deal le plus Hot au moins Hot' => '',
'Du deal le plus récent au plus ancien' => '-nouveaux',
'Du deal le plus commentés au moins commentés' => '-commentes'
)
)
)
);
const CACHE_TIMEOUT = 3600;
public function collectData(){
switch($this->queriedContext) {
case 'Recherche par Mot(s) clé(s)':
return $this->collectDataMotsCles();
break;
case 'Deals par groupe':
return $this->collectDataGroupe();
break;
}
}
/**
* Get the Deal data from the choosen groupe in the choose order
*/
public function collectDataGroupe()
{
$groupe = $this->getInput('groupe');
$ordre = $this->getInput('ordre');
$url = self::URI
. '/groupe/' . $groupe . $ordre;
$this->collectDeals($url);
}
/**
* Get the Deal data from the choosen keywords and parameters
*/
public function collectDataMotsCles()
{
$q = $this->getInput('q');
$hide_expired = $this->getInput('hide_expired');
$hide_local = $this->getInput('hide_local');
$priceFrom = $this->getInput('priceFrom');
$priceTo = $this->getInput('priceFrom');
/* Even if the original website uses POST with the search page, GET works too */
$url = self::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';
$this->collectDeals($url);
}
/**
* Get the Deal data using the given URL
*/
public function collectDeals($url){
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request Dealabs.');
$list = $html->find('article');
// Deal Image Link CSS Selector
$selectorImageLink = implode(
' ', /* Notice this is a space! */
array(
'cept-thread-image-link',
'imgFrame',
'imgFrame--noBorder',
'box--all-i',
'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(
'flex',
'flex--align-c',
'flex--justify-space-between',
'space--b-2',
)
);
// Deal Description CSS Selector
$selectorDescription = implode(
' ', /* Notice this is a space! */
array(
'cept-description-container',
'overflow--wrap-break',
'size--all-s',
'size--fromW3-m',
)
);
// Deal Date CSS Selector
$selectorDate = implode(
' ', /* Notice this is a space! */
array(
'size--all-s',
'flex',
'flex--wrap',
'flex--justify-e',
'flex--grow-1',
)
);
// If there is no results, we don't parse the content because it display some random deals
$noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
if($noresult != null && $noresult->plaintext == 'Il n&#039;y a rien à afficher pour le moment :(') {
$this->items = array();
} else {
foreach($list as $deal) {
$item = array();
$item['uri'] = $deal->find('div[class=threadGrid-title]', 0)->find('a', 0)->href;
$item['title'] = $deal->find('a[class*='. $selectorLink .']', 0
)->plaintext;
$item['author'] = $deal->find('span.thread-username', 0)->plaintext;
$item['content'] = '<table><tr><td><a href="'
. $deal->find(
'a[class*='. $selectorImageLink .']', 0)->href
. '"><img src="'
. $this->getImage($deal)
. '"/></td><td><h2><a href="'
. $deal->find('a[class*='. $selectorLink .']', 0)->href
. '">'
. $deal->find('a[class*='. $selectorLink .']', 0)->innertext
. '</a></h2>'
. $this->getPrix($deal)
. $this->getReduction($deal)
. $this->getExpedition($deal)
. $this->getLivraison($deal)
. $this->getOrigine($deal)
. $deal->find('div[class='. $selectorDescription .']', 0)->innertext
. '</td><td>'
. $deal->find('div[class='. $selectorHot .']', 0)->children(0)->outertext
. '</td></table>';
$dealDateDiv = $deal->find('div[class='. $selectorDate .']', 0)
->find('span[class=hide--toW3]');
$itemDate = end($dealDateDiv)->plaintext;
if(substr( $itemDate, 0, 6 ) === 'il y a') {
$item['timestamp'] = $this->relativeDateToTimestamp($itemDate);
} else {
$item['timestamp'] = $this->parseDate($itemDate);
}
$this->items[] = $item;
}
}
}
/**
* Get the Price from a Deal if it exists
* @return string String of the deal price
*/
private function getPrix($deal)
{
if($deal->find(
'span[class*=thread-price]', 0) != null) {
return '<div>Prix : '
. $deal->find(
'span[class*=thread-price]', 0
)->plaintext
. '</div>';
} else {
return '';
}
}
/**
* Get the Shipping costs from a Deal if it exists
* @return string String of the deal shipping Cost
*/
private function getLivraison($deal)
{
if($deal->find('span[class*=cept-shipping-price]', 0) != null) {
if($deal->find('span[class*=cept-shipping-price]', 0)->children(0) != null) {
return '<div>Livraison : '
. $deal->find('span[class*=cept-shipping-price]', 0)->children(0)->innertext
. '</div>';
} else {
return '<div>Livraison : '
. $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 getOrigine($deal)
{
if($deal->find('a[class=text--color-greyShade]', 0) != null) {
return '<div>Origine : '
. $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 getReduction($deal)
{
if($deal->find('span[class*=mute--text text--lineThrough]', 0) != null) {
return '<div>Réduction : <span style="text-decoration: line-through;">'
. $deal->find(
'span[class*=mute--text text--lineThrough]', 0
)->plaintext
. '</span>&nbsp;'
. $deal->find('span[class=space--ml-1 size--all-l size--fromW3-xl]', 0)->plaintext
. '</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 existsa
* @return string String of the deal originating country
*/
private function getExpedition($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 French date into a timestam
* @return int timestamp of the input date
*/
private function parseDate($string)
{
$month_fr = array(
'janvier',
'février',
'mars',
'avril',
'mai',
'juin',
'juillet',
'août',
'septembre',
'octobre',
'novembre',
'décembre'
);
$month_en = array(
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
);
$date_str = trim(str_replace($month_fr, $month_en, $string));
if(!preg_match('/[0-9]{4}/', $string)) {
$date_str .= ' ' . date('Y');
}
$date_str .= ' 00:00';
$date = DateTime::createFromFormat('j F Y H:i', $date_str);
return $date->getTimestamp();
}
/**
* Transforms a relate French date into a timestam
* @return int timestamp of the input date
*/
private function relativeDateToTimestamp($str) {
$date = new DateTime();
$search = array(
'il y a ',
'min',
'h',
'jour',
'jours',
'mois',
'ans',
'et '
);
$replace = array(
'-',
'minute',
'hour',
'day',
'month',
'year',
''
);
$date->modify(str_replace($search, $replace, $str));
return $date->getTimestamp();
}
public function getName(){
switch($this->queriedContext) {
case 'Recherche par Mot(s) clé(s)':
return self::NAME . ' - Recherche : '. $this->getInput('q');
break;
case 'Deals par groupe':
$values = self::PARAMETERS['Deals par groupe']['groupe']['values'];
$groupe = array_search($this->getInput('groupe'), $values);
return self::NAME . ' - Groupe : '. $groupe;
break;
default: // Return default value
return self::NAME;
}
}
}

166
bridges/DemonoidBridge.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
class DemonoidBridge extends BridgeAbstract {
const MAINTAINER = 'metaMMA';
const NAME = 'Demonoid';
const URI = 'https://www.demonoid.pw/';
const DESCRIPTION = 'Returns results from search';
const PARAMETERS = array(array(
'q' => array(
'name' => 'keywords',
'exampleValue' => 'keyword1 keyword2…',
'required' => true,
),
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All' => 0,
'Movies' => 1,
'Music' => 2,
'TV' => 3,
'Games' => 4,
'Applications' => 5,
'Pictures' => 8,
'Anime' => 9,
'Comics' => 10,
'Books' => 11,
'Audiobooks' => 17
)
)
), array(
'catOnly' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All' => 0,
'Movies' => 1,
'Music' => 2,
'TV' => 3,
'Games' => 4,
'Applications' => 5,
'Pictures' => 8,
'Anime' => 9,
'Comics' => 10,
'Books' => 11,
'Audiobooks' => 17
)
)
), array(
'userid' => array(
'name' => 'user id',
'exampleValue' => '00000',
'required' => true,
'type' => 'number'
),
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All' => 0,
'Movies' => 1,
'Music' => 2,
'TV' => 3,
'Games' => 4,
'Applications' => 5,
'Pictures' => 8,
'Anime' => 9,
'Comics' => 10,
'Books' => 11,
'Audiobooks' => 17
)
)
)
);
public function collectData() {
if(!empty($this->getInput('q'))) {
$html = getSimpleHTMLDOM(
self::URI .
'files/?category=' .
rawurlencode($this->getInput('category')) .
'&subcategory=All&quality=All&seeded=2&external=2&query=' .
urlencode($this->getInput('q')) .
'&uid=0&sort='
) or returnServerError('Could not request Demonoid.');
} elseif(!empty($this->getInput('catOnly'))) {
$html = getSimpleHTMLDOM(
self::URI .
'files/?uid=0&category=' .
rawurlencode($this->getInput('catOnly')) .
'&subcategory=0&language=0&seeded=2&quality=0&query=&sort='
) or returnServerError('Could not request Demonoid.');
} elseif(!empty($this->getInput('userid'))) {
$html = getSimpleHTMLDOM(
self::URI .
'files/?uid=' .
rawurlencode($this->getInput('userid')) .
'&seeded=2'
) or returnServerError('Could not request Demonoid.');
} else {
returnServerError('Invalid parameters !');
}
if(preg_match('~No torrents found~', $html)) {
return;
}
$table = $html->find('td[class=ctable_content_no_pad]', 0);
$cursorCount = 4;
$elementCount = 0;
while($elementCount != 40) {
$elementCount++;
$currentElement = $table->find('tr', $cursorCount);
if(preg_match('~items total~', $currentElement)) {
break;
}
$item = array();
//Do we have a date ?
if(preg_match('~Added.*?(.*)~', $currentElement->plaintext, $dateStr)) {
if(preg_match('~today~', $dateStr[0])) {
date_default_timezone_set('UTC');
$timestamp = mktime(0, 0, 0, gmdate('n'), gmdate('j'), gmdate('Y'));
} else {
preg_match('~(?<=ed on ).*\d+~', $currentElement->plaintext, $fullDateStr);
date_default_timezone_set('UTC');
$dateObj = strptime($fullDateStr[0], '%A, %b %d, %Y');
$timestamp = mktime(0, 0, 0, $dateObj['tm_mon'] + 1, $dateObj['tm_mday'], 1900 + $dateObj['tm_year']);
}
$cursorCount++;
}
$content = $table->find('tr', $cursorCount)->find('a', 1);
$cursorCount++;
$torrentInfo = $table->find('tr', $cursorCount);
$item['timestamp'] = $timestamp;
$item['title'] = $content->plaintext;
$item['id'] = self::URI . $content->href;
$item['uri'] = self::URI . $content->href;
$item['author'] = $torrentInfo->find('a[class=user]', 0)->plaintext;
$item['seeders'] = $torrentInfo->find('font[class=green]', 0)->plaintext;
$item['leechers'] = $torrentInfo->find('font[class=red]', 0)->plaintext;
$item['size'] = $torrentInfo->find('td', 3)->plaintext;
$item['content'] = 'Uploaded by ' . $item['author']
. ' , Size ' . $item['size']
. '<br>seeders: '
. $item['seeders']
. ' | leechers: '
. $item['leechers']
. '<br><a href="'
. $item['id']
. '">info page</a>';
$this->items[] = $item;
$cursorCount++;
}
}
}

112
bridges/DiscogsBridge.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
class DiscogsBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'DiscogsBridge';
const URI = 'https://www.discogs.com/';
const DESCRIPTION = 'Returns releases from discogs';
const PARAMETERS = array(
'Artist Releases' => array(
'artistid' => array(
'name' => 'Artist ID',
'type' => 'number',
)
),
'Label Releases' => array(
'labelid' => array(
'name' => 'Label ID',
'type' => 'number',
)
),
'User Wantlist' => array(
'username_wantlist' => array(
'name' => 'Username',
'type' => 'text',
)
),
'User Folder' => array(
'username_folder' => array(
'name' => 'Username',
'type' => 'text',
),
'folderid' => array(
'name' => 'Folder ID',
'type' => 'number',
)
)
);
public function collectData() {
if(!empty($this->getInput('artistid')) || !empty($this->getInput('labelid'))) {
if(!empty($this->getInput('artistid'))) {
$data = getContents("https://api.discogs.com/artists/"
. $this->getInput('artistid')
. "/releases?sort=year&sort_order=desc")
or returnServerError("Unable to query discogs !");
} elseif(!empty($this->getInput('labelid'))) {
$data = getContents("https://api.discogs.com/labels/"
. $this->getInput('labelid')
. "/releases?sort=year&sort_order=desc")
or returnServerError("Unable to query discogs !");
}
$jsonData = json_decode($data, true);
foreach($jsonData["releases"] as $release) {
$item = array();
$item["author"] = $release["artist"];
$item["title"] = $release["title"];
$item["id"] = $release["id"];
$resId = array_key_exists("main_release", $release) ? $release["main_release"] : $release["id"];
$item["uri"] = self::URI . $this->getInput('artistid') . "/release/" . $resId;
$item["timestamp"] = DateTime::createFromFormat("Y", $release["year"])->getTimestamp();
$item["content"] = $item["author"] . " - " . $item["title"];
$this->items[] = $item;
}
} elseif(!empty($this->getInput("username_wantlist")) || !empty($this->getInput("username_folder"))) {
if(!empty($this->getInput("username_wantlist"))) {
$data = getContents("https://api.discogs.com/users/"
. $this->getInput('username_wantlist')
. "/wants?sort=added&sort_order=desc")
or returnServerError("Unable to query discogs !");
$jsonData = json_decode($data, true)["wants"];
} elseif(!empty($this->getInput("username_folder"))) {
$data = getContents("https://api.discogs.com/users/"
. $this->getInput('username_folder')
. "/collection/folders/"
. $this->getInput("folderid")
."/releases?sort=added&sort_order=desc")
or returnServerError("Unable to query discogs !");
$jsonData = json_decode($data, true)["releases"];
}
foreach($jsonData as $element) {
$infos = $element["basic_information"];
$item = array();
$item["title"] = $infos["title"];
$item["author"] = $infos["artists"][0]["name"];
$item["id"] = $infos["artists"][0]["id"];
$item["uri"] = self::URI . $infos["artists"][0]["id"] . "/release/" . $infos["id"];
$item["timestamp"] = strtotime($element["date_added"]);
$item["content"] = $item["author"] . " - " . $item["title"];
$this->items[] = $item;
}
}
}
public function getURI() {
return self::URI;
}
public function getName() {
return static::NAME;
}
}

54
bridges/FDroidBridge.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
class FDroidBridge extends BridgeAbstract {
const MAINTAINER = 'Mitsukarenai';
const NAME = 'F-Droid Bridge';
const URI = 'https://f-droid.org/';
const CACHE_TIMEOUT = 60 * 60 * 2; // 2 hours
const DESCRIPTION = 'Returns latest added/updated apps on the open-source Android apps repository F-Droid';
const PARAMETERS = array( array(
'u' => array(
'name' => 'Widget selection',
'type' => 'list',
'required' => true,
'values' => array(
'Latest added apps' => 'added',
'Latest updated apps' => 'updated'
)
)
));
public function collectData(){
$url = self::URI;
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request F-Droid.');
// targetting the corresponding widget based on user selection
// "updated" is the 4th widget on the page, "added" is the 5th
switch($this->getInput('u')) {
case 'updated':
$html_widget = $html->find('div.sidebar-widget', 4);
break;
default:
$html_widget = $html->find('div.sidebar-widget', 5);
break;
}
// and now extracting app info from the selected widget (and yeah turns out icons are of heterogeneous sizes)
foreach($html_widget->find('a') as $element) {
$item = array();
$item['uri'] = self::URI . $element->href;
$item['title'] = $element->find('h4', 0)->plaintext;
$item['icon'] = $element->find('img', 0)->src;
$item['summary'] = $element->find('span.package-summary', 0)->plaintext;
$item['content'] = '
<a href="'.$item['uri'].'">
<img alt="" style="max-height:128px" src="'.$item['icon'].'">
</a><br>'.$item['summary'];
$this->items[] = $item;
}
}
}

View File

@@ -46,7 +46,7 @@ class FacebookBridge extends BridgeAbstract {
if(is_array($matches) && count($matches) > 1) {
$link = $matches[1];
if(strpos($link, '/') === 0)
$link = self::URI . $link . '"';
$link = self::URI . $link;
if(strpos($link, 'facebook.com/l.php?u=') !== false)
$link = urldecode(extractFromDelimiters($link, 'facebook.com/l.php?u=', '&'));
return ' href="' . $link . '"';
@@ -96,17 +96,15 @@ class FacebookBridge extends BridgeAbstract {
$captcha_action = $_SESSION['captcha_action'];
$captcha_fields = $_SESSION['captcha_fields'];
$captcha_fields['captcha_response'] = preg_replace("/[^a-zA-Z0-9]+/", "", $_POST['captcha_response']);
$http_options = array(
'http' => array(
'method' => 'POST',
'user_agent' => ini_get('user_agent'),
'header' => array("Content-type:
application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscript=1\r\n"),
'content' => http_build_query($captcha_fields)
),
$header = array("Content-type:
application/x-www-form-urlencoded\r\nReferer: $captcha_action\r\nCookie: noscript=1\r\n");
$opts = array(
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($captcha_fields)
);
$context = stream_context_create($http_options);
$html = getContents($captcha_action, false, $context);
$html = getContents($captcha_action, $header, $opts);
if($html === false) {
returnServerError('Failed to submit captcha response back to Facebook');
@@ -120,23 +118,18 @@ class FacebookBridge extends BridgeAbstract {
//Retrieve page contents
if(is_null($html)) {
$http_options = array(
'http' => array(
'method' => 'GET',
'user_agent' => ini_get('user_agent'),
'header' => 'Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n"
)
);
$context = stream_context_create($http_options);
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
// First character cannot be a forward slash
if(strpos($this->getInput('u'), "/") === 0) {
returnClientError('Remove leading slash "/" from the username!');
}
if(!strpos($this->getInput('u'), "/")) {
$html = getSimpleHTMLDOM(self::URI . urlencode($this->getInput('u')) . '?_fb_noscript=1',
false,
$context)
$html = getSimpleHTMLDOM(self::URI . urlencode($this->getInput('u')) . '?_fb_noscript=1', $header)
or returnServerError('No results for this query.');
} else {
$html = getSimpleHTMLDOM(self::URI . 'pages/' . $this->getInput('u') . '?_fb_noscript=1',
false,
$context)
$html = getSimpleHTMLDOM(self::URI . 'pages/' . $this->getInput('u') . '?_fb_noscript=1', $header)
or returnServerError('No results for this query.');
}
}

View File

@@ -3,7 +3,7 @@ class GoComicsBridge extends BridgeAbstract {
const MAINTAINER = 'sky';
const NAME = 'GoComics Unofficial RSS';
const URI = 'http://www.gocomics.com/';
const URI = 'https://www.gocomics.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'The Unofficial GoComics RSS';
const PARAMETERS = array( array(
@@ -18,25 +18,27 @@ class GoComicsBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request GoComics: ' . $this->getURI());
foreach($html->find('div.comic__container') as $element) {
//Get info from first page
$author = preg_replace('/By /', '', $html->find(".media-subheading", 0)->plaintext);
$img = $element->find('img', 0);
$link = $element->find('a.js-item-comic-link', 0);
$comic = $img->src;
$title = $link->title;
$url = $html->find('input.js-copy-link', 0)->value;
$date = substr($title, -10);
if (empty($title))
$title = 'GoComics ' . $this->getInput('comicname') . ' on ' . $date;
$date = strtotime($date);
$link = self::URI . $html->find(".gc-deck--cta-0", 0)->find('a', 0)->href;
for($i = 0; $i < 5; $i++) {
$item = array();
$item['id'] = $url;
$item['uri'] = $url;
$item['title'] = $title;
$item['author'] = preg_replace('/by /', '', $element->find('a.link-blended small', 0)->plaintext);
$item['timestamp'] = $date;
$item['content'] = '<img src="' . $comic . '" alt="' . $title . '" />';
$page = getSimpleHTMLDOM($link)
or returnServerError('Could not request GoComics: ' . $link);
$imagelink = $page->find(".img-fluid", 1)->src;
$date = explode("/", $link);
$item['id'] = $imagelink;
$item['uri'] = $link;
$item['author'] = $author;
$item['title'] = 'GoComics ' . $this->getInput('comicname');
$item['timestamp'] = DateTime::createFromFormat("Ymd", $date[5] . $date[6] . $date[7])->getTimestamp();
$item['content'] = '<img src="' . $imagelink . '" />';
$link = self::URI . $page->find(".js-previous-comic", 0)->href;
$this->items[] = $item;
}
}

310
bridges/IPBBridge.php Normal file
View File

@@ -0,0 +1,310 @@
<?php
class IPBBridge extends FeedExpander {
const NAME = 'IPB Bridge';
const URI = 'https://www.invisionpower.com';
const DESCRIPTION = 'Returns feeds for forums powered by IPB';
const MAINTAINER = 'logmanoriginal';
const PARAMETERS = array(
array(
'uri' => array(
'name' => 'URI',
'type' => 'text',
'required' => true,
'title' => 'Insert forum, subforum or topic URI',
'exampleValue' => 'https://invisioncommunity.com/forums/forum/499-feedback-and-ideas/'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'required' => false,
'title' => 'Specifies the number of items to return on each request (-1: all)',
'defaultValue' => 10
)
)
);
const CACHE_TIMEOUT = 3600;
// Constants for internal use
const FORUM_TYPE_LIST_FILTER = '.cForumTopicTable';
const FORUM_TYPE_TABLE_FILTER = '#forum_table';
const TOPIC_TYPE_ARTICLE = 'article';
const TOPIC_TYPE_DIV = 'div.post_block';
public function getURI(){
return $this->getInput('uri') ?: parent::getURI();
}
public function collectData(){
// The URI cannot be the mainpage (or anything related)
switch(parse_url($this->getInput('uri'), PHP_URL_PATH)) {
case null:
case '/index.php':
returnClientError('Provided URI is invalid!');
break;
default:
break;
}
// Sanitize the URI (because else it won't work)
$uri = rtrim($this->getInput('uri'), '/'); // No trailing slashes!
// Forums might provide feeds, though that's optional *facepalm*
// Let's check if there is a valid feed available
$headers = get_headers($uri . '.xml');
if($headers[0] === 'HTTP/1.1 200 OK') { // Heureka! It's a valid feed!
return $this->collectExpandableDatas($uri);
}
// No valid feed, so do it the hard way
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request ' . $this->getInput('uri') . '!');
$limit = $this->getInput('limit');
// Determine if this is a topic or a forum
switch(true) {
case $this->isTopic($html):
$this->collectTopic($html, $limit);
break;
case $this->isForum($html);
$this->collectForum($html);
break;
default:
returnClientError('Unknown type!');
break;
}
}
private function isForum($html){
return !is_null($html->find('div[data-controller*=forums.front.forum.forumPage]', 0))
|| !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0));
}
private function isTopic($html){
return !is_null($html->find('div[data-controller*=core.front.core.commentFeed]', 0))
|| !is_null($html->find(static::TOPIC_TYPE_DIV, 0));
}
private function collectForum($html){
// There are multiple forum designs in use (depends on version?)
// 1 - Uses an ordered list (based on https://invisioncommunity.com/forums)
// 2 - Uses a table (based on https://onehallyu.com)
switch(true) {
case !is_null($html->find(static::FORUM_TYPE_LIST_FILTER, 0)):
$this->collectForumList($html);
break;
case !is_null($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)):
$this->collectForumTable($html);
break;
default:
returnClientError('Unknown forum format!');
break;
}
}
private function collectForumList($html){
foreach($html->find(static::FORUM_TYPE_LIST_FILTER, 0)->children() as $row) {
// Columns: Title, Statistics, Last modified
$item = array();
$item['uri'] = $row->find('a', 0)->href;
$item['title'] = $row->find('a', 0)->title;
$item['author'] = $row->find('a', 1)->innertext;
$item['timestamp'] = strtotime($row->find('time', 0)->getAttribute('datetime'));
$this->items[] = $item;
}
}
private function collectForumTable($html){
foreach($html->find(static::FORUM_TYPE_TABLE_FILTER, 0)->children() as $row) {
// Columns: Icon, Content, Preview, Statistics, Last modified
$item = array();
// Skip header row
if(!is_null($row->find('th', 0))) continue;
$item['uri'] = $row->find('a', 0)->href;
$item['title'] = $row->find('.title', 0)->plaintext;
$item['timestamp'] = strtotime($row->find('[itemprop=dateCreated]', 0)->plaintext);
$this->items[] = $item;
}
}
private function collectTopic($html, $limit){
// There are multiple topic designs in use (depends on version?)
// 1 - Uses articles (based on https://invisioncommunity.com/forums)
// 2 - Uses divs (based on https://onehallyu.com)
switch(true) {
case !is_null($html->find(static::TOPIC_TYPE_ARTICLE, 0)):
$this->collectTopicHistory($html, $limit, 'collectTopicArticle');
break;
case !is_null($html->find(static::TOPIC_TYPE_DIV, 0)):
$this->collectTopicHistory($html, $limit, 'collectTopicDiv');
break;
default:
returnClientError('Unknown topic format!');
break;
}
}
private function collectTopicHistory($html, $limit, $callback){
// Make sure the callback is valid!
if(!method_exists($this, $callback))
returnServerError('Unknown function (\'' . $callback . '\')!');
$next = null; // Holds the URI of the next page
while(true) {
$next = $this->$callback($html, is_null($next));
if(is_null($next) || ($limit > 0 && count($this->items) >= $limit)) {
break;
}
$html = getSimpleHTMLDOMCached($next);
}
// We might have more items than specified, remove excess
$this->items = array_slice($this->items, 0, $limit);
}
private function collectTopicArticle($html, $firstrun = true){
$title = $html->find('h1.ipsType_pageTitle', 0)->plaintext;
// Are we on last page?
if($firstrun && !is_null($html->find('.ipsPagination', 0))) {
$last = $html->find('.ipsPagination_last a', 0)->{'data-page'};
$active = $html->find('.ipsPagination_active a', 0)->{'data-page'};
if($active !== $last) {
// Load last page into memory (cached)
$html = getSimpleHTMLDOMCached($html->find('.ipsPagination_last a', 0)->href);
}
}
foreach(array_reverse($html->find(static::TOPIC_TYPE_ARTICLE)) as $article) {
$item = array();
$item['uri'] = $article->find('time', 0)->parent()->href;
$item['author'] = $article->find('aside a', 0)->plaintext;
$item['title'] = $item['author'] . ' - ' . $title;
$item['timestamp'] = strtotime($article->find('time', 0)->getAttribute('datetime'));
$content = $article->find('[data-role=commentContent]', 0);
$content = $this->scaleImages($content);
$item['content'] = $this->fixContent($content);
$item['enclosures'] = $this->findImages($article->find('[data-role=commentContent]', 0)) ?: null;
$this->items[] = $item;
}
// Return whatever page comes next (previous, as we add in inverse order)
// Do we have a previous page? (inactive means no)
if(!is_null($html->find('li[class=ipsPagination_prev ipsPagination_inactive]', 0))) {
return null; // No, or no more
} elseif(!is_null($html->find('li[class=ipsPagination_prev]', 0))) {
return $html->find('.ipsPagination_prev a', 0)->href;
}
return null;
}
private function collectTopicDiv($html, $firstrun = true){
$title = $html->find('h1.ipsType_pagetitle', 0)->plaintext;
// Are we on last page?
if($firstrun && !is_null($html->find('.pagination', 0))) {
$active = $html->find('li[class=page active]', 0)->plaintext;
// There are two ways the 'last' page is displayed:
// - With a distict 'last' button (only if there are enough pages)
// - With a button for each page (use last button)
if(!is_null($html->find('li.last', 0))) {
$last = $html->find('li.last a', 0);
} else {
$last = $html->find('li[class=page] a', -1);
}
if($active !== $last->plaintext) {
// Load last page into memory (cached)
$html = getSimpleHTMLDOMCached($last->href);
}
}
foreach(array_reverse($html->find(static::TOPIC_TYPE_DIV)) as $article) {
$item = array();
$item['uri'] = $article->find('a[rel=bookmark]', 0)->href;
$item['author'] = $article->find('.author', 0)->plaintext;
$item['title'] = $item['author'] . ' - ' . $title;
$item['timestamp'] = strtotime($article->find('.published', 0)->getAttribute('title'));
$content = $article->find('[itemprop=commentText]', 0);
$content = $this->scaleImages($content);
$item['content'] = $this->fixContent($content);
$item['enclosures'] = $this->findImages($article->find('.post_body', 0)) ?: null;
$this->items[] = $item;
}
// Return whatever page comes next (previous, as we add in inverse order)
// Do we have a previous page?
if(!is_null($html->find('li.prev', 0))) {
return $html->find('li.prev a', 0)->href;
}
return null;
}
/** Returns all images from the provide HTML DOM */
private function findImages($html){
$images = array();
foreach($html->find('img') as $img) {
$images[] = $img->src;
}
return $images;
}
/** Sets the maximum width and height for all images */
private function scaleImages($html, $width = 400, $height = 400){
foreach($html->find('img') as $img) {
$img->style = "max-width: {$width}px; max-height: {$height}px;";
}
return $html;
}
/** Removes all unnecessary tags and adds formatting */
private function fixContent($html){
// Restore quote highlighting
foreach($html->find('blockquote') as $quote) {
$quote->style = <<<EOD
padding: 0px 15px;
border-width: 1px 1px 1px 2px;
border-style: solid;
border-color: #ededed #e8e8e8 #dbdbdb #666666;
background: #fbfbfb;
EOD;
}
// Remove unnecessary tags
$content = strip_tags(
$html->innertext,
'<p><a><img><ol><ul><li><table><tr><th><td><strong><blockquote><br><hr><h>'
);
return $content;
}
}

View File

@@ -6,23 +6,42 @@ class InstagramBridge extends BridgeAbstract {
const URI = 'https://instagram.com/';
const DESCRIPTION = 'Returns the newest images';
const PARAMETERS = array( array(
'u' => array(
'name' => 'username',
'required' => true
),
'media_type' => array(
'name' => 'Media type',
'type' => 'list',
'required' => false,
'values' => array(
'Both' => 'all',
'Video' => 'video',
'Picture' => 'picture'
const PARAMETERS = array(
array(
'u' => array(
'name' => 'username',
'required' => true
),
'defaultValue' => 'all'
'media_type' => array(
'name' => 'Media type',
'type' => 'list',
'required' => false,
'values' => array(
'Both' => 'all',
'Video' => 'video',
'Picture' => 'picture'
),
'defaultValue' => 'all'
)
),
array(
'h' => array(
'name' => 'hashtag',
'required' => true
),
'media_type' => array(
'name' => 'Media type',
'type' => 'list',
'required' => false,
'values' => array(
'Both' => 'all',
'Video' => 'video',
'Picture' => 'picture'
),
'defaultValue' => 'all'
)
)
));
);
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
@@ -47,9 +66,14 @@ class InstagramBridge extends BridgeAbstract {
$json = trim(substr($innertext, $pos + 18), ' =;');
$data = json_decode($json);
$userMedia = $data->entry_data->ProfilePage[0]->user->media->nodes;
if(!is_null($this->getInput('u'))) {
$userMedia = $data->entry_data->ProfilePage[0]->graphql->user->edge_owner_to_timeline_media->edges;
} else {
$userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges;
}
foreach($userMedia as $media) {
$media = $media->node;
// Check media type
switch($this->getInput('media_type')) {
case 'all': break;
@@ -63,14 +87,15 @@ class InstagramBridge extends BridgeAbstract {
}
$item = array();
$item['uri'] = self::URI . 'p/' . $media->code . '/';
$item['content'] = '<img src="' . htmlentities($media->display_src) . '" />';
if (isset($media->caption)) {
$item['title'] = $media->caption;
$item['uri'] = self::URI . 'p/' . $media->shortcode . '/';
$item['content'] = '<img src="' . htmlentities($media->display_url) . '" />';
if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
$item['title'] = $media->edge_media_to_caption->edges[0]->node->text;
} else {
$item['title'] = basename($media->display_src);
$item['title'] = basename($media->display_url);
}
$item['timestamp'] = $media->date;
$item['timestamp'] = $media->taken_at_timestamp;
$item['enclosures'] = array($media->display_url);
$this->items[] = $item;
}
}
@@ -86,6 +111,8 @@ class InstagramBridge extends BridgeAbstract {
public function getURI(){
if(!is_null($this->getInput('u'))) {
return self::URI . urlencode($this->getInput('u'));
} elseif(!is_null($this->getInput('h'))) {
return self::URI . 'explore/tags/' . urlencode($this->getInput('h'));
}
return parent::getURI();

View File

@@ -45,9 +45,7 @@ class KernelBugTrackerBridge extends BridgeAbstract {
// We use the print preview page for simplicity
$html = getSimpleHTMLDOMCached($this->getURI() . '&format=multiple',
86400,
false,
null,
0,
null,
true,
true,

View File

@@ -42,10 +42,10 @@ class LegifranceJOBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI)
or $this->returnServer('Unable to download ' . self::URI);
$this->author = trim($html->find('h2.title', 0)->plaintext);
$this->author = trim($html->find('h2.titleJO', 0)->plaintext);
$uri = $html->find('h2.titleELI', 0)->plaintext;
$this->uri = trim(substr($uri, strpos($uri, 'https')));
$this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/')));
$this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5));
foreach($html->find('h3') as $section) {
$subsections = $section->nextSibling()->find('h4');

View File

@@ -4,7 +4,7 @@ class MixCloudBridge extends BridgeAbstract {
const MAINTAINER = 'Alexis CHEMEL';
const NAME = 'MixCloud';
const URI = 'https://mixcloud.com/';
const URI = 'https://www.mixcloud.com';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns latest musics on user stream';
@@ -24,8 +24,9 @@ class MixCloudBridge extends BridgeAbstract {
}
public function collectData(){
ini_set('user_agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
$html = getSimpleHTMLDOM(self::URI . $this->getInput('u'))
$html = getSimpleHTMLDOM(self::URI . '/' . $this->getInput('u'))
or returnServerError('Could not request MixCloud.');
foreach($html->find('section.card') as $element) {

View File

@@ -0,0 +1,57 @@
<?php
class NotAlwaysBridge extends BridgeAbstract {
const MAINTAINER = 'mozes';
const NAME = 'Not Always family Bridge';
const URI = 'https://notalwaysright.com/';
const DESCRIPTION = 'Returns the latest stories';
const CACHE_TIMEOUT = 1800; // 30 minutes
const PARAMETERS = array( array(
'filter' => array(
'type' => 'list',
'name' => 'Filter',
'values' => array(
'All' => 'all',
'Right' => 'right',
'Working' => 'working',
'Romantic' => 'romantic',
'Related' => 'related',
'Learning' => 'learning',
'Friendly' => 'friendly',
'Hopeless' => 'hopeless',
'Unfiltered' => 'unfiltered'
),
'required' => true
)
));
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request NotAlways.');
foreach($html->find('.post') as $post) {
#print_r($post);
$item = array();
$item['uri'] = $post->find('h1', 0)->find('a', 0)->href;
$item['content'] = $post;
$item['title'] = $post->find('h1', 0)->find('a', 0)->innertext;
$this->items[] = $item;
}
}
public function getName(){
if(!is_null($this->getInput('filter'))) {
return $this->getInput('filter') . ' - NotAlways Bridge';
}
return parent::getName();
}
public function getURI(){
if(!is_null($this->getInput('filter'))) {
return self::URI . $this->getInput('filter') . "/";
}
return parent::getURI();
}
}

23
bridges/PcGamerBridge.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
class PcGamerBridge extends BridgeAbstract
{
const NAME = 'PC Gamer';
const URI = 'https://www.pcgamer.com/';
const DESCRIPTION = 'PC Gamer Most Read Stories';
const MAINTAINER = 'mdemoss';
public function collectData()
{
$html = getSimpleHTMLDOMCached($this->getURI(), 300);
$stories = $html->find('div#popularcontent li.most-popular-item');
foreach ($stories as $element) {
$item['uri'] = $element->find('a', 0)->href;
$articleHtml = getSimpleHTMLDOMCached($item['uri']);
$item['title'] = $element->find('h4 a', 0)->plaintext;
$item['timestamp'] = strtotime($articleHtml->find('meta[name=pub_date]', 0)->content);
$item['content'] = $articleHtml->find('meta[name=description]', 0)->content;
$item['author'] = $articleHtml->find('a[itemprop=author]', 0)->plaintext;
$this->items[] = $item;
}
}
}

View File

@@ -15,12 +15,6 @@ class PinterestBridge extends FeedExpander {
'b' => array(
'name' => 'board',
'required' => true
),
'r' => array(
'name' => 'Use custom RSS',
'type' => 'checkbox',
'required' => false,
'title' => 'Uncheck to return data via custom filters (more data)'
)
),
'From search' => array(
@@ -34,12 +28,8 @@ class PinterestBridge extends FeedExpander {
public function collectData(){
switch($this->queriedContext) {
case 'By username and board':
if($this->getInput('r')) {
$html = getSimpleHTMLDOMCached($this->getURI());
$this->getUserResults($html);
} else {
$this->collectExpandableDatas($this->getURI() . '.rss');
}
$this->collectExpandableDatas($this->getURI() . '.rss');
$this->fixLowRes();
break;
case 'From search':
default:
@@ -48,49 +38,17 @@ class PinterestBridge extends FeedExpander {
}
}
private function getUserResults($html){
$json = json_decode($html->find('#jsInit1', 0)->innertext, true);
$results = $json['tree']['children'][0]['children'][0]['children'][0]['options']['props']['data']['board_feed'];
$username = $json['resourceDataCache'][0]['data']['owner']['username'];
$fullname = $json['resourceDataCache'][0]['data']['owner']['full_name'];
$avatar = $json['resourceDataCache'][0]['data']['owner']['image_small_url'];
private function fixLowRes() {
foreach($results as $result) {
$item = array();
$newitems = [];
$pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//';
foreach($this->items as $item) {
$item['uri'] = $result['link'];
// Some use regular titles, others provide 'advanced' infos, a few
// provide even less info. Thus we attempt multiple options.
$item['title'] = trim($result['title']);
if($item['title'] === "")
$item['title'] = trim($result['rich_summary']['display_name']);
if($item['title'] === "")
$item['title'] = trim($result['description']);
$item['timestamp'] = strtotime($result['created_at']);
$item['username'] = $username;
$item['fullname'] = $fullname;
$item['avatar'] = $avatar;
$item['author'] = $item['username'] . ' (' . $item['fullname'] . ')';
$item['content'] = '<img align="left" style="margin: 2px 4px;" src="'
. htmlentities($item['avatar'])
. '" /><p><strong>'
. $item['username']
. '</strong><br>'
. $item['fullname']
. '</p><br><img src="'
. $result['images']['736x']['url']
. '" alt="" /><br><p>'
. $result['description']
. '</p>';
$item['enclosures'] = array($result['images']['orig']['url']);
$this->items[] = $item;
$item["content"] = preg_replace($pattern, 'https://i.pinimg.com/originals/', $item["content"]);
$newitems[] = $item;
}
$this->items = $newitems;
}
private function getSearchResults($html){

73
bridges/PixivBridge.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
class PixivBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Pixiv Bridge';
const URI = 'https://www.pixiv.net/';
const DESCRIPTION = 'Returns the tag search from pixiv.net';
const PARAMETERS = array( array(
'tag' => array(
'name' => 'Tag to search',
'exampleValue' => 'example',
'required' => true
),
));
public function collectData(){
$html = getContents(static::URI.'search.php?word=' . urlencode($this->getInput('tag')))
or returnClientError('Unable to query pixiv.net');
$regex = '/<input type="hidden"id="js-mount-point-search-result-list"data-items="([^"]*)/';
$timeRegex = '/img\/([0-9]{4})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\/([0-9]{2})\//';
preg_match_all($regex, $html, $matches, PREG_SET_ORDER, 0);
if(!$matches) return;
$content = json_decode(html_entity_decode($matches[0][1]), true);
$count = 0;
foreach($content as $result) {
if($count == 10) break;
$count++;
$item = array();
$item["id"] = $result["illustId"];
$item["uri"] = "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=" . $result["illustId"];
$item["title"] = $result["illustTitle"];
$item["author"] = $result["userName"];
preg_match_all($timeRegex, $result["url"], $dt, PREG_SET_ORDER, 0);
$elementDate = DateTime::createFromFormat("YmdHis",
$dt[0][1] . $dt[0][2] . $dt[0][3] . $dt[0][4] . $dt[0][5] . $dt[0][6]);
$item["timestamp"] = $elementDate->getTimestamp();
$item["content"] = "<img src='" . $this->cacheImage($result['url'], $item["id"]) . "' />";
$this->items[] = $item;
}
}
public function cacheImage($url, $illustId) {
$url = str_replace("_master1200", "", $url);
$url = str_replace("c/240x240/img-master/", "img-original/", $url);
$path = CACHE_DIR . '/pixiv_img';
if(!is_dir($path))
mkdir($path, 0755, true);
if(!is_file($path . '/' . $illustId . '.jpeg')) {
$headers = array("Referer: https://www.pixiv.net/member_illust.php?mode=medium&illust_id=" . $illustId);
$illust = getContents($url, $headers);
if(strpos($illust, "404 Not Found") !== false) {
$illust = getContents(str_replace("jpg", "png", $url), $headers);
}
file_put_contents($path . '/' . $illustId . '.jpeg', $illust);
}
return 'cache/pixiv_img/' . $illustId . ".jpeg";
}
}

View File

@@ -1,38 +0,0 @@
<?php
class PlanetLibreBridge extends BridgeAbstract {
const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'PlanetLibre';
const URI = 'http://www.planet-libre.org';
const DESCRIPTION = 'Returns the 5 newest posts from PlanetLibre (full text)';
private function extractContent($url){
$html2 = getSimpleHTMLDOM($url);
$text = $html2->find('div[class="post-text"]', 0)->innertext;
return $text;
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request PlanetLibre.');
$limit = 0;
foreach($html->find('div.post') as $element) {
if($limit < 5) {
$item = array();
$item['title'] = $element->find('h1', 0)->plaintext;
$item['uri'] = $element->find('a', 0)->href;
$item['timestamp'] = strtotime(
str_replace(
'/',
'-',
$element->find('div[class="post-date"]', 0)->plaintext
)
);
$item['content'] = $this->extractContent($item['uri']);
$this->items[] = $item;
$limit++;
}
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
class RadioMelodieBridge extends BridgeAbstract {
const NAME = 'Radio Melodie Actu';
const URI = 'https://www.radiomelodie.com/';
const DESCRIPTION = 'Retourne les actualités publiées par Radio Melodie';
const MAINTAINER = 'sysadminstory';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . 'actu')
or returnServerError('Could not request Radio Melodie.');
$list = $html->find('div[class=actuitem]');
foreach($list as $element) {
$item = array();
// Get picture URL
$pictureHTML = $element->find('div[class=picture]');
preg_match(
'/background-image:url\((.*)\);/',
$pictureHTML[0]->getAttribute('style'),
$pictures);
$pictureURL = $pictures[1];
$item['enclosures'] = array($pictureURL);
$item['uri'] = self::URI . $element->parent()->href;
$item['title'] = $element->find('h3', 0)->plaintext;
$item['content'] = $element->find('p', 0)->plaintext . '<br/><img src="'.$pictureURL.'"/>';
$this->items[] = $item;
}
}
}

View File

@@ -14,7 +14,7 @@ class SoundCloudBridge extends BridgeAbstract {
)
));
const CLIENT_ID = '0aca19eae3843844e4053c6d8fdb7875';
const CLIENT_ID = '4jkoEFmZEDaqjwJ9Eih4ATNhcH3vMVfp';
public function collectData(){

View File

@@ -2,9 +2,9 @@
class SteamBridge extends BridgeAbstract {
const NAME = 'Steam Bridge';
const URI = 'https://steamcommunity.com/';
const URI = 'https://store.steampowered.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns games list';
const DESCRIPTION = 'Returns apps list';
const MAINTAINER = 'jacknumber';
const PARAMETERS = array(
'Wishlist' => array(
@@ -47,16 +47,6 @@ class SteamBridge extends BridgeAbstract {
'AED' => 'ae',
),
),
'sort' => array(
'name' => 'Sort by',
'type' => 'list',
'values' => array(
'Rank' => 'rank',
'Date Added' => 'added',
'Name' => 'name',
'Price' => 'price',
)
),
'only_discount' => array(
'name' => 'Only discount',
'type' => 'checkbox',
@@ -68,62 +58,100 @@ class SteamBridge extends BridgeAbstract {
$username = $this->getInput('username');
$params = array(
'sort' => $this->getInput('sort'),
'cc' => $this->getInput('currency')
);
$url = self::URI . 'id/' . $username . '/wishlist?' . http_build_query($params);
$url = self::URI . 'wishlist/id/' . $username . '?' . http_build_query($params);
$targetVariable = 'g_rgAppInfo';
$sort = array();
$html = '';
$html = getSimpleHTMLDOM($url)
or returnServerError("Could not request Steam Wishlist. Tried:\n - $url");
foreach($html->find('#wishlist_items .wishlistRow') as $element) {
$jsContent = $html->find('.responsive_page_template_content script', 0)->innertext;
$gameTitle = $element->find('h4', 0)->plaintext;
$gameUri = $element->find('.storepage_btn_ctn a', 0)->href;
$gameImg = $element->find('.gameListRowLogo img', 0)->src;
if(preg_match('/var ' . $targetVariable . ' = (.*?);/s', $jsContent, $matches)) {
$appsData = json_decode($matches[1]);
} else {
returnServerError("Could not parse JS variable ($targetVariable) in page content.");
}
$discountBlock = $element->find('.discount_block', 0);
foreach($appsData as $id => $element) {
$appType = $element->type;
$appIsBuyable = 0;
$appHasDiscount = 0;
$appIsFree = 0;
if($element->subs) {
$appIsBuyable = 1;
if($element->subs[0]->discount_pct) {
$appHasDiscount = 1;
$discountBlock = str_get_html($element->subs[0]->discount_block);
$appDiscountValue = $discountBlock->find('.discount_pct', 0)->plaintext;
$appOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
$appNewPrice = $discountBlock->find('.discount_final_price', 0)->plaintext;
$appPrice = $appNewPrice;
} else {
if($this->getInput('only_discount')) {
continue;
}
$appPrice = $element->subs[0]->price / 100;
}
if($element->find('.discount_block', 0)) {
$gameHasPromo = 1;
} else {
if($this->getInput('only_discount')) {
continue;
}
$gameHasPromo = 0;
}
if($gameHasPromo) {
$gamePromoValue = $discountBlock->find('.discount_pct', 0)->plaintext;
$gameOldPrice = $discountBlock->find('.discount_original_price', 0)->plaintext;
$gameNewPrice = $discountBlock->find('.discount_final_price', 0)->plaintext;
$gamePrice = $gameNewPrice;
} else {
$gamePrice = $element->find('.gameListPriceData .price', 0)->plaintext;
if(isset($element->free) && $element->free = 1) {
$appIsFree = 1;
}
}
$item = array();
$item['uri'] = $gameUri;
$item['title'] = $gameTitle;
$item['price'] = $gamePrice;
$item['hasPromo'] = $gameHasPromo;
$item['uri'] = "http://store.steampowered.com/app/$id/";
$item['title'] = $element->name;
$item['type'] = $appType;
$item['cover'] = str_replace('_292x136', '', $element->capsule);
$item['timestamp'] = $element->added;
$item['isBuyable'] = $appIsBuyable;
$item['hasDiscount'] = $appHasDiscount;
$item['isFree'] = $appIsFree;
$item['priority'] = $element->priority;
if($gameHasPromo) {
if($appIsBuyable) {
$item['price'] = floatval(str_replace(',', '.', $appPrice));
}
$item['promoValue'] = $gamePromoValue;
$item['oldPrice'] = $gameOldPrice;
$item['newPrice'] = $gameNewPrice;
if($appHasDiscount) {
$item['discount']['value'] = $appDiscountValue;
$item['discount']['oldPrice'] = floatval(str_replace(',', '.', $appOldPrice));
$item['discount']['newPrice'] = floatval(str_replace(',', '.', $appNewPrice));
}
$item['enclosures'] = array();
$item['enclosures'][] = str_replace('_292x136', '', $element->capsule);
foreach($element->screenshots as $screenshot) {
$item['enclosures'][] = substr($element->capsule, 0, -31) . $screenshot;
}
$sort[$id] = $element->priority;
$this->items[] = $item;
}
array_multisort($sort, SORT_ASC, $this->items);
}
}

57
bridges/SupInfoBridge.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
class SupInfoBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'SupInfoBridge';
const URI = 'https://www.supinfo.com';
const DESCRIPTION = 'Returns the newest articles.';
const PARAMETERS = array(array(
'tag' => array(
'name' => 'Category (not mandatory)',
'type' => 'text',
)
));
public function collectData() {
if(empty($this->getInput('tag'))) {
$html = getSimpleHTMLDOM(self::URI . '/articles/')
or returnServerError('Unable to fetch articles !');
} else {
$html = getSimpleHTMLDOM(self::URI . '/articles/tag/' . $this->getInput('tag'))
or returnServerError('Unable to fetch articles !');
}
$content = $html->find('#latest', 0)->find('ul[class=courseContent]', 0);
for($i = 0; $i < 5; $i++) {
$this->items[] = $this->fetchArticle($content->find('h4', $i)->find('a', 0)->href);
}
}
public function fetchArticle($link) {
$articleHTML = getSimpleHTMLDOM(self::URI . $link)
or returnServerError('Unable to fetch article !');
$article = $articleHTML->find('div[id=courseDocZero]', 0);
$item = array();
$item['author'] = $article->find('#courseMetas', 0)->find('a', 0)->plaintext;
$item['id'] = $link;
$item['uri'] = self::URI . $link;
$item['title'] = $article->find('h1', 0)->plaintext;
$date = explode(' ', $article->find('#courseMetas', 0)->find('span', 1)->plaintext);
$item['timestamp'] = DateTime::createFromFormat('d/m/Y H:i:s', $date[2] . ' ' . $date[4])->getTimestamp();
$article->find('div[id=courseHeader]', 0)->innertext = '';
$article->find('div[id=author-infos]', 0)->innertext = '';
$article->find('div[id=cartouche-tete]', 0)->innertext = '';
$item['content'] = $article;
return $item;
}
}

View File

@@ -1,96 +0,0 @@
<?php
class T411Bridge extends BridgeAbstract {
const MAINTAINER = 'ORelio';
const NAME = 'T411 Bridge';
const URI = 'https://www.t411.al/';
const DESCRIPTION = 'Returns the 10 newest torrents with specified search
terms <br /> Use url part after "?" mark when using their search engine.';
const PARAMETERS = array( array(
'search' => array(
'name' => 'Search criteria',
'required' => true
)
));
public function collectData(){
//Utility function for retrieving text based on start and end delimiters
function extractFromDelimiters($string, $start, $end){
if(strpos($string, $start) !== false) {
$section_retrieved = substr($string, strpos($string, $start) + strlen($start));
$section_retrieved = substr($section_retrieved, 0, strpos($section_retrieved, $end));
return $section_retrieved;
}
return false;
}
//Retrieve torrent listing from search results, which does not contain torrent description
$url = self::URI
. 'torrents/search/?search='
. urlencode($this->getInput('search'))
. '&order=added&type=desc';
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request t411: ' . $url);
$results = $html->find('table.results', 0);
if (is_null($results))
returnServerError('No results from t411: ' . $url);
$limit = 0;
//Process each item individually
foreach($results->find('tr') as $element) {
//Limit total amount of requests and ignore table header
if($limit >= 10) {
break;
}
if(is_object($element->find('th', 0))) {
continue;
}
//Requests are rate-limited
usleep(500000); //So we need to wait (500ms)
//Retrieve data from RSS entry
$item_uri = self::URI
. 'torrents/details/?id='
. extractFromDelimiters($element->find('a.nfo', 0)->outertext, '?id=', '"');
$item_title = extractFromDelimiters($element->outertext, '" title="', '"');
$item_date = strtotime($element->find('dd', 0)->plaintext);
//Retrieve full description from torrent page
$item_html = getSimpleHTMLDOM($item_uri);
if(!$item_html) {
continue;
}
//Retrieve data from page contents
$item_desc = $item_html->find('div.description', 0);
$item_author = $item_html->find('a.profile', 0)->innertext;
//Cleanup advertisments
$divs = explode('<div class="align-center">', $item_desc->innertext);
$item_desc = '';
foreach ($divs as $text)
if (strpos($text, 'adprovider.adlure.net') === false)
$item_desc = $item_desc . '<div class="align-center">' . $text;
$item_desc = preg_replace('/<h2 class="align-center">LIENS DE T..?L..?CHARGEMENT<\/h2>/i', '', $item_desc);
//Build and add final item
$item = array();
$item['uri'] = $item_uri;
$item['title'] = $item_title;
$item['author'] = $item_author;
$item['timestamp'] = $item_date;
$item['content'] = $item_desc;
$this->items[] = $item;
$limit++;
}
}
}

38
bridges/TebeoBridge.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
class TebeoBridge extends FeedExpander {
const NAME = 'Tébéo Bridge';
const URI = 'http://www.tebeo.bzh/';
const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'Returns the newest Tébéo videos by category';
const MAINTAINER = 'Mitsukarenai';
const PARAMETERS = array( array(
'cat' => array(
'name' => 'Catégorie',
'type' => 'list',
'values' => array(
'Toutes les vidéos' => '/',
'Actualité' => '/14-actualite',
'Sport' => '/3-sport',
'Culture-Loisirs' => '/5-culture-loisirs',
'Société' => '/15-societe',
'Langue Bretonne' => '/9-langue-bretonne'
)
)
));
public function collectData(){
$url = self::URI . '/le-replay/' . $this->getInput('cat');
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request Tébéo.');
foreach($html->find('div[id=items_replay] div.replay') as $element) {
$item = array();
$item['uri'] = $element->find('a', 0)->href;
$item['title'] = $element->find('h3', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('p.moment-format-day', 0)->plaintext);
$item['content'] = '<a href="'.$item['uri'].'"><img alt="" src="'.$element->find('img', 0)->src.'"></a>';
$this->items[] = $item;
}
}
}

View File

@@ -11,7 +11,7 @@ class ThePirateBayBridge extends BridgeAbstract {
const PARAMETERS = array( array(
'q' => array(
'name' => 'keywords, separated by semicolons',
'name' => 'keywords/username/category, separated by semicolons',
'exampleValue' => 'first list;second list;…',
'required' => true
),
@@ -24,9 +24,9 @@ class ThePirateBayBridge extends BridgeAbstract {
'user' => 'usr'
)
),
'cat_check' => array(
'catCheck' => array(
'type' => 'checkbox',
'name' => 'Specify category for normal search ?',
'name' => 'Specify category for keyword search ?',
),
'cat' => array(
'name' => 'Category number',
@@ -94,7 +94,7 @@ class ThePirateBayBridge extends BridgeAbstract {
return $timestamp;
}
$catBool = $this->getInput('cat_check');
$catBool = $this->getInput('catCheck');
if($catBool) {
$catNum = $this->getInput('cat');
}

View File

@@ -3,7 +3,7 @@ class Torrent9Bridge extends BridgeAbstract {
const MAINTAINER = 'lagaisse';
const NAME = 'Torrent9 Bridge';
const URI = 'http://www.torrent9.biz';
const URI = 'http://www.torrent9.pe';
const CACHE_TIMEOUT = 86400; // 24h = 86400s
const DESCRIPTION = 'Returns latest torrents';

View File

@@ -44,6 +44,25 @@ class TwitterBridge extends BridgeAbstract {
'type' => 'checkbox',
'title' => 'Hide retweets'
)
),
'By list' => array(
'user' => array(
'name' => 'User',
'required' => true,
'exampleValue' => 'sebsauvage',
'title' => 'Insert a user name'
),
'list' => array(
'name' => 'List',
'required' => true,
'title' => 'Insert the list name'
),
'filter' => array(
'name' => 'Filter',
'exampleValue' => '#rss-bridge',
'required' => false,
'title' => 'Specify term to search for'
)
)
);
@@ -57,6 +76,8 @@ class TwitterBridge extends BridgeAbstract {
$specific = '@';
$param = 'u';
break;
case 'By list':
return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
default: return parent::getName();
}
return 'Twitter ' . $specific . $this->getInput($param);
@@ -74,6 +95,11 @@ class TwitterBridge extends BridgeAbstract {
. urlencode($this->getInput('u'));
// Always return without replies!
// . ($this->getInput('norep') ? '' : '/with_replies');
case 'By list':
return self::URI
. urlencode($this->getInput('user'))
. '/lists/'
. str_replace(' ', '-', strtolower($this->getInput('list')));
default: return parent::getURI();
}
}
@@ -88,6 +114,8 @@ class TwitterBridge extends BridgeAbstract {
returnServerError('No results for this query.');
case 'By username':
returnServerError('Requested username can\'t be found.');
case 'By list':
returnServerError('Requested username or list can\'t be found');
}
}
@@ -132,6 +160,18 @@ class TwitterBridge extends BridgeAbstract {
// generate the title
$item['title'] = strip_tags($this->fixAnchorSpacing($tweet->find('p.js-tweet-text', 0), '<a>'));
switch($this->queriedContext) {
case 'By list':
// Check if filter applies to list (using raw content)
if($this->getInput('filter')) {
if(stripos($tweet->find('p.js-tweet-text', 0)->plaintext, $this->getInput('filter')) === false) {
continue 2; // switch + for-loop!
}
}
break;
default:
}
$this->processContentLinks($tweet);
$this->processEmojis($tweet);

View File

@@ -1,9 +1,11 @@
<?php
class VkBridge extends BridgeAbstract {
class VkBridge extends BridgeAbstract
{
const MAINTAINER = 'ahiles3005';
const NAME = 'VK.com';
const URI = 'http://vk.com/';
const URI = 'https://vk.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Working with open pages';
const PARAMETERS = array(
@@ -15,52 +17,287 @@ class VkBridge extends BridgeAbstract {
)
);
public function getURI(){
if(!is_null($this->getInput('u'))) {
protected $pageName;
public function getURI()
{
if (!is_null($this->getInput('u'))) {
return static::URI . urlencode($this->getInput('u'));
}
return parent::getURI();
}
public function collectData(){
public function getName()
{
if ($this->pageName) {
return $this->pageName;
}
ini_set('user-agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
return parent::getName();
}
$text_html = getContents($this->getURI())
or returnServerError('No results for group or user name "' . $this->getInput('u') . '".');
public function collectData()
{
$text_html = $this->getContents()
or returnServerError('No results for group or user name "' . $this->getInput('u') . '".');
$text_html = iconv('windows-1251', 'utf-8', $text_html);
// makes album link generating work correctly
$text_html = str_replace('"class="page_album_link">', '" class="page_album_link">', $text_html);
$html = str_get_html($text_html);
$pageName = $html->find('.page_name', 0);
if (is_object($pageName)) {
$pageName = $pageName->plaintext;
$this->pageName = htmlspecialchars_decode($pageName);
}
$pinned_post_item = null;
$last_post_id = 0;
foreach($html->find('.post') as $post) {
foreach ($html->find('.post') as $post) {
if(is_object($post->find('a.wall_post_more', 0))) {
$is_pinned_post = false;
if (strpos($post->getAttribute('class'), 'post_fixed') !== false) {
$is_pinned_post = true;
}
if (is_object($post->find('a.wall_post_more', 0))) {
//delete link "show full" in content
$post->find('a.wall_post_more', 0)->outertext = '';
}
$content_suffix = "";
// looking for external links
$external_link_selectors = array(
'a.page_media_link_title',
'div.page_media_link_title > a',
'div.media_desc > a.lnk',
);
foreach($external_link_selectors as $sel) {
if (is_object($post->find($sel, 0))) {
$a = $post->find($sel, 0);
$innertext = $a->innertext;
$parsed_url = parse_url($a->getAttribute('href'));
if (strpos($parsed_url['path'], '/away.php') !== 0) continue;
parse_str($parsed_url["query"], $parsed_query);
$content_suffix .= "<br>External link: <a href='" . $parsed_query["to"] . "'>$innertext</a>";
}
}
// remove external link from content
$external_link_selectors_to_remove = array(
'div.page_media_thumbed_link',
'div.page_media_link_desc_wrap',
'div.media_desc > a.lnk',
);
foreach($external_link_selectors_to_remove as $sel) {
if (is_object($post->find($sel, 0))) {
$post->find($sel, 0)->outertext = '';
}
}
// looking for article
$article = $post->find("a.article_snippet", 0);
if (is_object($article)) {
$article_title = $article->find("div.article_snippet__title", 0)->innertext;
$article_author = $article->find("div.article_snippet__author", 0)->innertext;
$article_link = self::URI . ltrim($article->getAttribute('href'), '/');
$article_img_element_style = $article->find("div.article_snippet__image", 0)->getAttribute('style');
preg_match('/background-image: url\((.*)\)/', $article_img_element_style, $matches);
if (count($matches) > 0) {
$content_suffix .= "<br><img src='" . $matches[1] . "'>";
}
$content_suffix .= "<br>Article: <a href='$article_link'>$article_title ($article_author)</a>";
$article->outertext = '';
}
// get video on post
$video = $post->find('div.post_video_desc', 0);
if (is_object($video)) {
$video_title = $video->find('div.post_video_title', 0)->plaintext;
$video_link = self::URI . ltrim( $video->find('a.lnk', 0)->getAttribute('href'), '/' );
$content_suffix .= "<br>Video: <a href='$video_link'>$video_title</a>";
$video->outertext = '';
}
// get all photos
foreach($post->find('div.wall_text > a.page_post_thumb_wrap') as $a) {
$result = $this->getPhoto($a);
if ($result == null) continue;
$a->outertext = '';
$content_suffix .= "<br>$result";
}
// get albums
foreach($post->find('.page_album_wrap') as $el) {
$a = $el->find('.page_album_link', 0);
$album_title = $a->find('.page_album_title_text', 0)->getAttribute('title');
$album_link = self::URI . ltrim($a->getAttribute('href'), '/');
$el->outertext = '';
$content_suffix .= "<br>Album: <a href='$album_link'>$album_title</a>";
}
// get photo documents
foreach($post->find('a.page_doc_photo_href') as $a) {
$doc_link = self::URI . ltrim($a->getAttribute('href'), '/');
$doc_gif_label_element = $a->find(".page_gif_label", 0);
$doc_title_element = $a->find(".doc_label", 0);
if (is_object($doc_gif_label_element)) {
$gif_preview_img = backgroundToImg($a->find('.page_doc_photo', 0));
$content_suffix .= "<br>Gif: <a href='$doc_link'>$gif_preview_img</a>";
} else if (is_object($doc_title_element)) {
$doc_title = $doc_title_element->innertext;
$content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
} else {
continue;
}
$a->outertext = '';
}
// get other documents
foreach($post->find('div.page_doc_row') as $div) {
$doc_title_element = $div->find("a.page_doc_title", 0);
if (is_object($doc_title_element)) {
$doc_title = $doc_title_element->innertext;
$doc_link = self::URI . ltrim($doc_title_element->getAttribute('href'), '/');
$content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a>";
} else {
continue;
}
$div->outertext = '';
}
// get sign
$post_author = $pageName;
foreach($post->find('a.wall_signed_by') as $a) {
$post_author = $a->innertext;
$a->outertext = '';
}
if (is_object($post->find('div.copy_quote', 0))) {
$copy_quote = $post->find('div.copy_quote', 0);
if ($copy_post_header = $copy_quote->find('div.copy_post_header', 0)) {
$copy_post_header->outertext = '';
}
$copy_quote_content = $copy_quote->innertext;
$copy_quote->outertext = "<br>Reposted: <br>$copy_quote_content";
}
$item = array();
$item['content'] = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<br><img>');
if(is_object($post->find('a.page_media_link_title', 0))) {
$link = $post->find('a.page_media_link_title', 0)->getAttribute('href');
//external link in the post
$item['content'] .= "\n\rExternal link: "
. str_replace('/away.php?to=', '', urldecode($link));
}
//get video on post
if(is_object($post->find('span.post_video_title_content', 0))) {
$titleVideo = $post->find('span.post_video_title_content', 0)->plaintext;
$linkToVideo = self::URI . $post->find('a.page_post_thumb_video', 0)->getAttribute('href');
$item['content'] .= "\n\r {$titleVideo}: {$linkToVideo}";
}
$item['content'] .= $content_suffix;
// get post link
$item['uri'] = self::URI . $post->find('a.post_link', 0)->getAttribute('href');
$item['date'] = $post->find('span.rel_date', 0)->plaintext;
$this->items[] = $item;
// var_dump($item['date']);
$post_link = $post->find('a.post_link', 0)->getAttribute('href');
preg_match("/wall-?\d+_(\d+)/", $post_link, $preg_match_result);
$item['post_id'] = intval($preg_match_result[1]);
if (substr(self::URI, -1) == '/') {
$post_link = self::URI . ltrim($post_link, "/");
} else {
$post_link = self::URI . $post_link;
}
$item['uri'] = $post_link;
$item['timestamp'] = $this->getTime($post);
$item['title'] = $this->getTitle($item['content']);
$item['author'] = $post_author;
if ($is_pinned_post) {
// do not append it now
$pinned_post_item = $item;
} else {
$last_post_id = $item['post_id'];
$this->items[] = $item;
}
}
if (is_null($pinned_post_item)) {
return;
} else if (count($this->items) == 0) {
$this->items[] = $pinned_post_item;
} else if ($last_post_id < $pinned_post_item['post_id']) {
$this->items[] = $pinned_post_item;
usort($this->items, function ($item1, $item2) {
return $item2['post_id'] - $item1['post_id'];
});
}
}
private function getPhoto($a) {
$onclick = $a->getAttribute('onclick');
preg_match('/return showPhoto\(.+?({.*})/', $onclick, $preg_match_result);
if (count($preg_match_result) == 0) return;
$arg = htmlspecialchars_decode( str_replace('queue:1', '"queue":1', $preg_match_result[1]) );
$data = json_decode($arg, true);
if ($data == null) return;
$thumb = $data['temp']['base'] . $data['temp']['x_'][0] . ".jpg";
$original = '';
foreach(array('y_', 'z_', 'w_') as $key) {
if (!isset($data['temp'][$key])) continue;
$original = $data['temp']['base'] . $data['temp'][$key][0] . ".jpg";
}
if ($original) {
return "<a href='$original'><img src='$thumb'></a>";
} else {
return "<img src='$thumb'>";
}
}
private function getTitle($content)
{
preg_match('/^["\w\ \p{Cyrillic}\(\)\?#«»-]+/mu', htmlspecialchars_decode($content), $result);
if (count($result) == 0) return "untitled";
return $result[0];
}
private function getTime($post)
{
if ($time = $post->find('span.rel_date', 0)->getAttribute('time')) {
return $time;
} else {
$strdate = $post->find('span.rel_date', 0)->plaintext;
$date = date_parse($strdate);
if (!$date['year']) {
if (strstr($strdate, 'today') !== false) {
$strdate = date('d-m-Y') . ' ' . $strdate;
} elseif (strstr($strdate, 'yesterday ') !== false) {
$time = time() - 60 * 60 * 24;
$strdate = date('d-m-Y', $time) . ' ' . $strdate;
} else {
$strdate = $strdate . ' ' . date('Y');
}
$date = date_parse($strdate);
}
return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' .
$date['hour'] . ':' . $date['minute']);
}
}
public function getContents()
{
ini_set('user-agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0');
$header = array('Accept-language: en', 'Cookie: remixlang=3');
return getContents($this->getURI(), $header);
}
}

View File

@@ -0,0 +1,138 @@
<?php
/* This is a mashup of FlickrExploreBridge by sebsauvage and FlickrTagBridge
* by erwang.providing the functionality of both in one.
*/
class YGGTorrentBridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Yggtorrent Bridge';
const URI = 'https://yggtorrent.is';
const DESCRIPTION = 'Returns torrent search from Yggtorrent';
const PARAMETERS = array(
array(
"cat" => array(
"name" => "category",
"type" => "list",
"values" => array(
"Toute les catégories" => "all.all",
"Film/Vidéo - Toutes les sous-catégories" => "2145.all",
"Film/Vidéo - Animation" => "2145.2178",
"Film/Vidéo - Animation Série" => "2145.2179",
"Film/Vidéo - Concert" => "2145.2180",
"Film/Vidéo - Documentaire" => "2145.2181",
"Film/Vidéo - Émission TV" => "2145.2182",
"Film/Vidéo - Film" => "2145.2183",
"Film/Vidéo - Série TV" => "2145.2184",
"Film/Vidéo - Spectacle" => "2145.2185",
"Film/Vidéo - Sport" => "2145.2186",
"Film/Vidéo - Vidéo-clips" => "2145.2186",
"Audio - Toutes les sous-catégories" => "2139.all",
"Audio - Karaoké" => "2139.2147",
"Audio - Musique" => "2139.2148",
"Audio - Podcast Radio" => "2139.2150",
"Audio - Samples" => "2139.2149",
"Jeu vidéo - Toutes les sous-catégories" => "2142.all",
"Jeu vidéo - Autre" => "2142.2167",
"Jeu vidéo - Linux" => "2142.2159",
"Jeu vidéo - MacOS" => "2142.2160",
"Jeu vidéo - Microsoft" => "2142.2162",
"Jeu vidéo - Nintendo" => "2142.2163",
"Jeu vidéo - Smartphone" => "2142.2165",
"Jeu vidéo - Sony" => "2142.2164",
"Jeu vidéo - Tablette" => "2142.2166",
"Jeu vidéo - Windows" => "2142.2161",
"eBook - Toutes les sous-catégories" => "2140.all",
"eBook - Audio" => "2140.2151",
"eBook - Bds" => "2140.2152",
"eBook - Comics" => "2140.2153",
"eBook - Livres" => "2140.2154",
"eBook - Mangas" => "2140.2155",
"eBook - Presse" => "2140.2156",
"Emulation - Toutes les sous-catégories" => "2141.all",
"Emulation - Emulateurs" => "2141.2157",
"Emulation - Roms" => "2141.2158",
"GPS - Toutes les sous-catégories" => "2141.all",
"GPS - Applications" => "2141.2168",
"GPS - Cartes" => "2141.2169",
"GPS - Divers" => "2141.2170"
)
),
"nom" => array(
"name" => "Nom",
"description" => "Nom du torrent",
"type" => "text"
),
"description" => array(
"name" => "Description",
"description" => "Description du torrent",
"type" => "text"
),
"fichier" => array(
"name" => "Fichier",
"description" => "Fichier du torrent",
"type" => "text"
),
"uploader" => array(
"name" => "Uploader",
"description" => "Uploader du torrent",
"type" => "text"
),
)
);
public function collectData() {
$catInfo = explode(".", $this->getInput("cat"));
$category = $catInfo[0];
$subcategory = $catInfo[1];
$html = getSimpleHTMLDOM(self::URI . "/engine/search?name="
. $this->getInput("nom")
. "&description="
. $this->getInput("description")
. "&fichier="
. $this->getInput("fichier")
. "&file="
. $this->getInput("uploader")
. "&category="
. $category
. "&sub_category="
. $subcategory
. "&do=search")
or returnServerError("Unable to query Yggtorrent !");
$count = 0;
foreach($html->find(".results", 0)->find("tr") as $row) {
$count++;
if($count == 1) continue;
if($count == 12) break;
$item = array();
$item["timestamp"] = $row->find(".hidden", 1)->plaintext;
$item["title"] = $row->find("a", 1)->plaintext;
$torrentData = $this->collectTorrentData($row->find("a", 1)->href);
$item["author"] = $torrentData["author"];
$item["content"] = $torrentData["content"];
$item["seeders"] = $row->find("td", 7)->plaintext;
$item["leechers"] = $row->find("td", 8)->plaintext;
$item["size"] = $row->find("td", 5)->plaintext;
$this->items[] = $item;
}
}
public function collectTorrentData($url) {
//For weird reason, the link we get can be invalid, we fix it.
$url_full = explode("/", $url);
$url_full[6] = urlencode($url_full[6]);
$url = implode("/", $url_full);
$page = getSimpleHTMLDOM($url) or returnServerError("Unable to query Yggtorrent page !");
$author = $page->find(".informations", 0)->find("a", 4)->plaintext;
$content = $page->find(".default", 1);
return array("author" => $author, "content" => $content);
}
}

View File

@@ -50,12 +50,28 @@ class YoutubeBridge extends BridgeAbstract {
private function ytBridgeQueryVideoInfo($vid, &$author, &$desc, &$time){
$html = $this->ytGetSimpleHTMLDOM(self::URI . "watch?v=$vid");
$author = $html->innertext;
$author = substr($author, strpos($author, '"author=') + 8);
$author = substr($author, 0, strpos($author, '\u0026'));
if(!is_null($html->find('div#watch-description-text', 0)))
$desc = $html->find('div#watch-description-text', 0)->innertext;
// Skip unavailable videos
if(!strpos($html->innertext, 'IS_UNAVAILABLE_PAGE')) {
return;
}
foreach($html->find('script') as $script) {
$data = trim($script->innertext);
if(strpos($data, '{') !== 0)
continue; // Wrong script
$json = json_decode($data);
if(!isset($json->itemListElement))
continue; // Wrong script
$author = $json->itemListElement[0]->item->name;
}
if(!is_null($html->find('#watch-description-text', 0)))
$desc = $html->find('#watch-description-text', 0)->innertext;
if(!is_null($html->find('meta[itemprop=datePublished]', 0)))
$time = strtotime($html->find('meta[itemprop=datePublished]', 0)->getAttribute('content'));
@@ -88,13 +104,14 @@ class YoutubeBridge extends BridgeAbstract {
$vid = str_replace('yt:video:', '', $element->find('id', 0)->plaintext);
$time = strtotime($element->find('published', 0)->plaintext);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
if(strpos($vid, 'googleads') === false)
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
}
$this->request = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext);
$this->feedName = $this->ytBridgeFixTitle($xml->find('feed > title', 0)->plaintext); // feedName will be used by getName()
}
private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector){
$limit = 10;
private function ytBridgeParseHtmlListing($html, $element_selector, $title_selector, $add_parsed_items = true) {
$limit = $add_parsed_items ? 10 : INF;
$count = 0;
foreach($html->find($element_selector) as $element) {
if($count < $limit) {
@@ -104,13 +121,16 @@ class YoutubeBridge extends BridgeAbstract {
$vid = str_replace('/watch?v=', '', $element->find('a', 0)->href);
$vid = substr($vid, 0, strpos($vid, '&') ?: strlen($vid));
$title = $this->ytBridgeFixTitle($element->find($title_selector, 0)->plaintext);
if($title != '[Private Video]') {
$this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
if($title != '[Private Video]' && strpos($vid, 'googleads') === false) {
if ($add_parsed_items) {
$this->ytBridgeQueryVideoInfo($vid, $author, $desc, $time);
$this->ytBridgeAddItem($vid, $title, $author, $desc, $time);
}
$count++;
}
}
}
return $count;
}
private function ytBridgeFixTitle($title) {
@@ -120,10 +140,8 @@ class YoutubeBridge extends BridgeAbstract {
private function ytGetSimpleHTMLDOM($url){
return getSimpleHTMLDOM($url,
$use_include_path = false,
$context = null,
$offset = 0,
$maxLen = null,
$header = array(),
$opts = array(),
$lowercase = true,
$forceTagsClosed = true,
$target_charset = DEFAULT_TARGET_CHARSET,
@@ -159,11 +177,20 @@ class YoutubeBridge extends BridgeAbstract {
}
} elseif($this->getInput('p')) { /* playlist mode */
$this->request = $this->getInput('p');
$url_feed = self::URI . 'feeds/videos.xml?playlist_id=' . urlencode($this->request);
$url_listing = self::URI . 'playlist?list=' . urlencode($this->request);
$html = $this->ytGetSimpleHTMLDOM($url_listing)
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
$this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a');
$this->request = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
$item_count = $this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a', false);
if ($item_count <= 15 && ($xml = $this->ytGetSimpleHTMLDOM($url_feed))) {
$this->ytBridgeParseXmlFeed($xml);
} else {
$this->ytBridgeParseHtmlListing($html, 'tr.pl-video', '.pl-video-title a');
}
$this->feedName = 'Playlist: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
usort($this->items, function ($item1, $item2) {
return $item2['timestamp'] - $item1['timestamp'];
});
} elseif($this->getInput('s')) { /* search mode */
$this->request = $this->getInput('s');
$page = 1;
@@ -180,8 +207,8 @@ class YoutubeBridge extends BridgeAbstract {
$html = $this->ytGetSimpleHTMLDOM($url_listing)
or returnServerError("Could not request YouTube. Tried:\n - $url_listing");
$this->ytBridgeParseHtmlListing($html, 'div.yt-lockup', 'h3');
$this->request = 'Search: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext);
$this->ytBridgeParseHtmlListing($html, 'div.yt-lockup', 'h3 > a');
$this->feedName = 'Search: ' . str_replace(' - YouTube', '', $html->find('title', 0)->plaintext); // feedName will be used by getName()
} else { /* no valid mode */
returnClientError("You must either specify either:\n - YouTube
username (?u=...)\n - Channel id (?c=...)\n - Playlist id (?p=...)\n - Search (?s=...)");
@@ -189,6 +216,15 @@ class YoutubeBridge extends BridgeAbstract {
}
public function getName(){
return (!empty($this->request) ? $this->request . ' - ' : '') . 'YouTube Bridge';
}
// Name depends on queriedContext:
switch($this->queriedContext) {
case 'By username':
case 'By channel id':
case 'By playlist Id':
case 'Search result':
return $this->feedName . ' - YouTube'; // We already know it's a bridge, right?
default:
return parent::getName();
}
}
}

View File

@@ -19,6 +19,10 @@ define('PROXY_BYBRIDGE', false);
// Comment this line or keep PROXY_NAME empty to display PROXY_URL instead
define('PROXY_NAME', 'Hidden Proxy Name');
// Allows the operator to specify custom cache timeouts via '&_cache_timeout=3600'
// true: enabled, false: disabled (default)
define('CUSTOM_CACHE_TIMEOUT', false);
date_default_timezone_set('UTC');
error_reporting(0);
@@ -28,6 +32,14 @@ define('CACHE_DIR', __DIR__ . '/cache');
// Specify path for whitelist file
define('WHITELIST_FILE', __DIR__ . '/whitelist.txt');
/*
Move the CLI arguments to the $_GET array, in order to be able to use
rss-bridge from the command line
*/
parse_str(implode('&', array_slice($argv, 1)), $cliArgs);
$params = array_merge($_GET, $cliArgs);
/*
Create a file named 'DEBUG' for enabling debug mode.
For further security, you may put whitelisted IP addresses in the file,
@@ -62,6 +74,15 @@ if(!extension_loaded('openssl'))
if(!extension_loaded('libxml'))
die('"libxml" extension not loaded. Please check "php.ini"');
if(!extension_loaded('mbstring'))
die('"mbstring" extension not loaded. Please check "php.ini"');
if(!extension_loaded('simplexml'))
die('"simplexml" extension not loaded. Please check "php.ini"');
if(!extension_loaded('curl'))
die('"curl" extension not loaded. Please check "php.ini"');
// configuration checks
if(ini_get('allow_url_fopen') !== "1")
die('"allow_url_fopen" is not set to "1". Please check "php.ini');
@@ -124,8 +145,8 @@ try {
$whitelist_selection = array_map('strtolower', $whitelist_selection);
}
$action = filter_input(INPUT_GET, 'action');
$bridge = filter_input(INPUT_GET, 'bridge');
$action = array_key_exists('action', $params) ? $params['action'] : null;
$bridge = array_key_exists('bridge', $params) ? $params['bridge'] : null;
if($action === 'display' && !empty($bridge)) {
// DEPRECATED: 'nameBridge' scheme is replaced by 'name' in bridge parameter values
@@ -134,7 +155,8 @@ try {
$bridge = substr($bridge, 0, $pos);
}
$format = filter_input(INPUT_GET, 'format');
$format = $params['format']
or returnClientError('You must specify a format!');
// DEPRECATED: 'nameFormat' scheme is replaced by 'name' in format parameter values
// this is to keep compatibility until futher complete removal
@@ -151,12 +173,20 @@ try {
// Data retrieval
$bridge = Bridge::create($bridge);
$noproxy = filter_input(INPUT_GET, '_noproxy', FILTER_VALIDATE_BOOLEAN);
$noproxy = array_key_exists('_noproxy', $params) && filter_var($params['_noproxy'], FILTER_VALIDATE_BOOLEAN);
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
define('NOPROXY', true);
}
$params = $_GET;
// Custom cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $params)) {
if(!CUSTOM_CACHE_TIMEOUT) {
throw new \HttpException('This server doesn\'t support "_cache_timeout"!');
}
$cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
}
// Initialize cache
$cache = Cache::create('FileCache');
@@ -168,11 +198,17 @@ try {
unset($params['bridge']);
unset($params['format']);
unset($params['_noproxy']);
unset($params['_cache_timeout']);
// Load cache & data
try {
$bridge->setCache($cache);
$bridge->setCacheTimeout($cache_timeout);
$bridge->setDatas($params);
} catch(Error $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
} catch(Exception $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
@@ -185,10 +221,14 @@ try {
$format->setItems($bridge->getItems());
$format->setExtraInfos($bridge->getExtraInfos());
$format->display();
} catch(Exception $e) {
} catch(Error $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
http_response_code($e->getCode());
header('Content-Type: text/html');
die(buildBridgeException($e, $bridge));
}
die;
@@ -263,7 +303,7 @@ EOD;
echo $inactiveBridges;
?>
<section class="footer">
<a href="https://github.com/RSS-Bridge/rss-bridge">RSS-Bridge 2017-08-19 ~ Public Domain</a><br />
<a href="https://github.com/RSS-Bridge/rss-bridge">RSS-Bridge 2018-04-20 ~ Public Domain</a><br />
<?= $activeFoundBridgeCount; ?>/<?= count($bridgeList) ?> active bridges. <br />
<?php
if($activeFoundBridgeCount !== count($bridgeList)) {

View File

@@ -14,6 +14,7 @@ abstract class BridgeAbstract implements BridgeInterface {
protected $items = array();
protected $inputs = array();
protected $queriedContext = '';
protected $cacheTimeout;
/**
* Return cachable datas (extrainfos and items) stored in the bridge
@@ -171,7 +172,7 @@ abstract class BridgeAbstract implements BridgeInterface {
if(!is_null($this->cache)) {
$time = $this->cache->getTime();
if($time !== false
&& (time() - static::CACHE_TIMEOUT < $time)
&& (time() - $this->getCacheTimeout() < $time)
&& (!defined('DEBUG') || DEBUG !== true)) {
$cached = $this->cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
@@ -268,4 +269,17 @@ abstract class BridgeAbstract implements BridgeInterface {
public function setCache(\CacheInterface $cache){
$this->cache = $cache;
}
public function setCacheTimeout($timeout){
if(is_numeric($timeout) && ($timeout < 1 || $timeout > 86400)) {
$this->cacheTimeout = static::CACHE_TIMEOUT;
return;
}
$this->cacheTimeout = $timeout;
}
public function getCacheTimeout(){
return isset($this->cacheTimeout) ? $this->cacheTimeout : static::CACHE_TIMEOUT;
}
}

View File

@@ -68,4 +68,20 @@ interface BridgeInterface {
* @param object CacheInterface The cache instance
*/
public function setCache(\CacheInterface $cache);
/**
* Sets the timeout for clearing the cache files. The timeout must be
* specified between 1..86400 seconds (max. 24 hours). The default timeout
* (specified by the bridge maintainer) applies for invalid values.
*
* @param int $timeout The cache timeout in seconds
*/
public function setCacheTimeout($timeout);
/**
* Returns the cache timeout
*
* @return int Cache timeout
*/
public function getCacheTimeout();
}

View File

@@ -54,7 +54,7 @@ function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null
* provided parameter are invalid
*/
function buildBridgeException($e, $bridge){
if(!($e instanceof \Exception) || !($bridge instanceof \BridgeInterface)) {
if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
return null;
}
@@ -85,7 +85,7 @@ unable to receive or process the remote website's content!";
* provided parameter are invalid
*/
function buildTransformException($e, $bridge){
if(!($e instanceof \Exception) || !($bridge instanceof \BridgeInterface)) {
if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
return null;
}

View File

@@ -1,77 +1,45 @@
<?php
function getContents($url,
$use_include_path = false,
$context = null,
$offset = 0,
$maxlen = null){
$contextOptions = array(
'http' => array(
'user_agent' => ini_get('user_agent'),
'accept_encoding' => 'gzip'
)
);
function getContents($url, $header = array(), $opts = array()){
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
if(defined('PROXY_URL') && !defined('NOPROXY')) {
$contextOptions['http']['proxy'] = PROXY_URL;
$contextOptions['http']['request_fulluri'] = true;
if(is_array($header) && count($header) !== 0)
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
if(is_null($context)) {
$context = stream_context_create($contextOptions);
} else {
$prevContext = $context;
if(!stream_context_set_option($context, $contextOptions)) {
$context = $prevContext;
}
curl_setopt($ch, CURLOPT_USERAGENT, ini_get('user_agent'));
curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
if(is_array($opts)) {
foreach($opts as $key => $value) {
curl_setopt($ch, $key, $value);
}
}
if(is_null($maxlen)) {
$content = file_get_contents($url, $use_include_path, $context, $offset);
} else {
$content = file_get_contents($url, $use_include_path, $context, $offset, $maxlen);
if(defined('PROXY_URL') && !defined('NOPROXY')) {
curl_setopt($ch, CURLOPT_PROXY, PROXY_URL);
}
$content = curl_exec($ch);
curl_close($ch);
if($content === false)
debugMessage('Cant\'t download ' . $url);
// handle compressed data
foreach($http_response_header as $header) {
if(stristr($header, 'content-encoding')) {
switch(true) {
case stristr($header, 'gzip'):
$content = gzinflate(substr($content, 10, -8));
break;
case stristr($header, 'compress'):
//TODO
case stristr($header, 'deflate'):
//TODO
case stristr($header, 'brotli'):
//TODO
returnServerError($header . '=> Not implemented yet');
break;
case stristr($header, 'identity'):
break;
default:
returnServerError($header . '=> Unknown compression');
}
}
}
return $content;
}
function getSimpleHTMLDOM($url,
$use_include_path = false,
$context = null,
$offset = 0,
$maxLen = null,
$header = array(),
$opts = array(),
$lowercase = true,
$forceTagsClosed = true,
$target_charset = DEFAULT_TARGET_CHARSET,
$stripRN = true,
$defaultBRText = DEFAULT_BR_TEXT,
$defaultSpanText = DEFAULT_SPAN_TEXT){
$content = getContents($url, $use_include_path, $context, $offset, $maxLen);
$content = getContents($url, $header, $opts);
return str_get_html($content,
$lowercase,
$forceTagsClosed,
@@ -89,10 +57,8 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
*/
function getSimpleHTMLDOMCached($url,
$duration = 86400,
$use_include_path = false,
$context = null,
$offset = 0,
$maxLen = null,
$header = array(),
$opts = array(),
$lowercase = true,
$forceTagsClosed = true,
$target_charset = DEFAULT_TARGET_CHARSET,
@@ -116,7 +82,7 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
&& (!defined('DEBUG') || DEBUG !== true)) { // Contents within duration
$content = $cache->loadData();
} else { // Content not within duration
$content = getContents($url, $use_include_path, $context, $offset, $maxLen);
$content = getContents($url, $header, $opts);
if($content !== false) {
$cache->saveData($content);
}

View File

@@ -75,8 +75,24 @@ CARD;
. ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL)
. ')</label><br />'
. PHP_EOL;
}
} if(CUSTOM_CACHE_TIMEOUT) {
$idArg = 'arg-'
. urlencode($bridgeName)
. '-'
. urlencode('_cache_timeout');
$card .= '<label for="'
. $idArg
. '">Cache timeout in seconds : </label>'
. PHP_EOL;
$card .= '<input id="'
. $idArg
. '" type="number" value="'
. $bridge->getCacheTimeout()
. '" name="_cache_timeout" /><br />'
. PHP_EOL;
}
$card .= $getHelperButtonsFormat($formats);
} else {
$card .= '<span style="font-weight: bold;">Inactive</span>';
@@ -251,6 +267,23 @@ CARD;
. ((defined('PROXY_NAME') && PROXY_NAME) ? PROXY_NAME : PROXY_URL)
. ')</label><br />'
. PHP_EOL;
} if(CUSTOM_CACHE_TIMEOUT) {
$idArg = 'arg-'
. urlencode($bridgeName)
. '-'
. urlencode('_cache_timeout');
$card .= '<label for="'
. $idArg
. '">Cache timeout in seconds : </label>'
. PHP_EOL;
$card .= '<input id="'
. $idArg
. '" type="number" value="'
. $bridge->getCacheTimeout()
. '" name="_cache_timeout" /><br />'
. PHP_EOL;
}
$card .= $getHelperButtonsFormat($formats);
} else {

View File

@@ -5,7 +5,6 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq
border: 0;
outline: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
@@ -111,3 +110,8 @@ button.backbutton, button.rss-feed {
}
img {
max-width: 100%;
}