1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-16 13:34:11 +02:00

Compare commits

...

152 Commits

Author SHA1 Message Date
Lyra
0705a2e7bb Bump version to dev.2020-02-26 2020-02-26 22:24:20 +01:00
Lyra
84616f53bf Update contributors 2020-02-26 22:23:30 +01:00
Eugene Molotov
a981450ae0 [Dockerfile] Build memcached extension (#1415) 2020-02-26 22:16:46 +01:00
somini
d39741c296 [GithubIssueBridgeIssue] Fix bridge (#1453)
* fix bridge according to website evolution
2020-02-26 22:15:50 +01:00
Lorenzo Stanco
3179c1e884 [InstagramBridge] Fixed item thumb on video entries (#1387) 2020-02-26 22:13:40 +01:00
sysadminstory
c9e5f6c9dd [AllocineFRBridge] Update Show List and parsing (#1407)
* [AllocineFRBridge] Update Show List and parsing
2020-02-26 22:12:25 +01:00
Julien Desgats
6b6974d115 [NewOnNetflix] Add new bridge (#1408) 2020-02-26 22:11:54 +01:00
Anchit Bajaj
96e58d4c94 Add bridge for Phoronix (#1412) 2020-02-26 22:10:54 +01:00
Anchit Bajaj
f0363ba03b [PcGamerBridge] - Add all articles, full content and images (#1420) 2020-02-26 22:10:09 +01:00
somini
90147fc45c [FirstLookMediaTech]: New Bridge (#1438) 2020-02-26 22:08:14 +01:00
John Corser
a3b4bd2d08 [DaveRamseyBlogBridge] Add new bridge (#1459) 2020-02-26 22:05:55 +01:00
St. John Johnson
e102353ab8 [GoComics] Update to new website structure (#1464)
GoComics.com has updated their website.  The image location is now a
data attribute in a div.
2020-02-26 21:56:52 +01:00
Joseph
a54eb88ee1 [DevToBridge] Fix bridge & add getName() (#1470) 2020-02-26 21:56:03 +01:00
somini
1584636e5b TinyLetter: New Bridge (#1469)
* TinyLetter: New Bridge
2020-02-26 21:50:25 +01:00
Joseph
fe83d763a3 [PornhubBridge] Fix travis issues (#1471)
* [PornhubBridge] Fix travis issues
2020-02-26 21:34:46 +01:00
Mitsukarenai
480694e819 [PornhubBridge] Add bridge 2020-02-15 00:03:29 +01:00
Tyler Kenney
8697e1e1a2 [RoosterTeethBridge] Add a new bridge (#1450)
* Added RoosterTeethBridge
2020-02-10 16:57:08 +01:00
Binnette
1ab7e493a8 [DonnonsBridge] Add a new bridge (#1441) 2020-02-10 16:56:40 +01:00
86423355844265459587182778
e5303efba3 [SoundcloudBridge] Fix returned URL and title (#1449) 2020-02-07 16:16:55 +01:00
Joseph
5bd07723ad [ScribdBridge] Add bridge (#1391) 2020-02-04 17:26:34 +01:00
Anchit Bajaj
00dbde2c24 [IGNBridge] Removed Ugly Nonworking Widgets (#1413) 2020-02-04 17:25:56 +01:00
floviolleau
a00e75b71c [AtmoOccitanieBridge] Add new bridge for air quality in cities in Occitanie (#1422)
* Add new bridge for Air Quality in cities supported by Atmo Occitanie
2020-02-04 17:24:42 +01:00
floviolleau
f040e4dc9c [AtmoNouvelleAquitaine] Change description (#1423)
* [AtmoNouvelleAquitaine] Change description
2020-02-04 17:22:42 +01:00
sysadminstory
182e9e7b41 [ZoneTelechargement] Update URL (#1425)
Website changed again his URL
2020-02-04 17:21:02 +01:00
somini
275662b8d4 [FolhaDeSaoPaulo]: Add new Bridge (#1426)
* [FolhaDeSaoPaulo]: Add new Bridge
2020-02-04 17:19:39 +01:00
Antoine Turmel
f52eb43f8c Update GithubSearchBridge.php (#1431)
Fixes #1430
2020-01-31 15:01:46 +01:00
sysadminstory
2450f80823 [ExtremeDownloadBridge] Update URL (#1429)
Website URL has changed again !
2020-01-31 15:00:17 +01:00
Corentin Garcia
45287e6853 [RainbowSixSiegeBridge] Fix bridge (#1433) 2020-01-31 14:51:59 +01:00
Eugene Molotov
830f57f607 [TwitterBridge] Use IE's user-agent (#1442)
Twitter will return pages with legacy design and frontend code, which bridge can deal with
2020-01-31 14:36:25 +01:00
Eugene Molotov
6a90a9d33f phpcs: fix new sudden violations (#1443) 2020-01-31 14:30:31 +01:00
Eugene Molotov
46b9879c08 [VkBridge] Correct post date calculating (#1417)
* [VkBridge] Correct post date calculating

Before this commit, post dates from december past year were
calculated as december current year.
2020-01-16 12:00:10 +01:00
Mitsukarenai
1343dbe97a [index] Bump spoofed user-agent version 2020-01-15 21:36:12 +01:00
Nono
2175a4d08b [MozillaSecurityBridge] source has been modified (#1394)
adjustement following source change
2020-01-10 14:22:58 +01:00
Joe Digilio
ad661c4c91 [RedditBridge] Fix typo prevents bridge from working (#1383) 2019-12-05 18:07:50 +01:00
Grégory T
ba8c4623ed [DisplayAction] Fix function call on a member (add ->) (#1379) 2019-12-04 18:34:26 +01:00
logmanoriginal
ba43c87952 [RevolutBridge] Remove bridge
An official RSS feed is available at https://blog.revolut.com/rss/

Note that there is also an invisible "RSS" button next to the Facebook
and Twitter icons at the menu bar.

References #1321
2019-12-04 18:23:13 +01:00
Grégory T
595b87946d [TorrentGalaxyBridge] Add new bridge (#1378) 2019-12-02 20:31:50 +01:00
logmanoriginal
99d4e1a43d Bump version to dev.2019-12-01 2019-12-01 13:40:17 +01:00
logmanoriginal
477de4e2df Bump version to 2019-12-01 2019-12-01 13:34:09 +01:00
logmanoriginal
246470da18 [README] Update list of contributors 2019-12-01 13:33:03 +01:00
LogMANOriginal
df9f7eb778 [FacebookBridge] Fix permalink issue (#1358)
Facebook has changed their strategy regarding permalinks, which
now include lots of unnecessary target data. Fortunately it also
contains the unique story id which we can utilize as URI.
2019-12-01 13:24:11 +01:00
David
375831f516 [NineGagBridge] Add filter option for animated content (#1374) 2019-12-01 13:07:25 +01:00
Roliga
e518936be7 [SoundcloudBridge] Automatically acquire client_id (#1375)
Also some slight refactoring, as well as adding Roliga as maintainer.
2019-12-01 12:42:53 +01:00
Jacob Mansfield
583dfb4958 [TheWhiteboard] Create new bridge for The Whiteboard (#1327) 2019-12-01 11:30:16 +01:00
Jacob Mansfield
2de45b163e [FurAffinityUser] Add new bridge (#1326) 2019-12-01 11:27:41 +01:00
Shuto Yano
48b0164676 [InstagramBridge] Fix instagram GraphSidecar output and Video embedding (#1361)
* [InstagramBridge] Fix GraphSidecar output

Fix following issues which related to output of the GraphSidecar type posts.
- The GraphSidecar post's media wasn't outputted except for first picture when searching by hashtag or location
- Video didn't embedded
NOTE:
The function getInstagramStory() which was called when the post type is GraphSidecar didn't seem to work just as one intended.
Because the web request called in that function is just to get the media of single post, NOT to get the media of Story.
But I don't have any idea to solve #694, so it seems be better to rename these function and member variable properly.
2019-12-01 11:25:20 +01:00
somini
4b3c3c58d2 [DiárioDoAlentejo] Add new bridge (#1360) 2019-12-01 11:18:45 +01:00
somini
60768b4885 [DisplayAction] Don't return redirect error codes (#1359)
This might lead to redirect loops.

See
https://github.com/RSS-Bridge/rss-bridge/pull/1071#issuecomment-515632848

Cherry-picked from eb21d6f.
2019-12-01 11:13:57 +01:00
Eugene Molotov
02dd778124 [VkBridge] Save internal links in posts and get hashtags before making item (#1363) 2019-11-18 10:51:35 +01:00
Eugene Molotov
5b63121e92 [VkBridge] Change access token (#1357)
Previous access token was revoked
2019-11-09 18:51:16 +01:00
St. John Johnson
49019a843f [ComicsKingdomBridge] Add new bridge (#1353) 2019-11-09 18:50:08 +01:00
Roliga
d65714fa47 [HtmlFormat] Add syndication links (#1348)
Adds <link> elements for each additional output format in the <head> of
HTML format output to allow RSS readers to find the actual feeds
directly from the HTML page.
2019-11-09 18:43:21 +01:00
Joseph
8161829ad5 [OpenwhydBridge] Add new bridge (#1338)
Rename WhydBridge to OpenwhydBridge
2019-11-09 18:36:50 +01:00
Nemo
7f35fc9f6b [AppleAppStoreBridge] Add new bridge (#1316)
This bridge allows you to follow iOS/iPad application updates
directly over RSS. This allows you to get notified of an application
update even if you don't have a iDevice. This may be useful in cases
where you want to be notified of a competitor's application for eg.

I built this after I got tired of waiting for WhatsApp to push their
security release on iOS. It shows up on the AppStore 7 days later
2019-11-09 18:28:00 +01:00
logmanoriginal
3bc8c9468a phpcs: Always use long array syntax
Most of the code in RSS-Bridge uses the long array syntax.
This commit adds a check to enforce using this syntax over
the short array syntax.

All failures have been fixed.
2019-11-01 18:06:55 +01:00
logmanoriginal
1df3598a74 [Dockerfile] Drop minimum security level back to TLS 1.0
Debian increased the minimum security level for OpenSSL from TLS 1.0
to TLS 1.2 [1] which also affects the Debian-based PHP image for Docker.

This change can break some bridges which have to connect to servers with
lower security level. Since all browsers still connect to these servers,
so should RSS-Bridge.

Note that according to [2] Mozilla, Firefox, Microsoft, Google and Apple
plan to increase the minimum security level to TLS 1.2 around March 2020.
At this time RSS-Bridge should follow the browser changes.

This commit updates the Dockerfile to automatically drop the minimum
security level back to TLS 1.0.

Based on the solution provided by @theScrabi in #1318

[1] https://wiki.debian.org/ContinuousIntegration/TriagingTips/openssl-1.1.1
[2] 553fc8e61f/debian/libssl1.1.NEWS
2019-11-01 17:12:45 +01:00
logmanoriginal
5f64fe2516 [BridgeAbstract] Fix broken assignment of defaultValue
setInputs() currently looks if the global array defines a 'value'
for a given parameter, but that isn't supported by the API. It
needs to be 'defaultValue'.
2019-11-01 15:29:16 +01:00
logmanoriginal
50eee7e7b3 [KununuBridge] Add feed item limit
This bridge currently takes a very long time to process
all news items on the page, when in many cases only one
or two had been added since the last check.

This commit adds a new parameter 'limit', which defines
the maximum number of items to add to the feed. This is
an optional paramter that defaults to 3.
2019-11-01 15:27:35 +01:00
logmanoriginal
c0df9815c7 [DesoutterBridge] Add feed item limit
This bridge currently takes a very long time to process
all news items on the page, when in many cases only one
or two had been added since the last check.

This commit adds a new parameter 'limit', which defines
the maximum number of items to add to the feed. This is
an optional paramter that defaults to 3.
2019-11-01 15:07:25 +01:00
Léo Maradan
46d5895d1d [RedditBridge] Add new bridge (#1213) 2019-11-01 13:54:03 +01:00
Anchit Bajaj
7c16aaf303 [VarietyBridge] Add new bridge (#1307) 2019-11-01 13:48:09 +01:00
LogMANOriginal
cdc1d9c9ba action: Add action to check bridge connectivity (#1147)
* action: Add action to check bridge connectivity

It is currently not simply possible to check if the remote
server for a bridge is reachable or not, which means some
of the bridges might no longer work because the server is
no longer on the internet.

In order to find those bridges we can either check each
bridge individually (which takes a lot of effort), or use
an automated script to do this for us.

If a server is no longer reachable it could mean that it is
temporarily unavailable, or shutdown permanently. The results
of this script will at least help identifying such servers.

* [Connectivity] Use Bootstrap container to properly display contents

* [Connectivity] Limit connectivity checks to debug mode

Connectivity checks take a long time to execute and can require a lot
of bandwidth. Therefore, administrators should be able to determine
when and who is able to utilize this action. The best way to prevent
regular users from accessing this action is by making it available in
debug mode only (public servers should never run in debug mode anyway).

* [Connectivity] Split implemenation into multiple files

* [Connectivity] Make web page responsive to user input

* [Connectivity] Make status message sticky

* [Connectivity] Add icon to the status message

* [contents] Add the ability for getContents to return header information

* [Connectivity] Add header information to the reply Json data

* [Connectivity] Add new status (blue) for redirected sites

Also adds titles to status icons (Successful, Redirected, Inactive, Failed)

* [Connectivity] Fix show doesn't work for inactive bridges

* [Connectivity] Fix typo

* [Connectivity] Catch errors in promise chains

* [Connectivity] Allow search by status and update dynamically

* [Connectivity] Add a progress bar

* [Connectivity] Use bridge factory

* [Connectivity] Import Bootstrap v4.3.1 CSS
2019-10-31 22:02:38 +01:00
LogMANOriginal
6bc83310b9 core: Add info button for input fields with title (#1173)
The current solution for titles on input boxes is not obvious to the
user as support varies between bridges. This commit adds an button to
all input boxes with titles in order to make it clear to the user that
additional information is available.
2019-10-31 21:09:44 +01:00
Roliga
c8d5c85c76 formats: Add getMimeType() function (#1299)
Allows getting the expected MIME type of the format's output. A
corresponding MIME_TYPE constant is also defined in FormatAbstract for
the format implementations to overwrite.
2019-10-31 19:00:12 +01:00
somini
d1e4bd7285 [YahtzeeDevDiary] Add new bridge (#1297) 2019-10-31 18:55:08 +01:00
LogMANOriginal
1022b5fdf9 core: Add an option to suppress error reporting (#1179)
Error reporting currently takes place for each error. This can result
in many error messages if a server has connectivity issues (i.e. when
it re-connects to the internet every 24 hours).

This commit adds a new option to the configuration file to define the
number of error reports to suppress before returning an error message
to the user.

Error reports are cached and therefore automatically purged after 24
hours. A successful bridge request does **not** clear the error count
as sporadic issues can be the result of actual problems on the server.

The implementation currently makes no assumption on the type of error,
which means it also suppresses bridge errors in debug mode. The default
value is, however, set to 1 which means all errors are reported.

References #994
2019-10-31 18:49:45 +01:00
LogMANOriginal
e8536ac1b2 core: Add an option to return errors in different formats (#1071)
Bridge errors are currently included as part of the feed to
notify users about erroneous bridges (before that, bridges
silently failed).

This solution, however, can produce a high load of error
messages if servers are down (see #994 for more details).

Admins may also not want to include error messages in feeds
in order to keep those kind of problems away from users or
simply to silently fail by choice.

This commit adds a new configuration section "error" with
one option "output" which can be set to following values:

"feed": To include error messages in the feed (default)
"http": To return a HTTP header for each error
"none": To disable error reporting

Note that errors are always logged to 'error.log' independent
of the settings above.

Closes #1066
2019-10-31 18:40:51 +01:00
Lyra
a0afe36d56 [DownDetectorBridge] Add per-website status fetch. Note that this only fetches the last downtime, as this is the only thing that the API provides. Moreover, the site uses a different ID for every company for every country, resulting in a very large array 2019-10-29 23:14:51 +01:00
Lyra
0b80f9d61c [DownDetectorBridge] Add bridge for DownDetector, and all local variants. Fixes
#1339.
2019-10-29 19:11:28 +01:00
somini
424075981f [EsquerdaNetBridge] Add new Bridge (#1296) 2019-10-29 18:58:12 +01:00
logmanoriginal
c334df91ec composer: Add all details to the composer file
The composer file currently lacks a lot of details, especially the
"name" and "description", but also "require-dev" and "suggest" info.

This commit adds many more details to the composer file and updates
composer.lock for this repository. Technically the project is ready
to be shipped as composer package.
2019-10-28 20:01:19 +01:00
Dominik Thiemermann
f2346fb33e [RevolutBridge] Add new bridge (#1321)
* [RevolutBridge] Add new bridge
2019-10-28 19:49:01 +01:00
Matt DeMoss
8a21fd1476 [BloombergBridge] Remove after site redesign and paywall. (#1238) 2019-10-28 19:27:56 +01:00
somini
2ac44172ac Facebook: Clarify Facebook bridges (#1221)
* Clarify Facebook bridges status

Distinguish between both Facebook bridges by their title.
This preserves all existing URLs.

* Update all URLs to secure HTTPS versions.
* Configure author name abbreviation
* Improve feed names

Use the correct feed name on each bridge.
Make sure the feed names don't repeat the "Facebook" name.
2019-10-28 19:01:04 +01:00
Roliga
4c78721f03 [ParameterValidator] Ensure context has all user provided parameters (#1211)
* [ParameterValidator] Ensure context has all fields

Previously if a bridge had a set of parameters like:

const PARAMETERS = array(
    'ContextA' => array(
        'Param1' => array(
            'name' => 'Param1',
            'required' => true
        )
    ),
    'ContextB' => array(
        'Param1' => array(
            'name' => 'Param1',
            'required' => true
        ),
        'Param2' => array(
            'name' => 'Param2',
            'required' => true
        )
    )
)

and a query specifying both Param1 and Param2 was provided a 'Mixed
context parameters' error would be returned. This change ensures
ContextA in the above example would not be considered a relevant context.
2019-10-28 17:50:55 +01:00
Christian Archer
04be85996d [BastaBridge] Fix PHP 7.4 crash (#1323)
* Inline the function
2019-10-24 21:57:14 +02:00
Joseph
59be6bded2 [GoogleSearchBridge] Replace 'div[id=ires]' with 'div[id=res]' (#1329) 2019-10-16 21:44:41 +02:00
Joseph
46873e14fe [GoogleSearchBridge] Use getURI() to build URLs (#1330)
* [GoogleSearchBridge] Use getURI() to build URLs
2019-10-16 21:44:28 +02:00
Joseph
0f01cc97a4 [StoriesIGBridge] Add timestamp to feed items (#1331) 2019-10-16 21:44:01 +02:00
Joseph
a70e00a76d [SuperbWallpapersBridge] Delete bridge (#1336) 2019-10-16 21:43:38 +02:00
Joseph
f0260c62c3 [StoriesIGBridge] Use getName() to create custom feed titles (#1332)
* [StoriesIGBridge] Use getName()
2019-10-16 21:41:47 +02:00
Roliga
fc5a1526ca [BandcampBridge] Add band and album feeds (#1317)
* [BandcampBridge] Add band and artist feeds

This can return a limited number of the most recent releases by a band,
or a single release/album. Each release may be given a unique article ID
depending on its track list with the "Releases, new one when track track
changes" option, which should make them show up as new articles when
tracks are added or removed. Releases may also be split up to individual
articles for each track with the "Individual tracks" option.

This uses and undocumented API from the Bandcamp Android app. It's much
faster than loading and parsing the website HTML, and seems to fail less
often with more relaxed rate limits. It's still far from perfect in that
regard though.

The "Individual tracks" option generates requests for each individual
track so that can quickly run into rate limits.

The "Individual tracks" option also has a quirk where tracks released
under e.g. a music label will have their artist set to the label instead
of the actual artist of the track. This is a limitation of the API.
2019-10-16 21:37:25 +02:00
Joseph
4c0e234479 [Bridges] Use HTTPS (#1337)
* [Rule34pahealBridge] Use HTTPS
* [KonachanBridge] Use HTTPS
* [Rule34Bridge] Use HTTPS
* [SafebooruBridge] Use HTTPS
* [TbibBridge] Use HTTPS
* [XbooruBridge] Use HTTPS
* [ScmbBridge] Use HTTPS
* [ReporterreBridge] Use HTTPS
* [BastaBridge] Use HTTPS
* [NiceMatinBridge] Use HTTPS
* [ScoopItBridge] Use HTTPS
* [TheCodingLoveBridge] Use HTTPS
* [Shimmie2Bridge] Use HTTPS
* [HDWallpapersBridge] Use HTTPS
* [GiphyBridge] Use HTTPS
* [PickyWallpapersBridge] Use HTTPS
* [ParuVenduImmoBridge] Use HTTPS
* [ElsevierBridge] Use HTTPS
* [CastorusBridge] Use HTTPS
* [CollegeDeFranceBridge] Use HTTPS
* [MangareaderBridge] Use HTTPS
2019-10-16 21:34:28 +02:00
somini
0eab63d728 Update Facebook URL detection (#1334)
* add detectParameters to FacebookBridge.php
2019-10-16 21:32:29 +02:00
floviolleau
b0884e9158 [VieDeMerdeBridge] Add new bridge for quotes from Vie de Merde (#1313)
* Add new bridge for quotes from Vie de Merde
2019-10-03 22:36:08 +02:00
Nicolas Delsaux
b4581418d4 [PlantUMLBridge] Added bridge for PlantUML. Fixes #1191
* Fixes #1191 by implementing the RSS feed of PlantUML releases
2019-10-03 22:30:22 +02:00
sysadminstory
af1566f40d [ZoneTelechargementBridge] URL and name change (#1302)
Annuaire Telechargement has change name again to go back to Annuaire
Telechargement. Fixes #1279
2019-10-03 22:27:10 +02:00
sysadminstory
529e0d0cca [ExtremeDownloadBridge] Update Website URL (#1303)
Website URL was changed.
2019-10-03 22:25:56 +02:00
Paróczai Olivér
a3532804ac [Readme] Small grammar fixes (#1312) 2019-10-03 22:25:05 +02:00
Anchit Bajaj
8c19146d29 [ListverseBridge - add new bridge (#1305) 2019-10-03 22:24:14 +02:00
Anchit Bajaj
2a3d5865ad [FreeCodeCampBridge] - rss feed for FreeCodeCamp (#1311) 2019-10-03 22:23:14 +02:00
Lyra
4d36c9dc30 Merge branch 'master' of github.com:RSS-Bridge/rss-bridge 2019-10-03 22:14:33 +02:00
Lyra
a2e47a88c3 [InstagramBridge] Add option to get direct links 2019-10-03 22:14:21 +02:00
Anchit Bajaj
b09f50853f [ViceBridge] - RSS feed for Vice Publications. (#1310)
* [ViceBridge] - RSS feed for Vice Publications.
2019-10-03 22:02:30 +02:00
lukasklinger
9b5bf565b3 [N26Bridge] Updated bridge to reflect changes on N26 blog (#1295)
N26 made some changes to their blog, this commit fixes the N26Bridge
2019-10-03 21:58:57 +02:00
Lyra
5cc956367f [core] Fix travis 2019-10-03 21:46:49 +02:00
Lyra
548e28249b [ThePirateBayBridge] Remove nested function 2019-10-03 21:46:24 +02:00
Lyra
684c69b0cd [Releases3DSBridge] Remove nested functions 2019-10-03 21:46:09 +02:00
Lyra
3dae4e0801 [JapanExpoBridge] Remove nested function 2019-10-03 21:45:51 +02:00
Lyra
4622d9be1e [ReadComicsBridge] Deleted bridge since website no longer exists 2019-10-03 21:41:22 +02:00
Nicolas Delsaux
76183dcd44 [GQMagazineBridge] Fix article body detection again (Fixes #1280) 2019-10-03 21:26:41 +02:00
Eugene Molotov
50b234d893 [VkBridge] Photo and timestamp fixes (#1287)
* [VkBridge] Correct parsing of photos, fix timestamp for old posts
2019-09-16 21:30:27 +02:00
Eugene Molotov
af48f36fd2 [VkBridge] Switch maintainer (#1288) 2019-09-16 21:29:45 +02:00
Eugene Molotov
7f6ca23e8f [PikabuBridge] Preserve links (#1286)
* [PikabuBridge] Preserve links
2019-09-16 21:28:41 +02:00
oratosquilla-oratoria
1daef22a3d [NFLRUSBridge] Add new bridge (#1285)
* [NFLRUSBridge] Add new bridge
2019-09-16 21:27:01 +02:00
killruana
c694810d9a [MediapartBridge] Fix article parsing
* Only process article item, fix issue #1292
2019-09-16 21:26:19 +02:00
ORelio
f12f6a2dba [DarkReading] Add DarkReading Bridge (#1289) 2019-09-16 21:25:28 +02:00
Lyra
b1be45df6c [Configuration] Bump version to dev.2019-09-12 2019-09-12 17:09:30 +02:00
Lyra
b4f393a5cc [Configuration] Bump version to 2019-07-06 2019-09-12 17:08:15 +02:00
Lyra
29126ebe29 [README] Update list of contributors 2019-09-12 17:07:04 +02:00
triatic
50c971d545 [TwitterBridge] Enable cookies with curl (#1245)
* [TwitterBridge] Enable cookies with curl

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

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

* [PatreonBridge] Add UID to articles

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

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

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

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

View File

@@ -3,9 +3,20 @@ FROM php:7-apache
ENV APACHE_DOCUMENT_ROOT=/app
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
&& apt-get --yes update && apt-get --yes install libxml2-dev \
&& apt-get --yes update && apt-get --yes install libxml2-dev zlib1g-dev libmemcached-dev \
&& docker-php-ext-install -j$(nproc) simplexml \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
&& sed -ri -e 's/(MinProtocol\s*=\s*)TLSv1\.2/\1None/' /etc/ssl/openssl.cnf \
&& sed -ri -e 's/(CipherString\s*=\s*DEFAULT)@SECLEVEL=2/\1/' /etc/ssl/openssl.cnf
RUN curl https://codeload.github.com/php-memcached-dev/php-memcached/tar.gz/v3.1.5 --output /tmp/php-memcached.tar.gz \
&& mkdir -p /usr/src/php/ext \
&& tar xzvf /tmp/php-memcached.tar.gz -C /usr/src/php/ext \
&& mv /usr/src/php/ext/php-memcached-3.1.5 /usr/src/php/ext/memcached \
&& cd /usr/src/php/ext/memcached \
&& docker-php-ext-configure /usr/src/php/ext/memcached --disable-memcached-sasl \
&& docker-php-ext-install /usr/src/php/ext/memcached \
&& rm -rf /usr/src/php/ext/memcached
COPY --chown=www-data:www-data ./ /app/

View File

@@ -2,7 +2,7 @@
===
[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?logo=debian&label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-blue.svg)](https://www.gnu.org/software/guix/packages/R/) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg?logo=docker)](https://hub.docker.com/r/rssbridge/rss-bridge/)
RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one. It can be used on webservers or as stand alone application in CLI mode.
RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one. It can be used on webservers or as a stand-alone application in CLI mode.
**Important**: RSS-Bridge is __not__ a feed reader or feed aggregator, but a tool to generate feeds that are consumed by feed readers and feed aggregators. Find a list of feed aggregators on [Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators).
@@ -76,7 +76,7 @@ RSS-Bridge allows you to take full control over which bridges are displayed to t
Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting)
**Notice**: By default RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge!
**Notice**: By default, RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge!
Deploy
===
@@ -109,8 +109,8 @@ We are RSS-Bridge community, a group of developers continuing the project initia
Use this script to generate the list automatically (using the GitHub API):
https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
-->
* [16mhz](https://github.com/16mhz)
* [86423355844265459587182778](https://github.com/86423355844265459587182778)
* [adamchainz](https://github.com/adamchainz)
* [Ahiles3005](https://github.com/Ahiles3005)
* [Albirew](https://github.com/Albirew)
@@ -118,6 +118,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [alex73](https://github.com/alex73)
* [alexAubin](https://github.com/alexAubin)
* [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel)
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
@@ -125,38 +126,48 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [azdkj532](https://github.com/azdkj532)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
* [Binnette](https://github.com/Binnette)
* [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel)
* [ckiw](https://github.com/ckiw)
* [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting)
* [couraudt](https://github.com/couraudt)
* [cyberjacob](https://github.com/cyberjacob)
* [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei)
* [dawidsowa](https://github.com/dawidsowa)
* [disk0x](https://github.com/disk0x)
* [DJCrashdummy](https://github.com/DJCrashdummy)
* [Djuuu](https://github.com/Djuuu)
* [DnAp](https://github.com/DnAp)
* [dominik-th](https://github.com/dominik-th)
* [Draeli](https://github.com/Draeli)
* [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [floviolleau](https://github.com/floviolleau)
* [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos)
* [gloony](https://github.com/gloony)
* [GregThib](https://github.com/GregThib)
* [griffaurel](https://github.com/griffaurel)
* [Grummfy](https://github.com/Grummfy)
* [hunhejj](https://github.com/hunhejj)
* [husim0](https://github.com/husim0)
* [IceWreck](https://github.com/IceWreck)
* [j0k3r](https://github.com/j0k3r)
* [JackNUMBER](https://github.com/JackNUMBER)
* [jdesgats](https://github.com/jdesgats)
* [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand)
* [Jocker666z](https://github.com/Jocker666z)
* [johnnygroovy](https://github.com/johnnygroovy)
* [johnpc](https://github.com/johnpc)
* [killruana](https://github.com/killruana)
* [klimplant](https://github.com/klimplant)
* [kranack](https://github.com/kranack)
@@ -166,9 +177,11 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry)
* [Leomaradan](https://github.com/Leomaradan)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos)
* [lukasklinger](https://github.com/lukasklinger)
* [m0zes](https://github.com/m0zes)
* [matthewseal](https://github.com/matthewseal)
* [mcbyte-it](https://github.com/mcbyte-it)
@@ -177,13 +190,14 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [metaMMA](https://github.com/metaMMA)
* [mitsukarenai](https://github.com/mitsukarenai)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro)
* [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [Nono-m0le](https://github.com/Nono-m0le)
* [ObsidianWitch](https://github.com/ObsidianWitch)
* [OliverParoczai](https://github.com/OliverParoczai)
* [oratosquilla-oratoria](https://github.com/oratosquilla-oratoria)
* [ORelio](https://github.com/ORelio)
* [PaulVayssiere](https://github.com/PaulVayssiere)
* [pellaeon](https://github.com/pellaeon)
@@ -199,15 +213,21 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [rogerdc](https://github.com/rogerdc)
* [Roliga](https://github.com/Roliga)
* [sebsauvage](https://github.com/sebsauvage)
* [shutosg](https://github.com/shutosg)
* [somini](https://github.com/somini)
* [squeek502](https://github.com/squeek502)
* [stjohnjohnson](https://github.com/stjohnjohnson)
* [Strubbl](https://github.com/Strubbl)
* [sublimz](https://github.com/sublimz)
* [sunchaserinfo](https://github.com/sunchaserinfo)
* [sysadminstory](https://github.com/sysadminstory)
* [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene)
* [tgkenney](https://github.com/tgkenney)
* [thefranke](https://github.com/thefranke)
* [ThePadawan](https://github.com/ThePadawan)
* [TheRadialActive](https://github.com/TheRadialActive)
* [TitiTestScalingo](https://github.com/TitiTestScalingo)
* [triatic](https://github.com/triatic)
* [VerifiedJoseph](https://github.com/VerifiedJoseph)
* [WalterBarrett](https://github.com/WalterBarrett)

View File

@@ -0,0 +1,136 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/**
* Checks if the website for a given bridge is reachable.
*
* **Remarks**
* - This action is only available in debug mode.
* - Returns the bridge status as Json-formatted string.
* - Returns an error if the bridge is not whitelisted.
* - Returns a responsive web page that automatically checks all whitelisted
* bridges (using JavaScript) if no bridge is specified.
*/
class ConnectivityAction extends ActionAbstract {
public function execute() {
if(!Debug::isEnabled()) {
returnError('This action is only available in debug mode!');
}
if(!isset($this->userData['bridge'])) {
$this->returnEntryPage();
return;
}
$bridgeName = $this->userData['bridge'];
$this->reportBridgeConnectivity($bridgeName);
}
/**
* Generates a report about the bridge connectivity status and sends it back
* to the user.
*
* The report is generated as Json-formatted string in the format
* {
* "bridge": "<bridge-name>",
* "successful": true/false
* }
*
* @param string $bridgeName Name of the bridge to generate the report for
* @return void
*/
private function reportBridgeConnectivity($bridgeName) {
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
if(!$bridgeFac->isWhitelisted($bridgeName)) {
header('Content-Type: text/html');
returnServerError('Bridge is not whitelisted!');
}
header('Content-Type: text/json');
$retVal = array(
'bridge' => $bridgeName,
'successful' => false,
'http_code' => 200,
);
$bridge = $bridgeFac->create($bridgeName);
if($bridge === false) {
echo json_encode($retVal);
return;
}
$curl_opts = array(
CURLOPT_CONNECTTIMEOUT => 5
);
try {
$reply = getContents($bridge::URI, array(), $curl_opts, true);
if($reply) {
$retVal['successful'] = true;
if (isset($reply['header'])) {
if (strpos($reply['header'], 'HTTP/1.1 301 Moved Permanently') !== false) {
$retVal['http_code'] = 301;
}
}
}
} catch(Exception $e) {
$retVal['successful'] = false;
}
echo json_encode($retVal);
}
private function returnEntryPage() {
echo <<<EOD
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="static/bootstrap.min.css">
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
crossorigin="anonymous">
<link rel="stylesheet" href="static/connectivity.css">
<script src="static/connectivity.js" type="text/javascript"></script>
</head>
<body>
<div id="main-content" class="container">
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div id="status-message" class="sticky-top alert alert-primary alert-dismissible fade show" role="alert">
<i id="status-icon" class="fas fa-sync"></i>
<span>...</span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="stopConnectivityChecks()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<input type="text" class="form-control" id="search" onkeyup="search()" placeholder="Search for bridge..">
</div>
</body>
</html>
EOD;
}
}

View File

@@ -12,6 +12,15 @@
*/
class DisplayAction extends ActionAbstract {
private function get_return_code($error) {
$returnCode = $error->getCode();
if ($returnCode === 301 || $returnCode === 302) {
# Don't pass redirect codes to the exterior
$returnCode = 508;
}
return $returnCode;
}
public function execute() {
$bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
@@ -146,6 +155,8 @@ class DisplayAction extends ActionAbstract {
} catch(Error $e) {
error_log($e);
if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
if(Configuration::getConfig('error', 'output') === 'feed') {
$item = new \FeedItem();
// Create "new" error message every 24 hours
@@ -178,9 +189,16 @@ class DisplayAction extends ActionAbstract {
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} elseif(Configuration::getConfig('error', 'output') === 'http') {
header('Content-Type: text/html', true, $this->get_return_code($e));
die(buildTransformException($e, $bridge));
}
}
} catch(Exception $e) {
error_log($e);
if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
if(Configuration::getConfig('error', 'output') === 'feed') {
$item = new \FeedItem();
// Create "new" error message every 24 hours
@@ -203,6 +221,11 @@ class DisplayAction extends ActionAbstract {
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} elseif(Configuration::getConfig('error', 'output') === 'http') {
header('Content-Type: text/html', true, $this->get_return_code($e));
die(buildTransformException($e, $bridge));
}
}
}
// Store data in cache

View File

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

View File

@@ -134,11 +134,11 @@ EOT;
// data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
// data-asin-currency-code="USD" data-substitute-count="-1" ... />
if ($asinData) {
return [
return array(
'price' => $asinData->getAttribute('data-asin-price'),
'currency' => $asinData->getAttribute('data-asin-currency-code'),
'shipping' => $asinData->getAttribute('data-asin-shipping')
];
);
}
return false;
@@ -150,11 +150,11 @@ EOT;
preg_match('/^\s*([A-Z]{3}|£|\$)\s?([\d.,]+)\s*$/', $priceDiv->plaintext, $matches);
if (count($matches) === 3) {
return [
return array(
'price' => $matches[2],
'currency' => $matches[1],
'shipping' => '0'
];
);
}
return false;

View File

@@ -0,0 +1,149 @@
<?php
class AppleAppStoreBridge extends BridgeAbstract {
const MAINTAINER = 'captn3m0';
const NAME = 'Apple App Store';
const URI = 'https://apps.apple.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns version updates for a specific application';
const PARAMETERS = array(array(
'id' => array(
'name' => 'Application ID',
'required' => true,
'exampleValue' => '310633997'
),
'p' => array(
'name' => 'Platform',
'type' => 'list',
'values' => array(
'iPad' => 'ipad',
'iPhone' => 'iphone',
'Mac' => 'mac',
// The following 2 are present in responses
// but not yet tested
'Web' => 'web',
'Apple TV' => 'appletv',
),
'defaultValue' => 'iphone',
),
'country' => array(
'name' => 'Store Country',
'type' => 'list',
'values' => array(
'US' => 'US',
'India' => 'IN',
'Canada' => 'CA'
),
'defaultValue' => 'US',
),
));
const PLATFORM_MAPPING = array(
'iphone' => 'ios',
'ipad' => 'ios',
);
private function makeHtmlUrl($id, $country){
return 'https://apps.apple.com/' . $country . '/app/id' . $id;
}
private function makeJsonUrl($id, $platform, $country){
return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory";
}
public function getName(){
if (isset($this->name)) {
return $this->name . ' - AppStore Updates';
}
return parent::getName();
}
/**
* In case of some platforms, the data is present in the initial response
*/
private function getDataFromShoebox($id, $platform, $country){
$uri = $this->makeHtmlUrl($id, $country);
$html = getSimpleHTMLDOMCached($uri, 3600);
$script = $html->find('script[id="shoebox-ember-data-store"]', 0);
$json = json_decode($script->innertext, true);
return $json['data'];
}
private function getJWTToken($id, $platform, $country){
$uri = $this->makeHtmlUrl($id, $country);
$html = getSimpleHTMLDOMCached($uri, 3600);
$meta = $html->find('meta[name="web-experience-app/config/environment"]', 0);
$json = urldecode($meta->content);
$json = json_decode($json);
return $json->MEDIA_API->token;
}
private function getAppData($id, $platform, $country, $token){
$uri = $this->makeJsonUrl($id, $platform, $country);
$headers = array(
"Authorization: Bearer $token",
);
$json = json_decode(getContents($uri, $headers), true);
return $json['data'][0];
}
/**
* Parses the version history from the data received
* @return array list of versions with details on each element
*/
private function getVersionHistory($data, $platform){
switch($platform) {
case 'mac':
return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory'];
default:
$os = self::PLATFORM_MAPPING[$platform];
return $data['attributes']['platformAttributes'][$os]['versionHistory'];
}
}
public function collectData() {
$id = $this->getInput('id');
$country = $this->getInput('country');
$platform = $this->getInput('p');
switch ($platform) {
case 'mac':
$data = $this->getDataFromShoebox($id, $platform, $country);
break;
default:
$token = $this->getJWTToken($id, $platform, $country);
$data = $this->getAppData($id, $platform, $country, $token);
}
$versionHistory = $this->getVersionHistory($data, $platform);
$name = $this->name = $data['attributes']['name'];
$author = $data['attributes']['artistName'];
foreach ($versionHistory as $row) {
$item = array();
$item['content'] = nl2br($row['releaseNotes']);
$item['title'] = $name . ' - ' . $row['versionDisplay'];
$item['timestamp'] = $row['releaseDate'];
$item['author'] = $author;
$item['uri'] = $this->makeHtmlUrl($id, $country);
$this->items[] = $item;
}
}
}

View File

@@ -5,19 +5,19 @@ class AppleMusicBridge extends BridgeAbstract {
const URI = 'https://www.apple.com';
const DESCRIPTION = 'Fetches the latest releases from an artist';
const MAINTAINER = 'Limero';
const PARAMETERS = [[
'url' => [
const PARAMETERS = array(array(
'url' => array(
'name' => 'Artist URL',
'exampleValue' => 'https://itunes.apple.com/us/artist/dunderpatrullen/329796274',
'required' => true,
],
'imgSize' => [
),
'imgSize' => array(
'name' => 'Image size for thumbnails (in px)',
'type' => 'number',
'defaultValue' => 512,
'required' => true,
]
]];
)
));
const CACHE_TIMEOUT = 21600; // 6 hours
public function collectData() {
@@ -36,12 +36,12 @@ class AppleMusicBridge extends BridgeAbstract {
// Loop through each object
foreach ($json->included as $obj) {
if ($obj->type === 'lockup/album') {
$this->items[] = [
$this->items[] = array(
'title' => $obj->attributes->artistName . ' - ' . $obj->attributes->name,
'uri' => $obj->attributes->url,
'timestamp' => $obj->attributes->releaseDate,
'enclosures' => $obj->relationships->artwork->data->id,
];
);
} elseif ($obj->type === 'image') {
$images[$obj->id] = $obj->attributes->url;
}
@@ -49,9 +49,9 @@ class AppleMusicBridge extends BridgeAbstract {
// Add the images to each item
foreach ($this->items as &$item) {
$item['enclosures'] = [
$item['enclosures'] = array(
str_replace('{w}x{h}bb.{f}', $imgSize . 'x0w.jpg', $images[$item['enclosures']]),
];
);
}
// Sort the order to put the latest albums first

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,27 +1,81 @@
<?php
class BandcampBridge extends BridgeAbstract {
const MAINTAINER = 'sebsauvage';
const NAME = 'Bandcamp Tag';
const MAINTAINER = 'sebsauvage, Roliga';
const NAME = 'Bandcamp Bridge';
const URI = 'https://bandcamp.com/';
const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'New bandcamp release by tag';
const PARAMETERS = array( array(
const DESCRIPTION = 'New bandcamp releases by tag, band or album';
const PARAMETERS = array(
'By tag' => array(
'tag' => array(
'name' => 'tag',
'type' => 'text',
'required' => true
)
));
),
'By band' => array(
'band' => array(
'name' => 'band',
'type' => 'text',
'title' => 'Band name as seen in the band page URL',
'required' => true
),
'type' => array(
'name' => 'Articles are',
'type' => 'list',
'values' => array(
'Releases' => 'releases',
'Releases, new one when track list changes' => 'changes',
'Individual tracks' => 'tracks'
),
'defaultValue' => 'changes'
),
'limit' => array(
'name' => 'limit',
'type' => 'number',
'title' => 'Number of releases to return',
'defaultValue' => 5
)
),
'By album' => array(
'band' => array(
'name' => 'band',
'type' => 'text',
'title' => 'Band name as seen in the album page URL',
'required' => true
),
'album' => array(
'name' => 'album',
'type' => 'text',
'title' => 'Album name as seen in the album page URL',
'required' => true
),
'type' => array(
'name' => 'Articles are',
'type' => 'list',
'values' => array(
'Releases' => 'releases',
'Releases, new one when track list changes' => 'changes',
'Individual tracks' => 'tracks'
),
'defaultValue' => 'tracks'
)
)
);
const IMGURI = 'https://f4.bcbits.com/';
const IMGSIZE_300PX = 23;
const IMGSIZE_700PX = 16;
private $feedName;
public function getIcon() {
return 'https://s4.bcbits.com/img/bc_favicon.ico';
}
public function collectData(){
switch($this->queriedContext) {
case 'By tag':
$url = self::URI . 'api/hub/1/dig_deeper';
$data = $this->buildRequestJson();
$header = array(
@@ -66,6 +120,141 @@ class BandcampBridge extends BridgeAbstract {
$item['enclosures'] = array($img);
$this->items[] = $item;
}
break;
case 'By band':
case 'By album':
$html = getSimpleHTMLDOMCached($this->getURI(), 86400);
$titleElement = $html->find('head meta[name=title]', 0)
or returnServerError('Unable to find title on: ' . $this->getURI());
$this->feedName = $titleElement->content;
$regex = '/band_id=(\d+)/';
if(preg_match($regex, $html, $matches) == false)
returnServerError('Unable to find band ID on: ' . $this->getURI());
$band_id = $matches[1];
$tralbums = array();
switch($this->queriedContext) {
case 'By band':
$query_data = array(
'band_id' => $band_id
);
$band_data = $this->apiGet('mobile/22/band_details', $query_data);
$num_albums = min(count($band_data->discography), $this->getInput('limit'));
for($i = 0; $i < $num_albums; $i++) {
$album_basic_data = $band_data->discography[$i];
// 'a' or 't' for albums and individual tracks respectively
$tralbum_type = substr($album_basic_data->item_type, 0, 1);
$query_data = array(
'band_id' => $band_id,
'tralbum_type' => $tralbum_type,
'tralbum_id' => $album_basic_data->item_id
);
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
}
break;
case 'By album':
$regex = '/album=(\d+)/';
if(preg_match($regex, $html, $matches) == false)
returnServerError('Unable to find album ID on: ' . $this->getURI());
$album_id = $matches[1];
$query_data = array(
'band_id' => $band_id,
'tralbum_type' => 'a',
'tralbum_id' => $album_id
);
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
break;
}
foreach ($tralbums as $tralbum_data) {
if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') {
foreach ($tralbum_data->tracks as $track) {
$query_data = array(
'band_id' => $band_id,
'tralbum_type' => 't',
'tralbum_id' => $track->track_id
);
$track_data = $this->apiGet('mobile/22/tralbum_details', $query_data);
$this->items[] = $this->buildTralbumItem($track_data);
}
} else {
$this->items[] = $this->buildTralbumItem($tralbum_data);
}
}
break;
}
}
private function buildTralbumItem($tralbum_data){
$band_data = $tralbum_data->band;
// Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER)
// Format artist/author like: ARTIST (OPTIONAL RELEASER)
//
// If the album/track is released under a label/a band other than the artist
// themselves, append that releaser name to the title and artist/author.
//
// This sadly doesn't always work right for individual tracks as the artist
// of the track is always set to the releaser.
$artist = $tralbum_data->tralbum_artist;
$full_title = $artist . ' - ' . $tralbum_data->title;
$full_artist = $artist;
if (isset($tralbum_data->label)) {
$full_title .= ' (' . $tralbum_data->label . ')';
$full_artist .= ' (' . $tralbum_data->label . ')';
} elseif ($band_data->name !== $artist) {
$full_title .= ' (' . $band_data->name . ')';
$full_artist .= ' (' . $band_data->name . ')';
}
$small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX);
$img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX);
$item = array(
'uri' => $tralbum_data->bandcamp_url,
'author' => $full_artist,
'title' => $full_title,
'enclosures' => array($img),
'timestamp' => $tralbum_data->release_date
);
$item['categories'] = array();
foreach ($tralbum_data->tags as $tag) {
$item['categories'][] = $tag->norm_name;
}
// Give articles a unique UID depending on its track list
// Releases should then show up as new articles when tracks are added
if ($this->getInput('type') === 'changes') {
$item['uid'] = "bandcamp/$band_data->band_id/$tralbum_data->id/";
foreach ($tralbum_data->tracks as $track) {
$item['uid'] .= $track->track_id;
}
}
$item['content'] = "<img src='$small_img' /><br/>$full_title<br/>";
if ($tralbum_data->type === 'a') {
$item['content'] .= '<ol>';
foreach ($tralbum_data->tracks as $track) {
$item['content'] .= "<li>$track->title</li>";
}
$item['content'] .= '</ol>';
}
if (!empty($tralbum_data->about)) {
$item['content'] .= '<p>'
. nl2br($tralbum_data->about)
. '</p>';
}
return $item;
}
private function buildRequestJson(){
@@ -81,11 +270,94 @@ class BandcampBridge extends BridgeAbstract {
return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg';
}
private function apiGet($endpoint, $query_data) {
$url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data);
$data = json_decode(getContents($url))
or returnServerError('API request to "' . $url . '" failed.');
return $data;
}
public function getURI(){
switch($this->queriedContext) {
case 'By tag':
if(!is_null($this->getInput('tag'))) {
return self::URI
. 'tag/'
. urlencode($this->getInput('tag'))
. '?sort_field=date';
}
break;
case 'By band':
if(!is_null($this->getInput('band'))) {
return 'https://'
. $this->getInput('band')
. '.bandcamp.com/music';
}
break;
case 'By album':
if(!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) {
return 'https://'
. $this->getInput('band')
. '.bandcamp.com/album/'
. $this->getInput('album');
}
break;
}
return parent::getURI();
}
public function getName(){
switch($this->queriedContext) {
case 'By tag':
if(!is_null($this->getInput('tag'))) {
return $this->getInput('tag') . ' - Bandcamp Tag';
}
break;
case 'By band':
if(isset($this->feedName)) {
return $this->feedName . ' - Bandcamp Band';
} elseif(!is_null($this->getInput('band'))) {
return $this->getInput('band') . ' - Bandcamp Band';
}
break;
case 'By album':
if(isset($this->feedName)) {
return $this->feedName . ' - Bandcamp Album';
} elseif(!is_null($this->getInput('album'))) {
return $this->getInput('album') . ' - Bandcamp Album';
}
break;
}
return parent::getName();
}
public function detectParameters($url) {
$params = array();
// By tag
$regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['tag'] = urldecode($matches[2]);
return $params;
}
// By band
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/';
if(preg_match($regex, $url, $matches) > 0) {
$params['band'] = urldecode($matches[2]);
return $params;
}
// By album
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['band'] = urldecode($matches[2]);
$params['album'] = urldecode($matches[3]);
return $params;
}
return null;
}
}

View File

@@ -3,17 +3,11 @@ class BastaBridge extends BridgeAbstract {
const MAINTAINER = 'qwertygc';
const NAME = 'Bastamag Bridge';
const URI = 'http://www.bastamag.net/';
const URI = 'https://www.bastamag.net/';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns the newest articles.';
public function collectData(){
// Replaces all relative image URLs by absolute URLs.
// Relative URLs always start with 'local/'!
function replaceImageUrl($content){
return preg_replace('/src=["\']{1}([^"\']+)/ims', 'src=\'' . self::URI . '$1\'', $content);
}
$html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend')
or returnServerError('Could not request Bastamag.');
@@ -25,7 +19,13 @@ class BastaBridge extends BridgeAbstract {
$item['title'] = $element->find('title', 0)->innertext;
$item['uri'] = $element->find('guid', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
$item['content'] = replaceImageUrl(getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext);
// Replaces all relative image URLs by absolute URLs.
// Relative URLs always start with 'local/'!
$item['content'] = preg_replace(
'/src=["\']{1}([^"\']+)/ims',
'src=\'' . self::URI . '$1\'',
getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext
);
$this->items[] = $item;
$limit++;
}

View File

@@ -92,7 +92,7 @@ class BingSearchBridge extends BridgeAbstract
or returnServerError('Could not request ' . self::NAME);
$sizeKey = $this->getInput('image_size');
$items = [];
$items = array();
foreach ($html->find('a.iusc') as $element) {
$data = json_decode(htmlspecialchars_decode($element->getAttribute('m')), true);

View File

@@ -1,69 +0,0 @@
<?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 getIcon() {
return 'https://assets.bwbx.io/s3/javelin/public/hub/images/favicon-black-63fe5249d3.png';
}
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

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

View File

@@ -22,7 +22,7 @@ class CachetBridge extends BridgeAbstract {
);
const CACHE_TIMEOUT = 300;
private $componentCache = [];
private $componentCache = array();
public function getURI() {
return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host');
@@ -114,13 +114,13 @@ class CachetBridge extends BridgeAbstract {
$uidOrig = $permalink . $incident->created_at;
$uid = hash('sha512', $uidOrig);
$timestamp = strtotime($incident->created_at);
$categories = [];
$categories = array();
$categories[] = $incident->human_status;
if ($componentName !== '') {
$categories[] = $componentName;
}
$item = [];
$item = array();
$item['uri'] = $permalink;
$item['title'] = $title;
$item['timestamp'] = $timestamp;

View File

@@ -2,7 +2,7 @@
class CastorusBridge extends BridgeAbstract {
const MAINTAINER = 'logmanoriginal';
const NAME = 'Castorus Bridge';
const URI = 'http://www.castorus.com';
const URI = 'https://www.castorus.com';
const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'Returns the latest changes';

View File

@@ -3,7 +3,7 @@ class CollegeDeFranceBridge extends BridgeAbstract {
const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'CollegeDeFrance';
const URI = 'http://www.college-de-france.fr/';
const URI = 'https://www.college-de-france.fr/';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance';

View File

@@ -0,0 +1,65 @@
<?php
class ComicsKingdomBridge extends BridgeAbstract {
const MAINTAINER = 'stjohnjohnson';
const NAME = 'Comics Kingdom Unofficial RSS';
const URI = 'https://www.comicskingdom.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Comics Kingdom Unofficial RSS';
const PARAMETERS = array( array(
'comicname' => array(
'name' => 'comicname',
'type' => 'text',
'required' => true
)
));
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI(), array(), array(), true, false)
or returnServerError('Could not request Comics Kingdom: ' . $this->getURI());
// Get author from first page
$author = $html->find('div.author p', 0)->plaintext
or returnServerError('Comics Kingdom comic does not exist: ' . $this->getURI());;
// Get current date/link
$link = $html->find('meta[property=og:url]', 0)->content;
for($i = 0; $i < 5; $i++) {
$item = array();
$page = getSimpleHTMLDOM($link)
or returnServerError('Could not request Comics Kingdom: ' . $link);
$imagelink = $page->find('meta[property=og:image]', 0)->content;
$prevSlug = $page->find('slider-arrow[:is-left-arrow=true]', 0);
$link = $this->getURI() . '/' . $prevSlug->getAttribute('date-slug');
$date = explode('/', $link);
$item['id'] = $imagelink;
$item['uri'] = $link;
$item['author'] = $author;
$item['title'] = 'Comics Kingdom ' . $this->getInput('comicname');
$item['timestamp'] = DateTime::createFromFormat('Y-m-d', $date[count($date) - 1])->getTimestamp();
$item['content'] = '<img src="' . $imagelink . '" />';
$this->items[] = $item;
}
}
public function getURI(){
if(!is_null($this->getInput('comicname'))) {
return self::URI . urlencode($this->getInput('comicname'));
}
return parent::getURI();
}
public function getName(){
if(!is_null($this->getInput('comicname'))) {
return $this->getInput('comicname') . ' - Comics Kingdom';
}
return parent::getName();
}
}

View File

@@ -10,20 +10,20 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
const BETA = 'beta';
const ALPHA = 'alpha';
const PARAMETERS = [
[
'channel' => [
const PARAMETERS = array(
array(
'channel' => array(
'name' => 'Release Channel',
'type' => 'list',
'defaultValue' => self::STABLE,
'values' => [
'values' => array(
'Stable' => self::STABLE,
'Beta' => self::BETA,
'Alpha' => self::ALPHA,
],
]
]
];
),
)
)
);
private function getReleaseFeed($jsonUrl) {
$json = getContents($jsonUrl)
@@ -39,7 +39,7 @@ class ContainerLinuxReleasesBridge extends BridgeAbstract {
$data = $this->getReleaseFeed($this->getJsonUri());
foreach ($data as $releaseVersion => $release) {
$item = [];
$item = array();
$item['uri'] = "https://coreos.com/releases/#$releaseVersion";
$item['title'] = $releaseVersion;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,79 @@
<?php
class DarkReadingBridge extends FeedExpander {
const MAINTAINER = 'ORelio';
const NAME = 'Dark Reading Bridge';
const URI = 'https://www.darkreading.com/';
const DESCRIPTION = 'Returns the newest articles from Dark Reading';
const PARAMETERS = array( array(
'feed' => array(
'name' => 'Feed',
'type' => 'list',
'values' => array(
'All Dark Reading Stories' => '000_AllArticles',
'Attacks/Breaches' => '644_Attacks/Breaches',
'Application Security' => '645_Application%20Security',
'Database Security' => '646_Database%20Security',
'Cloud' => '647_Cloud',
'Endpoint' => '648_Endpoint',
'Authentication' => '649_Authentication',
'Privacy' => '650_Privacy',
'Mobile' => '651_Mobile',
'Perimeter' => '652_Perimeter',
'Risk' => '653_Risk',
'Compliance' => '654_Compliance',
'Operations' => '655_Operations',
'Careers and People' => '656_Careers%20and%20People',
'Identity and Access Management' => '657_Identity%20and%20Access%20Management',
'Analytics' => '658_Analytics',
'Threat Intelligence' => '659_Threat%20Intelligence',
'Security Monitoring' => '660_Security%20Monitoring',
'Vulnerabilities / Threats' => '661_Vulnerabilities%20/%20Threats',
'Advanced Threats' => '662_Advanced%20Threats',
'Insider Threats' => '663_Insider%20Threats',
'Vulnerability Management' => '664_Vulnerability%20Management',
)
)
));
public function collectData(){
$feed = $this->getInput('feed');
$feed_splitted = explode('_', $feed);
$feed_id = $feed_splitted[0];
$feed_name = $feed_splitted[1];
if(empty($feed) || !ctype_digit($feed_id) || !preg_match('/[A-Za-z%20\/]/', $feed_name)) {
returnClientError('Invalid feed, please check the "feed" parameter.');
}
$feed_url = $this->getURI() . 'rss_simple.asp';
if ($feed_id != '000') {
$feed_url .= '?f_n=' . $feed_id . '&f_ln=' . $feed_name;
}
$this->collectExpandableDatas($feed_url);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
$article = getSimpleHTMLDOMCached($item['uri'])
or returnServerError('Could not request Dark Reading: ' . $item['uri']);
$item['content'] = $this->extractArticleContent($article);
$item['enclosures'] = array(); //remove author profile picture
return $item;
}
private function extractArticleContent($article){
$content = $article->find('div#article-main', 0)->innertext;
foreach (array(
'<div class="divsplitter',
'<div style="float: left; margin-right: 2px;',
'<div class="more-insights',
'<div id="more-insights',
) as $div_start) {
$content = stripRecursiveHTMLSection($content, 'div', $div_start);
}
$content = stripWithDelimiters($content, '<h1 ', '</h1>');
return $content;
}
}

View File

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

View File

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

View File

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

View File

@@ -116,6 +116,12 @@ class DesoutterBridge extends BridgeAbstract {
'name' => 'Load full articles',
'type' => 'checkbox',
'title' => 'Enable to load the full article for each item'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 3,
'title' => "Maximum number of items to return in the feed.\n0 = unlimited"
)
)
);
@@ -156,6 +162,8 @@ class DesoutterBridge extends BridgeAbstract {
$this->title = html_entity_decode($html->find('title', 0)->plaintext, ENT_QUOTES);
$limit = $this->getInput('limit') ?: 0;
foreach($html->find('article') as $article) {
$item = array();
@@ -169,6 +177,8 @@ class DesoutterBridge extends BridgeAbstract {
}
$this->items[] = $item;
if ($limit > 0 && count($this->items) >= $limit) break;
}
}

View File

@@ -51,15 +51,10 @@ apple-icon-5c6fa9f2bce280428589c6195b7f1924206a53b782b371cfe2d02da932c8c173.png'
$html = defaultLinkTo($html, static::URI);
$articles = $html->find('div[class="single-article"]')
$articles = $html->find('div.single-article')
or returnServerError('Could not find articles!');
foreach($articles as $article) {
if($article->find('[class*="cta"]', 0)) { // Skip ads
continue;
}
$item = array();
$item['uri'] = $article->find('a[id*=article-link]', 0)->href;
@@ -92,6 +87,14 @@ EOD;
}
public function getName() {
if (!is_null($this->getInput('tag'))) {
return ucfirst($this->getInput('tag')) . ' - dev.to';
}
return parent::getName();
}
private function getFullArticle($url) {
$html = getSimpleHTMLDOMCached($url)
or returnServerError('Unable to load article from "' . $url . '"!');

View File

@@ -0,0 +1,60 @@
<?php
class DiarioDoAlentejoBridge extends BridgeAbstract {
const MAINTAINER = 'somini';
const NAME = 'Diário do Alentejo';
const URI = 'https://www.diariodoalentejo.pt';
const DESCRIPTION = 'Semanário Regionalista Independente';
const CACHE_TIMEOUT = 28800; // 8h
/* This is used to hack around obtaining a timestamp. It's just a list of Month names in Portuguese ... */
const PT_MONTH_NAMES = array(
'janeiro',
'fevereiro',
'março',
'abril',
'maio',
'junho',
'julho',
'agosto',
'setembro',
'outubro',
'novembro',
'dezembro');
public function getIcon() {
return 'https://www.diariodoalentejo.pt/images/favicon/apple-touch-icon.png';
}
public function collectData(){
/* This is slow as molasses (>30s!), keep the cache timeout high to avoid killing the host */
$html = getSimpleHTMLDOMCached($this->getURI() . '/pt/noticias-listagem.aspx')
or returnServerError('Could not load content');
foreach($html->find('.list_news .item') as $element) {
$item = array();
$item_link = $element->find('.body h2.title a', 0);
/* Another broken URL, see also `bridges/ComboiosDePortugalBridge.php` */
$item['uri'] = self::URI . implode('/', array_map('urlencode', explode('/', $item_link->href)));
$item['title'] = $item_link->innertext;
$item['timestamp'] = str_ireplace(
array_map(function($name) { return ' ' . $name . ' '; }, self::PT_MONTH_NAMES),
array_map(function($num) { return sprintf('-%02d-', $num); }, range(1, sizeof(self::PT_MONTH_NAMES))),
$element->find('span.date', 0)->innertext);
/* Fix the Image URL */
$item_image = $element->find('img.thumb', 0);
$item_image->src = preg_replace('/.*&img=([^&]+).*/', '\1', $item_image->getAttribute('data-src'));
/* Content: */
/* - Image */
/* - Category */
$content = $item_image .
'<center>' . $element->find('a.category', 0) . '</center>';
$item['content'] = defaultLinkTo($content, self::URI);
$this->items[] = $item;
}
}
}

123
bridges/DonnonsBridge.php Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
$json = $this->loadEmbeddedJsonData($html);
foreach($html->find('li[id^="screenshot-"]') as $shot) {
$item = [];
$item = array();
$additional_data = $this->findJsonForShot($shot, $json);
if ($additional_data === null) {
@@ -38,14 +38,14 @@ favicon-63b2904a073c89b52b19aa08cebc16a154bcf83fee8ecc6439968b1e6db569c7.ico';
$preview_path = $shot->find('picture source', 0)->attr['srcset'];
$item['content'] .= $this->getImageTag($preview_path, $item['title']);
$item['enclosures'] = [$this->getFullSizeImagePath($preview_path)];
$item['enclosures'] = array($this->getFullSizeImagePath($preview_path));
$this->items[] = $item;
}
}
private function loadEmbeddedJsonData($html){
$json = [];
$json = array();
$scripts = $html->find('script');
foreach($scripts as $script) {

View File

@@ -40,7 +40,7 @@ class EconomistBridge extends BridgeAbstract {
if ($nextprev)
$nextprev->outertext = '';
$section = [ $article->find('h3[itemprop="articleSection"]', 0)->plaintext ];
$section = array( $article->find('h3[itemprop="articleSection"]', 0)->plaintext );
$item = array();
$item['title'] = $header->find('span', 0)->innertext . ': '

View File

@@ -95,7 +95,7 @@ class ElloBridge extends BridgeAbstract {
private function getEnclosures($post, $postData) {
$assets = [];
$assets = array();
foreach($post->links->assets as $asset) {
foreach($postData->linked->assets as $assetLink) {
if($asset == $assetLink->id) {
@@ -124,7 +124,7 @@ class ElloBridge extends BridgeAbstract {
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope(get_called_class());
$cache->setKey(['key']);
$cache->setKey(array('key'));
$key = $cache->loadData();
if($key == null) {

View File

@@ -3,7 +3,7 @@ class ElsevierBridge extends BridgeAbstract {
const MAINTAINER = 'Pierre Mazière';
const NAME = 'Elsevier journals recent articles';
const URI = 'http://www.journals.elsevier.com/';
const URI = 'https://www.journals.elsevier.com/';
const CACHE_TIMEOUT = 43200; //12h
const DESCRIPTION = 'Returns the recent articles published in Elsevier journals';

View File

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

View File

@@ -0,0 +1,70 @@
<?php
class EsquerdaNetBridge extends FeedExpander {
const MAINTAINER = 'somini';
const NAME = 'Esquerda.net';
const URI = 'https://www.esquerda.net';
const DESCRIPTION = 'Esquerda.net';
const PARAMETERS = array(
array(
'feed' => array(
'name' => 'Feed',
'type' => 'list',
'defaultValue' => 'Geral',
'values' => array(
'Geral' => 'geral',
'Dossier' => 'artigos-dossier',
'Vídeo' => 'video',
'Opinião' => 'opinioes',
'Rádio' => 'radio',
)
)
)
);
public function getURI() {
$type = $this->getInput('feed');
return self::URI . '/rss/' . $type;
}
public function getIcon() {
return 'https://www.esquerda.net/sites/default/files/favicon_0.ico';
}
public function collectData(){
parent::collectExpandableDatas($this->getURI());
}
protected function parseItem($newsItem){
# Fix Publish date
$badDate = $newsItem->pubDate;
preg_match('|(?P<day>\d\d)/(?P<month>\d\d)/(?P<year>\d\d\d\d) - (?P<hour>\d\d):(?P<minute>\d\d)|', $badDate, $d);
$newsItem->pubDate = sprintf('%s-%s-%sT%s:%s', $d['year'], $d['month'], $d['day'], $d['hour'], $d['minute']);
$item = parent::parseItem($newsItem);
# Include all the content
$uri = $item['uri'];
$html = getSimpleHTMLDOMCached($uri)
or returnServerError('Could not load content for ' . $uri);
$content = $html->find('div#content div.content', 0);
## Fix author
$authorHTML = $html->find('.field-name-field-op-author a', 0);
if ($authorHTML) {
$item['author'] = $authorHTML->innertext;
$authorHTML->remove();
}
## Remove crap
$content->find('.field-name-addtoany', 0)->remove();
## Fix links
$content = defaultLinkTo($content, self::URI);
## Fix Images
foreach($content->find('img') as $img) {
$altSrc = $img->getAttribute('data-src');
if ($altSrc) {
$img->setAttribute('src', $altSrc);
}
$img->width = null;
$img->height = null;
}
$item['content'] = $content;
return $item;
}
}

View File

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

View File

@@ -2,7 +2,7 @@
class FB2Bridge extends BridgeAbstract {
const MAINTAINER = 'teromene';
const NAME = 'Facebook Alternate';
const NAME = 'Facebook Bridge | Touch Site';
const URI = 'https://www.facebook.com/';
const CACHE_TIMEOUT = 1000;
const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
@@ -12,7 +12,12 @@ class FB2Bridge extends BridgeAbstract {
'u' => array(
'name' => 'Username',
'required' => true
)
),
'abbrev_name' => array(
'name' => 'Abbreviate author name in title',
'type' => 'checkbox',
'defaultValue' => true,
),
));
public function getIcon() {
@@ -102,12 +107,12 @@ EOD
else
$timestamp = 0;
$item['uri'] = html_entity_decode('http://touch.facebook.com'
$item['uri'] = html_entity_decode('https://touch.facebook.com'
. $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES);
//Decode images
$imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) {
return "<img src='" . str_replace(['\\3a ', '\\3d ', '\\26 '], [':', '=', '&'], $matches[1]) . "' />";
return "<img src='" . str_replace(array('\\3a ', '\\3d ', '\\26 '), array(':', '=', '&'), $matches[1]) . "' />";
}, $content);
$content = str_get_html($imagecleaned);
@@ -159,7 +164,11 @@ EOD
$content = preg_replace('/<img src=\'.*?safe_image\.php.*?\' \/>/m', '', $content);
//Remove the double section tags
$content = str_replace(['<section><section>', '</section></section>'], ['<section>', '</section>'], $content);
$content = str_replace(
array('<section><section>', '</section></section>'),
array('<section>', '</section>'),
$content
);
//Move the section tag link upper, if it is down
$content = str_get_html($content);
@@ -182,8 +191,10 @@ EOD
$item['content'] = html_entity_decode($content, ENT_QUOTES);
$title = $author;
if ($this->getInput('abbrev_name') === true) {
if (strlen($title) > 24)
$title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...';
}
$title = $title . ' | ' . strip_tags($content);
if (strlen($title) > 64)
$title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
@@ -281,10 +292,20 @@ EOD
}
public function getName(){
return (isset($this->name) ? $this->name . ' - ' : '') . 'Facebook Bridge';
$username = $this->getInput('u');
if (isset($username)) {
return $this->getInput('u') . ' | Facebook';
} else {
return self::NAME;
}
}
public function getURI(){
return 'http://facebook.com';
$username = $this->getInput('u');
if (isset($username)) {
return 'https://facebook.com/' . $this->getInput('u') . '/posts';
} else {
return self::URI;
}
}
}

View File

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

View File

@@ -2,7 +2,7 @@
class FacebookBridge extends BridgeAbstract {
const MAINTAINER = 'teromene, logmanoriginal';
const NAME = 'Facebook Bridge';
const NAME = 'Facebook Bridge | Main Site';
const URI = 'https://www.facebook.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Input a page title or a profile log. For a profile log,
@@ -66,14 +66,13 @@ class FacebookBridge extends BridgeAbstract {
case 'User':
if(!empty($this->authorName)) {
return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName
. ' - ' . static::NAME;
return isset($this->extraInfos['name']) ? $this->extraInfos['name'] : $this->authorName;
}
break;
case 'Group':
if(!empty($this->groupName)) {
return $this->groupName . ' - ' . static::NAME;
return $this->groupName;
}
break;
@@ -82,6 +81,34 @@ class FacebookBridge extends BridgeAbstract {
return parent::getName();
}
public function detectParameters($url){
$params = array();
// By profile
$regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/profile\.php\?id\=([^\/?&\n]+)?(.*)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['u'] = urldecode($matches[3]);
return $params;
}
// By group
$regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/groups\/([^\/?\n]+)?(.*)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['g'] = urldecode($matches[3]);
return $params;
}
// By username
$regex = '/^(https?:\/\/)?(www\.)?facebook\.com\/([^\/?\n]+)/';
if(preg_match($regex, $url, $matches) > 0) {
$params['u'] = urldecode($matches[3]);
return $params;
}
return null;
}
public function getURI() {
$uri = self::URI;
@@ -142,7 +169,11 @@ class FacebookBridge extends BridgeAbstract {
private function collectGroupData() {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE') . "\r\n");
if(getEnv('HTTP_ACCEPT_LANGUAGE')) {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
} else {
$header = array();
}
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('Failed loading facebook page: ' . $this->getURI());
@@ -505,7 +536,11 @@ EOD;
// Retrieve page contents
if(is_null($html)) {
if(getEnv('HTTP_ACCEPT_LANGUAGE')) {
$header = array('Accept-Language: ' . getEnv('HTTP_ACCEPT_LANGUAGE'));
} else {
$header = array();
}
$html = getSimpleHTMLDOM($this->getURI(), $header)
or returnServerError('No results for this query.');
@@ -580,6 +615,8 @@ EOD;
'._5mly', // Remove embedded videos (the preview image remains)
'._2ezg', // Remove "Views ..."
'.hidden_elem', // Remove hidden elements (they are hidden anyway)
'.timestampContent', // Remove relative timestamp
'._6spk', // Remove redundant separator
);
foreach($content_filters as $filter) {
@@ -664,9 +701,16 @@ EOD;
$uri = $post->find('abbr')[0]->parent()->getAttribute('href');
if (false !== strpos($uri, '?')) {
// Extract fbid and patch link
if (strpos($uri, '?') !== false) {
$query = substr($uri, strpos($uri, '?') + 1);
parse_str($query, $query_params);
if (isset($query_params['story_fbid'])) {
$uri = self::URI . $query_params['story_fbid'];
} else {
$uri = substr($uri, 0, strpos($uri, '?'));
}
}
//Build and add final item
$item['uri'] = htmlspecialchars_decode($uri, ENT_QUOTES);
@@ -685,6 +729,7 @@ EOD;
}
}
}
#endregion (User)
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
<?php
class FreeCodeCampBridge extends FeedExpander {
const MAINTAINER = 'IceWreck';
const NAME = 'FreeCodecamp Bridge';
const URI = 'https://www.freecodecamp.org';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'RSS feed for FreeCodeCamp';
// Freecodecamp removed their old full content rss feed and replaced it with one liner content.
public function collectData(){
$this->collectExpandableDatas('https://www.freecodecamp.org/news/rss/', 15);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
// $articlePage gets the entire page's contents
$articlePage = getSimpleHTMLDOM($newsItem->link);
// figure contain's the main article image
$article = $articlePage->find('figure', 0);
// the actual article
foreach($articlePage->find('.post-full-content') as $element)
$article = $article . $element;
$item['content'] = $article;
return $item;
}
}

View File

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

View File

@@ -0,0 +1,110 @@
<?php
class FurAffinityUserBridge extends BridgeAbstract {
const NAME = 'FurAffinity User Gallery';
const URI = 'https://www.furaffinity.net';
const MAINTAINER = 'CyberJacob';
const PARAMETERS = array(
array(
'searchUsername' => array(
'name' => 'Search Username',
'type' => 'text',
'required' => true,
'title' => 'Username to fetch the gallery for'
),
'loginUsername' => array(
'name' => 'Login Username',
'type' => 'text',
'required' => true
),
'loginPassword' => array(
'name' => 'Login Password',
'type' => 'text',
'required' => true
)
)
);
public function collectData() {
$cookies = self::login();
$url = self::URI . '/gallery/' . $this->getInput('searchUsername');
$html = getSimpleHTMLDOM($url, $cookies)
or returnServerError('Could not load the user\'s galary page.');
$submissions = $html->find('section[id=gallery-gallery]', 0)->find('figure');
foreach($submissions as $submission) {
$item = array();
$item['title'] = $submission->find('figcaption', 0)->find('a', 0)->plaintext;
$thumbnail = $submission->find('a', 0);
$thumbnail->href = self::URI . $thumbnail->href;
$item['content'] = $submission->find('a', 0);
$this->items[] = $item;
}
}
public function getName() {
return self::NAME . ' for ' . $this->getInput('searchUsername');
}
public function getURI() {
return self::URI . '/user/' . $this->getInput('searchUsername');
}
private function login() {
$ch = curl_init(self::URI . '/login/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, ini_get('user_agent'));
curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
$fields = implode('&', array(
'action=login',
'retard_protection=1',
'name=' . urlencode($this->getInput('loginUsername')),
'pass=' . urlencode($this->getInput('loginPassword')),
'login=Login to Faraffinity'
));
curl_setopt($ch, CURLOPT_POST, 5);
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields);
if(defined('PROXY_URL') && !defined('NOPROXY')) {
curl_setopt($ch, CURLOPT_PROXY, PROXY_URL);
}
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$data = curl_exec($ch);
$errorCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
$curlInfo = curl_getinfo($ch);
if($data === false)
fDebug::log("Cant't download {$url} cUrl error: {$curlError} ({$curlErrno})");
curl_close($ch);
if($errorCode != 200) {
returnServerError(error_get_last());
} else {
preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $data, $matches);
$cookies = array();
foreach($matches[1] as $item) {
parse_str($item, $cookie);
$cookies = array_merge($cookies, $cookie);
}
return $cookies;
}
}
}

View File

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

View File

@@ -5,7 +5,7 @@ class GiphyBridge extends BridgeAbstract {
const MAINTAINER = 'kraoc';
const NAME = 'Giphy Bridge';
const URI = 'http://giphy.com/';
const URI = 'https://giphy.com/';
const CACHE_TIMEOUT = 300; //5min
const DESCRIPTION = 'Bridge for giphy.com';

View File

@@ -82,18 +82,21 @@ class GithubIssueBridge extends BridgeAbstract {
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0)->plaintext;
$title .= ' / ' . trim($comment->plaintext);
$content = $title;
if (null !== $comment->nextSibling()) {
$content = $comment->nextSibling()->innertext;
if ($comment->nextSibling()->nodeName() === 'span') {
$content = $comment->nextSibling()->nextSibling()->innertext;
}
$author = $comment->find('.author', 0);
if ($author) {
$author = $author->plaintext;
} else {
$author = '';
}
$title .= ' / '
. trim(str_replace(
array('octicon','-'), array(''),
$comment->find('.octicon', 0)->getAttribute('class')
));
$content = $comment->plaintext;
$item = array();
$item['author'] = $author;
$item['uri'] = $uri;
@@ -135,32 +138,20 @@ class GithubIssueBridge extends BridgeAbstract {
substr($issue->find('.gh-header-number', 0)->plaintext, 1)
);
$comments = $issue->find('
[id^="issue-"] > .comment,
[id^="issuecomment-"] > .comment,
[id^="event-"],
[id^="ref-"]
');
$comments = $issue->find(
'.comment, .TimelineItem-badge'
);
foreach($comments as $comment) {
if (!$comment->hasChildNodes()) {
continue;
}
if (!$comment->hasClass('discussion-item-header')) {
if ($comment->hasClass('comment')) {
$comment = $comment->parent;
$item = $this->extractIssueComment($issueNbr, $title, $comment);
$items[] = $item;
continue;
}
while ($comment->hasClass('discussion-item-header')) {
} else {
$comment = $comment->parent;
$item = $this->extractIssueEvent($issueNbr, $title, $comment);
$items[] = $item;
$comment = $comment->nextSibling();
if (null == $comment) {
break;
}
$classes = explode(' ', $comment->getAttribute('class'));
}
}

View File

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

View File

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

View File

@@ -25,13 +25,10 @@ class GoogleSearchBridge extends BridgeAbstract {
public function collectData(){
$html = '';
$html = getSimpleHTMLDOM(self::URI
. 'search?q='
. urlencode($this->getInput('q'))
. '&num=100&complete=0&tbs=qdr:y,sbd:1')
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('No results for this query.');
$emIsRes = $html->find('div[id=ires]', 0);
$emIsRes = $html->find('div[id=res]', 0);
if(!is_null($emIsRes)) {
foreach($emIsRes->find('div[class=g]') as $element) {
@@ -54,6 +51,17 @@ class GoogleSearchBridge extends BridgeAbstract {
}
}
public function getURI() {
if (!is_null($this->getInput('q'))) {
return self::URI
. 'search?q='
. urlencode($this->getInput('q'))
. '&num=100&complete=0&tbs=qdr:y,sbd:1';
}
return parent::getURI();
}
public function getName(){
if(!is_null($this->getInput('q'))) {
return $this->getInput('q') . ' - Google search';

View File

@@ -2,7 +2,7 @@
class HDWallpapersBridge extends BridgeAbstract {
const MAINTAINER = 'nel50n';
const NAME = 'HD Wallpapers Bridge';
const URI = 'http://www.hdwallpapers.in/';
const URI = 'https://www.hdwallpapers.in/';
const CACHE_TIMEOUT = 43200; //12h
const DESCRIPTION = 'Returns the latests wallpapers from HDWallpapers';
@@ -72,7 +72,7 @@ class HDWallpapersBridge extends BridgeAbstract {
public function getName(){
if(!is_null($this->getInput('c')) && !is_null($this->getInput('r'))) {
return 'HDWallpapers - '
. str_replace(['__', '_'], [' & ', ' '], $this->getInput('c'))
. str_replace(array('__', '_'), array(' & ', ' '), $this->getInput('c'))
. ' ['
. $this->getInput('r')
. ']';

View File

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

View File

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

65
bridges/IGNBridge.php Normal file
View File

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

View File

@@ -32,28 +32,61 @@ class InstagramBridge extends BridgeAbstract {
'required' => false,
'values' => array(
'All' => 'all',
'Story' => 'story',
'Video' => 'video',
'Picture' => 'picture',
'Multiple' => 'multiple',
),
'defaultValue' => 'all'
),
'direct_links' => array(
'name' => 'Use direct media links',
'type' => 'checkbox',
)
)
);
public function collectData(){
const USER_QUERY_HASH = '58b6785bea111c67129decbe6a448951';
const TAG_QUERY_HASH = '174a5243287c5f3a7de741089750ab3b';
const SHORTCODE_QUERY_HASH = '865589822932d1b43dfe312121dd353a';
if(is_null($this->getInput('u')) && $this->getInput('media_type') == 'story') {
returnClientError('Stories are not supported for hashtags nor locations!');
protected function getInstagramUserId($username) {
if(is_numeric($username)) return $username;
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope(get_called_class());
$cache->setKey(array($username));
$key = $cache->loadData();
if($key == null) {
$data = getContents(self::URI . 'web/search/topsearch/?query=' . $username);
foreach(json_decode($data)->users as $user) {
if($user->user->username === $username) {
$key = $user->user->pk;
}
}
if($key == null) {
returnServerError('Unable to find username in search result.');
}
$cache->saveData($key);
}
return $key;
}
public function collectData(){
$directLink = !is_null($this->getInput('direct_links')) && $this->getInput('direct_links');
$data = $this->getInstagramJSON($this->getURI());
if(!is_null($this->getInput('u'))) {
$userMedia = $data->entry_data->ProfilePage[0]->graphql->user->edge_owner_to_timeline_media->edges;
$userMedia = $data->data->user->edge_owner_to_timeline_media->edges;
} elseif(!is_null($this->getInput('h'))) {
$userMedia = $data->entry_data->TagPage[0]->graphql->hashtag->edge_hashtag_to_media->edges;
$userMedia = $data->data->hashtag->edge_hashtag_to_media->edges;
} elseif(!is_null($this->getInput('l'))) {
$userMedia = $data->entry_data->LocationsPage[0]->graphql->location->edge_location_to_media->edges;
}
@@ -61,23 +94,19 @@ class InstagramBridge extends BridgeAbstract {
foreach($userMedia as $media) {
$media = $media->node;
if(!is_null($this->getInput('u'))) {
switch($this->getInput('media_type')) {
case 'all': break;
case 'video':
if($media->__typename != 'GraphVideo') continue 2;
if($media->__typename != 'GraphVideo' || !$media->is_video) continue 2;
break;
case 'picture':
if($media->__typename != 'GraphImage') continue 2;
break;
case 'story':
case 'multiple':
if($media->__typename != 'GraphSidecar') continue 2;
break;
default: break;
}
} else {
if($this->getInput('media_type') == 'video' && !$media->is_video) continue;
}
$item = array();
$item['uri'] = self::URI . 'p/' . $media->shortcode . '/';
@@ -86,65 +115,137 @@ class InstagramBridge extends BridgeAbstract {
$item['author'] = $media->owner->username;
}
if (isset($media->edge_media_to_caption->edges[0]->node->text)) {
$textContent = $media->edge_media_to_caption->edges[0]->node->text;
} else {
$textContent = '(no text)';
}
$textContent = $this->getTextContent($media);
$item['title'] = ($media->is_video ? '▶ ' : '') . trim($textContent);
$item['title'] = ($media->is_video ? '▶ ' : '') . $textContent;
$titleLinePos = strpos(wordwrap($item['title'], 120), "\n");
if ($titleLinePos != false) {
$item['title'] = substr($item['title'], 0, $titleLinePos) . '...';
}
if(!is_null($this->getInput('u')) && $media->__typename == 'GraphSidecar') {
$data = $this->getInstagramStory($item['uri']);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
if($directLink) {
$mediaURI = $media->display_url;
} else {
$mediaURI = self::URI . 'p/' . $media->shortcode . '/media?size=l';
}
switch($media->__typename) {
case 'GraphSidecar':
$data = $this->getInstagramSidecarData($item['uri'], $item['title']);
$item['content'] = $data[0];
$item['enclosures'] = $data[1];
break;
case 'GraphImage':
$item['content'] = '<a href="' . htmlentities($item['uri']) . '" target="_blank">';
$item['content'] .= '<img src="' . htmlentities($mediaURI) . '" alt="' . $item['title'] . '" />';
$item['content'] .= '</a><br><br>' . nl2br(htmlentities($textContent));
$item['enclosures'] = array($mediaURI);
break;
case 'GraphVideo':
$data = $this->getInstagramVideoData($item['uri'], $mediaURI);
$item['content'] = $data[0];
if($directLink) {
$item['enclosures'] = $data[1];
} else {
$item['enclosures'] = array($mediaURI);
}
$item['thumbnail'] = $mediaURI;
break;
default: break;
}
$item['timestamp'] = $media->taken_at_timestamp;
$this->items[] = $item;
}
}
protected function getInstagramStory($uri) {
// returns Sidecar(a post which has multiple media)'s contents and enclosures
protected function getInstagramSidecarData($uri, $postTitle) {
$mediaInfo = $this->getSinglePostData($uri);
$data = $this->getInstagramJSON($uri);
$mediaInfo = $data->entry_data->PostPage[0]->graphql->shortcode_media;
$textContent = $this->getTextContent($mediaInfo);
//Process the first element, that isn't in the node graph
if (count($mediaInfo->edge_media_to_caption->edges) > 0) {
$caption = $mediaInfo->edge_media_to_caption->edges[0]->node->text;
$enclosures = array();
$content = '';
foreach($mediaInfo->edge_sidecar_to_children->edges as $singleMedia) {
$singleMedia = $singleMedia->node;
if($singleMedia->is_video) {
if(in_array($singleMedia->video_url, $enclosures)) continue; // check if not added yet
$content .= '<video controls><source src="' . $singleMedia->video_url . '" type="video/mp4"></video><br>';
array_push($enclosures, $singleMedia->video_url);
} else {
$caption = '';
}
$enclosures = [$mediaInfo->display_url];
$content = '<img src="' . htmlentities($mediaInfo->display_url) . '" alt="' . $caption . '" />';
foreach($mediaInfo->edge_sidecar_to_children->edges as $media) {
$display_url = $media->node->display_url;
if(!in_array($display_url, $enclosures)) { // add only if not added yet
$content .= '<img src="' . htmlentities($display_url) . '" alt="' . $caption . '" />';
$enclosures[] = $display_url;
if(in_array($singleMedia->display_url, $enclosures)) continue; // check if not added yet
$content .= '<a href="' . $singleMedia->display_url . '" target="_blank">';
$content .= '<img src="' . $singleMedia->display_url . '" alt="' . $postTitle . '" />';
$content .= '</a><br>';
array_push($enclosures, $singleMedia->display_url);
}
}
$content .= '<br>' . nl2br(htmlentities($textContent));
return [$content, $enclosures];
return array($content, $enclosures);
}
// returns Video post's contents and enclosures
protected function getInstagramVideoData($uri, $mediaURI) {
$mediaInfo = $this->getSinglePostData($uri);
$textContent = $this->getTextContent($mediaInfo);
$content = '<video controls>';
$content .= '<source src="' . $mediaInfo->video_url . '" poster="' . $mediaURI . '" type="video/mp4">';
$content .= '<img src="' . $mediaURI . '" alt="">';
$content .= '</video><br>';
$content .= '<br>' . nl2br(htmlentities($textContent));
return array($content, array($mediaInfo->video_url));
}
protected function getTextContent($media) {
$textContent = '(no text)';
//Process the first element, that isn't in the node graph
if (count($media->edge_media_to_caption->edges) > 0) {
$textContent = trim($media->edge_media_to_caption->edges[0]->node->text);
}
return $textContent;
}
protected function getSinglePostData($uri) {
$shortcode = explode('/', $uri)[4];
$data = getContents(self::URI .
'graphql/query/?query_hash=' .
self::SHORTCODE_QUERY_HASH .
'&variables={"shortcode"%3A"' .
$shortcode .
'"}');
return json_decode($data)->data->shortcode_media;
}
protected function getInstagramJSON($uri) {
if(!is_null($this->getInput('u'))) {
$userId = $this->getInstagramUserId($this->getInput('u'));
$data = getContents(self::URI .
'graphql/query/?query_hash=' .
self::USER_QUERY_HASH .
'&variables={"id"%3A"' .
$userId .
'"%2C"first"%3A10}');
return json_decode($data);
} elseif(!is_null($this->getInput('h'))) {
$data = getContents(self::URI .
'graphql/query/?query_hash=' .
self::TAG_QUERY_HASH .
'&variables={"tag_name"%3A"' .
$this->getInput('h') .
'"%2C"first"%3A10}');
return json_decode($data);
} else {
$html = getContents($uri)
or returnServerError('Could not request Instagram.');
$scriptRegex = '/window\._sharedData = (.*);<\/script>/';
@@ -155,6 +256,8 @@ class InstagramBridge extends BridgeAbstract {
}
}
public function getName(){
if(!is_null($this->getInput('u'))) {
return $this->getInput('u') . ' - Instagram Bridge';

View File

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

View File

@@ -19,28 +19,6 @@ class JapanExpoBridge extends BridgeAbstract {
public function collectData(){
function frenchPubDateToTimestamp($date_to_parse) {
return strtotime(
strtr(
strtolower(str_replace('Publié le ', '', $date_to_parse)),
array(
'janvier' => 'jan',
'février' => 'feb',
'mars' => 'march',
'avril' => 'apr',
'mai' => 'may',
'juin' => 'jun',
'juillet' => 'jul',
'août' => 'aug',
'septembre' => 'sep',
'octobre' => 'oct',
'novembre' => 'nov',
'décembre' => 'dec'
)
)
);
}
$convert_article_images = function($matches){
if(is_array($matches) && count($matches) > 1) {
return '<img src="' . $matches[1] . '" />';
@@ -82,7 +60,7 @@ class JapanExpoBridge extends BridgeAbstract {
$content = $headings . $article;
} else {
$date_text = $element->find('span.date', 0)->plaintext;
$timestamp = frenchPubDateToTimestamp($date_text);
$timestamp = $this->frenchPubDateToTimestamp($date_text);
$title = trim($element->find('span._title', 0)->plaintext);
$content = '<img src="'
. $thumbnail
@@ -103,4 +81,26 @@ class JapanExpoBridge extends BridgeAbstract {
$count++;
}
}
private function frenchPubDateToTimestamp($date_to_parse) {
return strtotime(
strtr(
strtolower(str_replace('Publié le ', '', $date_to_parse)),
array(
'janvier' => 'jan',
'février' => 'feb',
'mars' => 'march',
'avril' => 'apr',
'mai' => 'may',
'juin' => 'jun',
'juillet' => 'jul',
'août' => 'aug',
'septembre' => 'sep',
'octobre' => 'oct',
'novembre' => 'nov',
'décembre' => 'dec'
)
)
);
}
}

View File

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

View File

@@ -5,7 +5,7 @@ class KonachanBridge extends MoebooruBridge {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Konachan';
const URI = 'http://konachan.com/';
const URI = 'https://konachan.com/';
const DESCRIPTION = 'Returns images from given page';
}

View File

@@ -34,6 +34,12 @@ class KununuBridge extends BridgeAbstract {
'name' => 'Include benefits',
'type' => 'checkbox',
'title' => 'Activate to include benefits in the feed'
),
'limit' => array(
'name' => 'Limit',
'type' => 'number',
'defaultValue' => 3,
'title' => "Maximum number of items to return in the feed.\n0 = unlimited"
)
),
array(
@@ -108,6 +114,8 @@ class KununuBridge extends BridgeAbstract {
$articles = $section->find('article')
or returnServerError('Unable to find articles!');
$limit = $this->getInput('limit') ?: 0;
// Go through all articles
foreach($articles as $article) {
@@ -141,6 +149,8 @@ class KununuBridge extends BridgeAbstract {
$this->items[] = $item;
if ($limit > 0 && count($this->items) >= $limit) break;
}
}

View File

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

View File

@@ -356,6 +356,7 @@ class LeBonCoinBridge extends BridgeAbstract {
$data = $this->buildRequestJson();
$header = array(
'User-Agent: LBC;Android;Null;Null;Null;Null;Null;Null;Null;Null',
'Content-Type: application/json',
'Content-Length: ' . strlen($data),
'api_key: ' . self::$LBC_API_KEY
@@ -430,11 +431,11 @@ class LeBonCoinBridge extends BridgeAbstract {
);
if($this->getInput('region') != '') {
$requestJson->filters->location['regions'] = [$this->getInput('region')];
$requestJson->filters->location['regions'] = array($this->getInput('region'));
}
if($this->getInput('department') != '') {
$requestJson->filters->location['departments'] = [$this->getInput('department')];
$requestJson->filters->location['departments'] = array($this->getInput('department'));
}
if($this->getInput('cities') != '') {
@@ -466,7 +467,7 @@ class LeBonCoinBridge extends BridgeAbstract {
}
if($this->getInput('estate') != '') {
$requestJson->filters->enums['real_estate_type'] = [$this->getInput('estate')];
$requestJson->filters->enums['real_estate_type'] = array($this->getInput('estate'));
}
if($this->getInput('roomsmin') != ''
@@ -525,7 +526,7 @@ class LeBonCoinBridge extends BridgeAbstract {
}
if($this->getInput('fuel') != '') {
$requestJson->filters->enums['fuel'] = [$this->getInput('fuel')];
$requestJson->filters->enums['fuel'] = array($this->getInput('fuel'));
}
$requestJson->limit = 30;

View File

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

View File

@@ -3,7 +3,7 @@ class MangareaderBridge extends BridgeAbstract {
const MAINTAINER = 'logmanoriginal';
const NAME = 'Mangareader Bridge';
const URI = 'http://www.mangareader.net';
const URI = 'https://www.mangareader.net';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the latest updates, popular mangas or manga updates (new chapters)';

View File

@@ -30,6 +30,10 @@ class MediapartBridge extends FeedExpander {
protected function parseItem($newsItem) {
$item = parent::parseItem($newsItem);
// Mediapart provide multiple type of contents.
// We only process items relative to the newspaper
// See issue #1292 - https://github.com/RSS-Bridge/rss-bridge/issues/1292
if (strpos($item['uri'], self::URI . 'journal/') === 0) {
// Enable single page mode?
if ($this->getInput('single_page_mode') === true) {
$item['uri'] .= '?onglet=full';
@@ -54,6 +58,7 @@ class MediapartBridge extends FeedExpander {
$content = defaultLinkTo($content, static::URI);
$item['content'] .= $content;
}
}
return $item;
}

View File

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

View File

@@ -15,11 +15,11 @@ class N26Bridge extends BridgeAbstract
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI . '/en-fr/blog-archive')
$html = getSimpleHTMLDOM(self::URI . '/en-eu/blog-archive')
or returnServerError('Error while downloading the website content');
foreach($html->find('div.ga') as $article) {
$item = [];
foreach($html->find('div[class="ag ah ai aj bs bt dx ea fo gx ie if ih ii ij ik s"]') as $article) {
$item = array();
$item['uri'] = self::URI . $article->find('h2 a', 0)->href;
$item['title'] = $article->find('h2 a', 0)->plaintext;
@@ -27,9 +27,9 @@ class N26Bridge extends BridgeAbstract
$fullArticle = getSimpleHTMLDOM($item['uri'])
or returnServerError('Error while downloading the full article');
$dateElement = $fullArticle->find('span[class="fk fl de ch fm by"]', 0);
$dateElement = $fullArticle->find('time', 0);
$item['timestamp'] = strtotime($dateElement->plaintext);
$item['content'] = $fullArticle->find('main article', 0)->innertext;
$item['content'] = $fullArticle->find('div[class="af ag ah ai an"]', 1)->innertext;
$this->items[] = $item;
}

60
bridges/NFLRUSBridge.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
class NFLRUSBridge extends BridgeAbstract {
const NAME = 'NFLRUS';
const URI = 'http://nflrus.ru/';
const DESCRIPTION = 'Returns the recent articles published on nflrus.ru';
const MAINTAINER = 'Maxim Shpak';
private function getEnglishMonth($month) {
$months = array(
'Января' => 'January',
'Февраля' => 'February',
'Марта' => 'March',
'Апреля' => 'April',
'Мая' => 'May',
'Июня' => 'June',
'Июля' => 'July',
'Августа' => 'August',
'Сентября' => 'September',
'Октября' => 'October',
'Ноября' => 'November',
'Декабря' => 'December',
);
if (isset($months[$month])) {
return $months[$month];
}
return false;
}
private function extractArticleTimestamp($article) {
$time = $article->find('time', 0);
if($time) {
$timestring = trim($time->plaintext);
$parts = explode(' ', $timestring);
$month = $this->getEnglishMonth($parts[1]);
if ($month) {
$timestring = $parts[0] . ' ' . $month . ' ' . $parts[2];
return strtotime($timestring);
}
}
return 0;
}
public function collectData() {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Unable to get any articles from NFLRUS');
$html = defaultLinkTo($html, self::URI);
foreach($html->find('article') as $article) {
$item = array();
$item['uri'] = $article->find('.b-article__title a', 0)->href;
$item['title'] = $article->find('.b-article__title a', 0)->plaintext;
$item['author'] = $article->find('.link-author', 0)->plaintext;
$item['timestamp'] = $this->extractArticleTimestamp($article);
$item['content'] = $article->find('div', 0)->innertext;
$this->items[] = $item;
}
}
}

26
bridges/NYTBridge.php Normal file
View File

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

View File

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

View File

@@ -3,7 +3,7 @@ class NiceMatinBridge extends FeedExpander {
const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'NiceMatin';
const URI = 'http://www.nicematin.com/';
const URI = 'https://www.nicematin.com/';
const DESCRIPTION = 'Returns the 10 newest posts from NiceMatin (full text)';
public function collectData(){

View File

@@ -17,6 +17,15 @@ class NineGagBridge extends BridgeAbstract {
'Fresh' => 'fresh',
),
),
'video' => array(
'name' => 'Filter Video',
'type' => 'list',
'values' => array(
'NotFiltred' => 'none',
'VideoFiltred' => 'without',
'VideoOnly' => 'only',
),
),
'p' => array(
'name' => 'Pages',
'type' => 'number',
@@ -121,6 +130,24 @@ class NineGagBridge extends BridgeAbstract {
}
foreach ($posts as $post) {
$AvoidElement = false;
switch ($this->getInput('video')) {
case 'without':
if ($post['type'] === 'Animated') {
$AvoidElement = true;
}
break;
case 'only':
echo $post['type'];
if ($post['type'] !== 'Animated') {
$AvoidElement = true;
}
break;
case 'none': default:
break;
}
if (!$AvoidElement) {
$item['uri'] = $post['url'];
$item['title'] = $post['title'];
$item['content'] = self::getContent($post);
@@ -130,6 +157,7 @@ class NineGagBridge extends BridgeAbstract {
$this->items[] = $item;
}
}
}
public function getName() {
if ($this->getInput('d')) {

View File

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

View File

@@ -1,9 +1,9 @@
<?php
class WhydBridge extends BridgeAbstract {
class OpenwhydBridge extends BridgeAbstract {
const MAINTAINER = 'kranack';
const NAME = 'Whyd Bridge';
const URI = 'http://www.whyd.com/';
const NAME = 'Openwhyd Bridge';
const URI = 'https://openwhyd.org';
const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'Returns 10 newest music from user profile';
@@ -17,8 +17,7 @@ class WhydBridge extends BridgeAbstract {
private $userName = '';
public function getIcon() {
return self::URI . 'assets/favicons/
32-6b62a9f14d5e1a9213090d8f00f286bba3a6022381a76390d1d0926493b12593.png?v=6';
return self::URI . '/images/favicon.ico';
}
public function collectData(){
@@ -26,11 +25,11 @@ class WhydBridge extends BridgeAbstract {
if(strlen(preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))) == 24) {
// is input the userid ?
$html = getSimpleHTMLDOM(
self::URI . 'u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))
self::URI . '/u/' . preg_replace('/[^0-9a-f]/', '', $this->getInput('u'))
) or returnServerError('No results for this query.');
} else { // input may be the username
$html = getSimpleHTMLDOM(
self::URI . 'search?q=' . urlencode($this->getInput('u'))
self::URI . '/search?q=' . urlencode($this->getInput('u'))
) or returnServerError('No results for this query.');
for($j = 0; $j < 5; $j++) {
@@ -57,6 +56,6 @@ class WhydBridge extends BridgeAbstract {
}
public function getName(){
return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Whyd Bridge';
return (!empty($this->userName) ? $this->userName . ' - ' : '') . 'Openwhyd Bridge';
}
}

View File

@@ -3,7 +3,7 @@ class ParuVenduImmoBridge extends BridgeAbstract {
const MAINTAINER = 'polo2ro';
const NAME = 'Paru Vendu Immobilier';
const URI = 'http://www.paruvendu.fr';
const URI = 'https://www.paruvendu.fr';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the ads from the first page of search result.';

203
bridges/PatreonBridge.php Normal file
View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ class PickyWallpapersBridge extends BridgeAbstract {
const MAINTAINER = 'nel50n';
const NAME = 'PickyWallpapers Bridge';
const URI = 'http://www.pickywallpapers.com/';
const URI = 'https://www.pickywallpapers.com/';
const CACHE_TIMEOUT = 43200; // 12h
const DESCRIPTION = 'Returns the latests wallpapers from PickyWallpapers';

View File

@@ -32,6 +32,13 @@ class PikabuBridge extends BridgeAbstract {
'required' => true
),
'filter' => self::PARAMETERS_FILTER
),
'По пользователю' => array(
'user' => array(
'name' => 'Пользователь',
'exampleValue' => 'admin',
'required' => true
)
)
);
@@ -40,6 +47,8 @@ class PikabuBridge extends BridgeAbstract {
public function getURI() {
if ($this->getInput('tag')) {
return self::URI . '/tag/' . rawurlencode($this->getInput('tag')) . '/' . rawurlencode($this->getInput('filter'));
} else if ($this->getInput('user')) {
return self::URI . '/@' . rawurlencode($this->getInput('user'));
} else if ($this->getInput('community')) {
$uri = self::URI . '/community/' . rawurlencode($this->getInput('community'));
if ($this->getInput('filter') != 'hot') {
@@ -101,6 +110,10 @@ class PikabuBridge extends BridgeAbstract {
}
}
$img->outertext = '<img src="' . $src . '">';
// it is assumed, that img's parents are links to post itself
// we don't need them
$img->parent()->outertext = $img->outertext;
}
$categories = array();
@@ -116,7 +129,10 @@ class PikabuBridge extends BridgeAbstract {
$item['categories'] = $categories;
$item['author'] = $post->find('.user__nick', 0)->innertext;
$item['title'] = $title->plaintext;
$item['content'] = strip_tags(backgroundToImg($post->find('.story__content-inner', 0)->innertext), '<br><p><img>');
$item['content'] = strip_tags(
backgroundToImg($post->find('.story__content-inner', 0)->innertext),
'<br><p><img><a>
');
$item['uri'] = $title->href;
$item['timestamp'] = strtotime($time->getAttribute('datetime'));
$this->items[] = $item;

View File

@@ -30,7 +30,7 @@ class PinterestBridge extends FeedExpander {
private function fixLowRes() {
$newitems = [];
$newitems = array();
$pattern = '/https\:\/\/i\.pinimg\.com\/[a-zA-Z0-9]*x\//';
foreach($this->items as $item) {

View File

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

View File

@@ -0,0 +1,67 @@
<?php
/**
* PlantUML releases bridge showing latest releases content
* @author nicolas-delsaux
*
*/
class PlantUMLReleasesBridge extends BridgeAbstract
{
const MAINTAINER = 'Riduidel';
const NAME = 'PlantUML Releases';
const AUTHOR = 'PlantUML team';
// URI is no more valid, since we can address the whole gq galaxy
const URI = 'http://plantuml.com/fr/changes';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'PlantUML releases bridge, showing for each release the changelog';
const DEFAULT_DOMAIN = 'plantuml.com';
const PARAMETERS = array( array(
));
const REPLACED_ATTRIBUTES = array(
'href' => 'href',
'src' => 'src',
'data-original' => 'src'
);
private function getDomain() {
$domain = $this->getInput('domain');
if (empty($domain))
$domain = self::DEFAULT_DOMAIN;
if (strpos($domain, '://') === false)
$domain = 'https://' . $domain;
return $domain;
}
public function getURI()
{
return self::URI;
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI()) or returnServerError('Could not request ' . $this->getURI());
// Since GQ don't want simple class scrapping, let's do it the hard way and ... discover content !
$main = $html->find('div[id=root]', 0);
foreach ($main->find('h2') as $release) {
$item = array();
$item['author'] = self::AUTHOR;
$release_text = $release->innertext;
if (preg_match('/(.+) \((.*)\)/', $release_text, $matches)) {
$item['title'] = $matches[1];
// And now, build the date from the date text
$item['timestamp'] = strtotime($matches[2]);
}
$item['uri'] = $this->getURI();
$item['content'] = $release->next_sibling ();
$this->items[] = $item;
}
}
}

99
bridges/PornhubBridge.php Normal file
View File

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

View File

@@ -2,19 +2,18 @@
class RainbowSixSiegeBridge extends BridgeAbstract {
const MAINTAINER = 'corenting';
const NAME = 'Rainbow Six Siege Blog';
const URI = 'https://rainbow6.ubisoft.com/siege/en-us/news/';
const NAME = 'Rainbow Six Siege News';
const URI = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege/news-updates';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Latest articles from the Rainbow Six Siege blog';
const DESCRIPTION = 'Latest news about Rainbow Six Siege';
public function getIcon() {
return 'https://ubistatic19-a.akamaihd.net/resource/en-us/game/rainbow6/siege-v3/r6s-favicon_316592.ico';
return 'https://static-dm.akamaized.net/siege/prod/favicon-144x144.png';
}
public function collectData(){
$dlUrl = 'https://prod-tridionservice.ubisoft.com/live/v1/News/Latest?templateId=tcm%3A152-7677';
$dlUrl .= '8-32&pageIndex=0&pageSize=10&language=en-US&detailPageId=tcm%3A150-194572-64';
$dlUrl .= '&keywordList=233416%2C316144%2C233418%2C233417&siteId=undefined&useSeoFriendlyUrl=true';
$dlUrl = 'https://www.ubisoft.com/api/updates/items?categoriesFilter=all';
$dlUrl = $dlUrl . '&limit=6&mediaFilter=all&skip=0&startIndex=undefined&locale=en-us';
$jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
$json = json_decode($jsonString, true);
@@ -22,17 +21,47 @@ class RainbowSixSiegeBridge extends BridgeAbstract {
// Start at index 2 to remove highlighted articles
for($i = 0; $i < count($json); $i++) {
$jsonItem = $json[$i]['Content'];
$article = str_get_html($jsonItem);
$jsonItem = $json[$i];
$uri = 'https://www.ubisoft.com/en-us/game/rainbow-six/siege';
$uri = $uri . $jsonItem['button']['buttonUrl'];
$thumbnail = '<img src="' . $jsonItem['thumbnail']['url'] . '" alt="Thumbnail">';
$content = $thumbnail . '<br />' . $jsonItem['content'];
// Markdown parsing from https://gist.github.com/jbroadway/2836900
// Line breaks
$content = preg_replace("/\r\n|\r|\n/", '<br/>', $content);
// Links
$regex = '/\[([^\[]+)\]\(([^\)]+)\)/';
$replacement = '<a href=\'\2\'>\1</a>';
$content = preg_replace($regex, $replacement, $content);
// Bold text
$regex = '/(\*\*|__)(.*?)\1/';
$replacement = '<strong>\2</strong>';
$content = preg_replace($regex, $replacement, $content);
// Lists
$regex = '/\n\s*[\*|\-](.*)/';
$content = preg_replace_callback($regex, function($regs) {
$item = $regs[1];
return sprintf ('<ul><li>%s</li></ul>', trim ($item));
}, $content);
// Italic text
$regex = '/(\*\*|\*)(.*?)\1/';
$replacement = '<i>\2</i>';
$content = preg_replace($regex, $replacement, $content);
$item = array();
$uri = $article->find('h3 a', 0)->href;
$uri = 'https://rainbow6.ubisoft.com' . $uri;
$item['uri'] = $uri;
$item['title'] = $article->find('h3', 0)->plaintext;
$item['content'] = $article->find('img', 0)->outertext . '<br />' . $article->find('strong', 0)->plaintext;
$item['timestamp'] = strtotime($article->find('p.news_date', 0)->plaintext);
$item['id'] = $jsonItem['id'];
$item['title'] = $jsonItem['title'];
$item['content'] = $content;
$item['timestamp'] = strtotime($jsonItem['date']);
$this->items[] = $item;
}

View File

@@ -1,44 +0,0 @@
<?php
class ReadComicsBridge extends BridgeAbstract {
const MAINTAINER = 'niawag';
const NAME = 'Read Comics';
const URI = 'http://www.readcomics.tv/';
const DESCRIPTION = 'Enter the comics as they appear in the website uri,
separated by semicolons, ex: good-comic-1;good-comic-2; ...';
const PARAMETERS = array( array(
'q' => array(
'name' => 'keywords, separated by semicolons',
'exampleValue' => 'first list;second list;...',
'required' => true
),
));
public function collectData(){
function parseDateTimestamp($element){
$guessedDate = $element->find('span', 0)->plaintext;
$guessedDate = strptime($guessedDate, '%m/%d/%Y');
$timestamp = mktime(0, 0, 0, $guessedDate['tm_mon'] + 1, $guessedDate['tm_mday'], date('Y'));
return $timestamp;
}
$keywordsList = explode(';', $this->getInput('q'));
foreach($keywordsList as $keywords) {
$html = $this->getSimpleHTMLDOM(self::URI . 'comic/' . rawurlencode($keywords))
or $this->returnServerError('Could not request readcomics.tv.');
foreach($html->find('li') as $element) {
$item = array();
$item['uri'] = $element->find('a.ch-name', 0)->href;
$item['id'] = $item['uri'];
$item['timestamp'] = parseDateTimestamp($element);
$item['title'] = $element->find('a.ch-name', 0)->plaintext;
if(isset($item['title']))
$this->items[] = $item;
}
}
}
}

40
bridges/RedditBridge.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
class RedditBridge extends FeedExpander {
const MAINTAINER = 'leomaradan';
const NAME = 'Reddit Bridge';
const URI = 'https://www.reddit.com/';
const DESCRIPTION = 'Reddit RSS Feed fixer';
const PARAMETERS = array(
'single' => array(
'r' => array(
'name' => 'SubReddit',
'required' => true,
'exampleValue' => 'selfhosted',
'title' => 'SubReddit name'
)
),
'multi' => array(
'rs' => array(
'name' => 'SubReddits',
'required' => true,
'exampleValue' => 'selfhosted, php',
'title' => 'SubReddit names, separated by commas'
)
)
);
public function collectData(){
switch($this->queriedContext) {
case 'single': $subreddits[] = $this->getInput('r'); break;
case 'multi': $subreddits = explode(',', $this->getInput('rs')); break;
}
foreach ($subreddits as $subreddit) {
$name = trim($subreddit);
$this->collectExpandableDatas("https://www.reddit.com/r/$name/.rss");
}
}
}

View File

@@ -9,22 +9,6 @@ class Releases3DSBridge extends BridgeAbstract {
public function collectData(){
function typeToString($type){
switch($type) {
case 1: return '3DS Game';
case 4: return 'eShop';
default: return '??? (' . $type . ')';
}
}
function cardToString($card){
switch($card) {
case 1: return 'Regular (CARD1)';
case 2: return 'NAND (CARD2)';
default: return '??? (' . $card . ')';
}
}
$dataUrl = self::URI . 'xml.php';
$xml = getContents($dataUrl)
or returnServerError('Could not request 3dsdb: ' . $dataUrl);
@@ -95,8 +79,8 @@ class Releases3DSBridge extends BridgeAbstract {
. '<br /><b>Release Name: </b>' . $releasename
. '<br /><b>Trimmed size: </b>' . intval(intval($trimmedsize) / 1048576)
. 'MB<br /><b>Firmware: </b>' . $firmware
. '<br /><b>Type: </b>' . typeToString($type)
. '<br /><b>Card: </b>' . cardToString($card)
. '<br /><b>Type: </b>' . $this->typeToString($type)
. '<br /><b>Card: </b>' . $this->cardToString($card)
. '<br />';
//Build search links section to facilitate release search using search engines
@@ -124,4 +108,20 @@ class Releases3DSBridge extends BridgeAbstract {
$limit++;
}
}
private function typeToString($type){
switch($type) {
case 1: return '3DS Game';
case 4: return 'eShop';
default: return '??? (' . $type . ')';
}
}
private function cardToString($card){
switch($card) {
case 1: return 'Regular (CARD1)';
case 2: return 'NAND (CARD2)';
default: return '??? (' . $card . ')';
}
}
}

View File

@@ -3,7 +3,7 @@ class ReporterreBridge extends BridgeAbstract {
const MAINTAINER = 'nyutag';
const NAME = 'Reporterre Bridge';
const URI = 'http://www.reporterre.net/';
const URI = 'https://www.reporterre.net/';
const DESCRIPTION = 'Returns the newest articles.';
private function extractContent($url){

View File

@@ -25,7 +25,7 @@ class RoadAndTrackBridge extends BridgeAbstract {
private function fixImages($content) {
$enclosures = [];
$enclosures = array();
foreach($content->find('img') as $image) {
$image->src = explode('?', $image->getAttribute('data-src'))[0];
$enclosures[] = $image->src;

View File

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

View File

@@ -5,7 +5,7 @@ class Rule34Bridge extends GelbooruBridge {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Rule34';
const URI = 'http://rule34.xxx/';
const URI = 'https://rule34.xxx/';
const DESCRIPTION = 'Returns images from given page';
const PIDBYPAGE = 50;

View File

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

View File

@@ -5,7 +5,7 @@ class SafebooruBridge extends GelbooruBridge {
const MAINTAINER = 'mitsukarenai';
const NAME = 'Safebooru';
const URI = 'http://safebooru.org/';
const URI = 'https://safebooru.org/';
const DESCRIPTION = 'Returns images from given page';
const PIDBYPAGE = 40;

View File

@@ -3,7 +3,7 @@ class ScmbBridge extends BridgeAbstract {
const MAINTAINER = 'Astalaseven';
const NAME = 'Se Coucher Moins Bête Bridge';
const URI = 'http://secouchermoinsbete.fr';
const URI = 'https://secouchermoinsbete.fr';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns the newest anecdotes.';

View File

@@ -3,7 +3,7 @@ class ScoopItBridge extends BridgeAbstract {
const MAINTAINER = 'Pitchoule';
const NAME = 'ScoopIt';
const URI = 'http://www.scoop.it/';
const URI = 'https://www.scoop.it/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns most recent results from ScoopIt.';

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