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

Compare commits

...

222 Commits

Author SHA1 Message Date
Eugene Molotov
b24b5ed3ee Bump version to dev.2021-04-25 2021-04-25 15:32:35 +05:00
Eugene Molotov
f06a8ae307 [README] Update list of contributors 2021-04-25 15:30:17 +05:00
dawidsowa
4f7ef212b7 [RedditBridge] Add detectParameters (#2070) 2021-04-19 22:17:36 +05:00
dawidsowa
13e9a96cf3 [RedditBridge]: Add score filter (#2045) 2021-04-19 22:14:35 +05:00
ORelio
00a24a98be [NyaaTorrents] Rewrite as Feed Expander (#2073)
NyaaTorrents allows search criteria as URL parameters in RSS feed so we just need to expand feed items
2021-04-19 21:59:51 +05:00
FiveFilters.org
76c38332ee [TwitterBridge] Improve timeline processing for username mode (#1946) 2021-04-12 23:08:38 +05:00
Joseph
65be209a47 [TwitScoopBridge] Remove less than (<) character from item title (#2034) 2021-04-12 23:01:46 +05:00
sysadminstory
146639ffc9 [ZoneTelechargement] Update unprotected URL and Feed URL (#2065)
The unproteced URL has changed again, and has been updated.

The RSS Feed URL is now a link to the Show page and not to the Homepage anymore.
2021-04-12 23:01:09 +05:00
sysadminstory
e1c19461ca [ExtremeDownloadBridge] Feed URL updated (#2066)
The Feed URL is now a link to the TV Show and not the Homepage !
2021-04-12 22:59:16 +05:00
Harvey Christian Pacleb
ff0c7a9013 [GenshinImpactBridge] Use Asia/Shanghai time zone for article dates (#2040) 2021-04-10 13:35:34 +05:00
ORelio
b754d14698 [FeedExpander] Handle Atom enclosures (#2039) 2021-04-04 15:21:15 +05:00
sysadminstory
3423b3bbe1 [ZoneTelechargement] Change URL load method (#2044) 2021-04-04 14:31:48 +05:00
Joseph
5966cc0a9c [TheFarSideBridge] Add bridge (#1484) 2021-04-03 23:31:49 +05:00
Joseph
579bfa669c [WallmineNewsBridge] Add bridge (#2035) 2021-04-02 18:01:51 +05:00
ORelio
d61871a45e [NyaaTorrents] Allow searching by username (#2033) 2021-03-31 10:59:31 +05:00
Eugene Molotov
0c8fabeb11 [PikabuBridge] Marking posts from "Как бы Новости" section, which are funny and deliberately fake (#2032) 2021-03-30 23:06:23 +05:00
Matthieu Rakotojaona
40c84b5dc3 [HackerNewsUserThreads] New bridge (#1902) 2021-03-30 21:56:17 +05:00
Yaman Qalieh
6f75d07456 [GitHubPullRequestBridge] Add new bridge inheriting GithubIssueBridge (#2001) 2021-03-29 22:15:56 +05:00
guigot
b4f809aa44 [MondeDiploBridge] Fix blog article uri (#1961) 2021-03-25 22:15:02 +05:00
Joseph
bcecd70df7 [DownDetectorBridge] Fix bridge (#1957) 2021-03-25 22:02:45 +05:00
Joseph
a6c0874b9a [TwitScoopBridge] Fix encoding of less than character (<) (#2023) 2021-03-21 11:39:01 +05:00
somini
9e6f063cfd [PresidenciaPT]: New Bridge (#2016) 2021-03-17 21:30:47 +05:00
Joseph
f904353fd2 [InternetArchiveBridge] Fix collection links (#1551) 2021-03-16 18:07:04 +05:00
Andrea Draghetti
3aafd44079 [TelegramBridge] Display the name of the attachments (#2003)
Sometimes attachments are posted in Telegram channels without any text. 

The script recognizes a new message but does not report any text, with this commit the file names will also be included in the RSS Feed.
2021-03-16 18:04:07 +05:00
Joseph
75b85f61e7 [BandcampBridge] Fix title extraction on empty band pages (#1966) 2021-03-16 18:00:26 +05:00
Joseph
07e1e8497c [DockerHubBridge] Add detectParameters() (#1996) 2021-03-15 21:54:26 +05:00
Joseph
700813e924 [FirefoxAddonsBridge] Add detectParameters() (#1997) 2021-03-15 21:27:53 +05:00
Joseph
5c011c8d90 [TwitScoopBridge] Add bridge (#2018) 2021-03-15 21:20:02 +05:00
Joseph
8d0d08a4d8 [YeggiBridge] Fix lint error (#2019) 2021-03-15 21:15:01 +05:00
Antoine Turmel
55548dcb5f [YeggiBridge] New bridge (#1910) 2021-03-13 21:57:30 +05:00
Justin Goette
0217b270a7 [README] Fix typo (#2004) 2021-03-10 22:13:41 +05:00
Joseph
2ed34f5ebe [FirefoxAddonsBridge] Set unique id for items (#2007)
Adds unique id for each item using item title.
2021-03-09 11:16:56 +05:00
csisoap
2448ed41c9 [ReutersBridge] Add new wireitem template type (#2006)
and retain the list of parameters
2021-03-09 11:15:48 +05:00
Joseph
b25674b3a0 [HtmlFormat] Use str_ireplace() when creating feed format URLs (#2008)
Fixes feed format URLs not being created with correct format value if html feed URL uses a lowercase format value.
2021-03-08 12:17:12 +05:00
Joseph
2ce1a6365b [README] Update build status badges (#1995) 2021-02-28 18:41:03 +05:00
Joseph
30aeeb2a0c [DockerHubBridge] Add support for official images (#1999) 2021-02-28 18:26:24 +05:00
Joseph
c294a652a3 [TelegramBridge] Add detectParameters() (#1998) 2021-02-28 18:20:44 +05:00
Park0
a5f2175531 [SymfonyCasts] Added new bridge (#2000) 2021-02-28 18:17:54 +05:00
dawidsowa
569276f4ef [RedditBridge]: Add user option (#1943) 2021-02-23 12:08:43 +05:00
Joseph
687eb728d4 [DockerHubBridge] Fix bridge name (#1994) 2021-02-22 15:03:04 +00:00
Monocularity
0521ba5873 [NordbayernBridge] Fixed typo of region "Hilpoltstein" (#1962) 2021-02-21 16:43:23 +00:00
Joseph
3d642971c0 [FirefoxAddonsBridge] Add bridge (#1952) 2021-02-21 17:30:01 +05:00
Shikiryu
8f086169cc [TheYeteeBridge] Fix HTML parsing (#1986) 2021-02-21 15:19:20 +05:00
Joseph
ce34e7eb89 [DockerhubBridge] Add bridge (#1990) 2021-02-21 15:17:07 +05:00
sysadminstory
ee5d190391 [RadioMelodieBridge] Fix header image (#1985)
Header Image is now using a direct link to the image, but without the
website base URL : the bridge now sends the right URL.
2021-02-17 10:07:35 +05:00
Eugene Molotov
98352845a1 [VkBridge] Remove non ascii chars from post date to correctly parse it (#1977) 2021-02-10 18:11:48 +05:00
Joseph
9e58735b01 [FormatFactory] Ignore case in format values (#1967) 2021-02-09 18:13:03 +05:00
Thomas
771b851b52 [contents.php] Fix logical error in getSimpleHTMLDOMCached function (#1974)
Previously content was only loaded from cache when debug mode was enabled (the opposite of the expected behavior)
2021-02-09 17:40:16 +05:00
Joseph
809343ed06 [SoundcloudBridge] Fix client ID extraction (#1973) 2021-02-09 17:33:14 +05:00
Lyra
e9424f6a08 Merge branch 'master' of github.com:RSS-Bridge/rss-bridge 2021-02-07 14:44:36 +01:00
Lyra
e5846c03ba Attempt to fix LeBonCoinBridge 2021-02-07 14:44:28 +01:00
Joseph
6224fbb6a2 [BridgeCard] Display configuration options (if enabled) when bridge has no parameters (#1968)
Updates displayBridgeCard() in BridgeCard to allow configuration options noproxy and cache_timeout to be displayed, if enabled, when a bridge has no parameters in its PARAMETERS array
2021-02-05 10:17:30 +05:00
sysadminstory
eab575dc9d [ZoneTelechargement] Update Direct Download Unprotected URL (#1963) 2021-01-30 18:02:36 +00:00
André Andersson
b56637c833 [BukowskisBridge] Add bridge (#1927) 2021-01-29 23:36:45 +05:00
Simon816
005b22701d [FSecureBlogBridge] Add bridge (#1932) 2021-01-29 23:27:35 +05:00
hollowleviathan
43b7621f45 [ReutersBridge] Add bridge (#1653) 2021-01-29 22:57:40 +05:00
Joseph
ea289a0cea [GithubIssueBridge] Fix issue id and comment id extraction (#1950) 2021-01-27 11:06:59 +05:00
Joseph
43acb555e0 [ChristianDailyReporterBridge] Remove bridge (#1948)
> The time has come for the Christian Daily Reporter to ride off into the sunset. Disrn is here now and it delivers everything CDR did, times ten.

https://christiandailyreporter.com/
2021-01-22 10:20:17 +05:00
Eugene Molotov
3b7e61fb55 [Arte7Bridge] Mitsu is no longer maintainer of the bridge
> That said, please also feel free to remove me as "maintainer" since that's simply no longer reality

https://github.com/RSS-Bridge/rss-bridge/issues/1906#issuecomment-765015048
2021-01-22 09:52:07 +05:00
Corentin Garcia
fbbd6a02c6 [DribbbleBridge] Fix pictures parsing (#1911) 2021-01-20 22:26:15 +05:00
sysadminstory
3534193032 [ZoneTelechargement] Update URL and fix typos (#1936) 2021-01-17 16:32:59 +05:00
sysadminstory
81fc8c89d4 [ExtremeDownloadBridge] Update URL (#1937) 2021-01-17 16:31:44 +05:00
Eugene Molotov
ea1de07fe5 [CI] Fix non-working phpcompatibility job (#1928) 2021-01-12 12:02:49 +05:00
Eugene Molotov
2de5ce8387 [CI] Fix phpcompatibility job running wrong scenario 2021-01-12 11:00:38 +05:00
Eugene Molotov
28f9215913 [CI] Replace Travis CI with Github Actions (#1926)
Travis stopped working again and no answers from support
2021-01-11 22:39:15 +05:00
FiveFilters.org
f927781750 [TwitterBridge] Add option to hide pinned tweet (#1908) 2021-01-10 13:50:06 +05:00
Jacques Heunis
e128ce807a [ItchioBridge] Add bridge (#1918) 2021-01-10 13:30:12 +05:00
Derrick Lee
6b870f0c3e [TwitterBridge] Fix username matching to be case insensitive with noretweet option (#1924) 2021-01-10 13:19:38 +05:00
JimDog546
5ed161943c [InstagramBridge] Remove redundant data collection for sidecar and video (#1920)
getInstagramSidecarData and getInstagramVideoData were unnecessarily calling getSinglePostData to retrieve data already present in collectData's call of getInstagramJSON. getSinglePostData sometimes doesn't retrieve data properly resulting in incomplete post information. Since the information needed is already present, pass it from collectData instead, eliminating the redundant data collection and improving reliability.
2021-01-10 13:14:58 +05:00
Corentin Garcia
1edec1aa45 [GenshinImpactBridge] Add bridge (#1912) 2020-12-30 23:30:02 +05:00
dawidsowa
3c285d50ec [RedditBridge] Rewrite to use JSON (#1781) 2020-12-23 22:42:15 +05:00
Clemens Neubauer
3aae00b56a [HDWallpapersBridge] Fix URLs (#1892) 2020-12-23 22:19:32 +05:00
sysadminstory
21798e8228 [ZoneTelechargement] Add support for Streaming links (#1858) 2020-12-23 22:13:10 +05:00
Eugene Molotov
3226a5e31e [core] Use more correct text to indicate errored response from upstream 2020-12-23 17:49:11 +05:00
Eugene Molotov
59bbc9d2e7 [VkBridge] Several improvements (#1802)
* Improved post author computation
* Show repost sources
* Handle second copy quote
* Fixed incorrect image uri's
2020-12-18 07:58:51 +05:00
Eugene Molotov
2ddd357a62 [JS] Fixed TypeError: textValue is null 2020-12-18 07:51:33 +05:00
Devon Hess
c302bca1e6 [IKWYDBridge] New bridge (#1874) 2020-12-13 16:08:25 +05:00
t0stiman
0b3082609d [RaceDepartmentBridge] added a bridge for racedepartment.com (#1681) 2020-12-13 15:50:54 +05:00
Lyra
810a2503c9 [core] Add configuration for bridges, allowing private bridges (#1343) 2020-12-12 21:05:22 +05:00
Eugene Molotov
56b2c516e4 [InstagramBridge] pauder is defacto not a maintainer of InstagramBridge 2020-12-09 10:47:48 +05:00
Eugene Molotov
fc81bed717 [BridgeAbstract] Correct getIcon method 2020-12-07 21:08:58 +04:00
Tobias Alexander Franke
56eb829a66 [EconomistBridge] Fixes for fetching new page structure (#1836) 2020-11-29 15:31:20 +05:00
Alex Kirk
7705d097e3 [SkimfeedBridge] Add parameter detection (#1877) 2020-11-29 15:28:07 +05:00
Peter Dave Hello
be9df41e07 [Dockerfile] Clean up apt cache, make image smaller (#1880) 2020-11-29 15:18:20 +05:00
Joseph
1e75f9d3d5 [ReporterreBridge + KernelBugTrackerBridge + BastaBridge] Use defaultLinkTo() (#1862) 2020-11-23 23:49:25 +05:00
Eugene Molotov
0755181555 [CeskaTelevizeBridge] Remove executable flag from bridge file 2020-11-19 16:19:46 +05:00
Joseph
e6c73a1fe3 [FlickrBridge] Add filter by media and sort by options (#1758) 2020-11-16 22:33:48 +05:00
David Pedersen
5729e069e9 [AppleMusicBridge] Use title from website (#1855) 2020-11-16 22:15:39 +05:00
David Pedersen
0b494d9c0e [AmazonPriceTrackerBridge] Add support for Swedish Amazon (#1856) 2020-11-16 22:13:23 +05:00
Eugene Molotov
c855d5089f Revert "[TelegramBridge] Prevent double encoding entities (#1182)"
This reverts commit 5e2f0fb626.
2020-11-11 23:31:24 +05:00
dawidsowa
6baf64f29e [Rule34pahealBridge] Use full size image (#1775) 2020-11-11 23:28:12 +05:00
Alex Balgavy
7d4b76be99 [SeznamZpravyBridge] New bridge (#1806) 2020-11-11 22:39:34 +05:00
Eugene Molotov
ff50e4918c Bump version to dev.2020-11-10 2020-11-10 16:26:08 +05:00
Eugene Molotov
8e4d6d8fdb [README] Update list of contributors 2020-11-10 16:24:58 +05:00
ayacoo
cc548b16a8 [HeiseBridge] Check for article (#1790) 2020-11-10 11:14:09 +05:00
Roman Remizov
b66026e241 [TwitterBridge] Add support for querying by list ID (#1834) 2020-11-10 11:12:02 +05:00
Joseph
a23d4bd0e6 [BrutBridge] Add support for Spain edition and health category (#1833) 2020-11-09 18:14:13 +05:00
ymeister
bde4159a9e [README] Add "filter" to Requirements (#1827)
This extension is required by filter_var()
2020-11-08 12:25:06 +05:00
Niehztog
3ad138026d [BridgeXPathAbstract + BlizzardNewsBridge + XPathBridge] Add new abstract class + two example implementations (#1671) 2020-11-08 12:22:41 +05:00
Joshua Coales
d05a8b79fe [contents.php] Fix return type hints (#1824) 2020-11-08 12:19:18 +05:00
Joshua Coales
efe32aad22 [FacebookBridge] Permalink fix (#1838)
Also removing timestamp which does not work
2020-11-06 18:43:10 +05:00
Petr Kolář
0655b3cb39 [MallTvBridge] New bridge (#1819) 2020-10-31 22:05:13 +05:00
csisoap
59082368c7 [BleepingComputerBridge] New bridge (#1815) 2020-10-31 22:01:19 +05:00
msch
c8b2c1bf74 [OpenwrtSecurityBridge] Add new bridge (#1812) 2020-10-31 21:57:29 +05:00
Roliga
b48bc77c22 [TwitchBridge] Switch to unofficial GraphQL API (#1829)
* [TwitchBridge] Switch to unofficial GraphQL API

The GraphQL API that the twitch.tv website uses has a lot more
information available than the official APIs. Hopefully it'll be stable.
2020-10-30 13:50:36 +00:00
Joshua Coales
6af87b2f32 [FacebookBridge] Use touch.facebook.com for groups (#1817) 2020-10-29 08:42:49 +05:00
Eugene Molotov
93cdf5e342 [core] Fixed passive XSS vulnerability
Reference: https://www.openbugbounty.org/reports/1140367/
2020-10-26 15:08:11 +05:00
Joseph
164b407f28 [BridgeCard] Fix parameter layout issue (#1816)
Fixes parameter layout issue on small screens.
2020-10-26 12:11:58 +05:00
ORelio
2714c3d816 [WordPress] Limit feed to 20 items (#1801) 2020-10-21 14:59:04 +05:00
Eugene Molotov
364b5282a3 [GoogleSearch] Use other class for content retreiving (#1803) 2020-10-19 16:22:37 +05:00
Eugene Molotov
5e4f3c351e [NineGagBridge] Lint previous commit 2020-10-15 14:18:46 +05:00
Gregor Santner
a332a5a414 [NineGagBridge] In post URI replace scheme from "http" to "https" 2020-10-15 14:12:54 +05:00
Joshua Coales
45e2f385b3 [FacebookBridge] Handle mobile links and unify host validation (#1789) 2020-10-15 14:08:03 +05:00
Joseph
0a1ff10a52 [KoreusBridge + VarietyBridge] Use HTTPS when fetching feedburner feeds (#1797) 2020-10-15 13:03:51 +05:00
Eugene Molotov
645a8f62c6 [.travis] Fix several phpcs and phpunit errors (#1799) 2020-10-15 12:53:19 +05:00
ORelio
64ec488f70 [ZDNet] Fix article layout (#1793) 2020-10-13 21:46:58 +05:00
Petr Kolář
7b6ff78623 [CeskaTelevizeBridge] Add New bridge (#1784) 2020-10-12 11:35:06 +05:00
Corentin Garcia
82acbbb421 [DribbbleBridge] Fix picture parsing (#1787) 2020-10-10 00:46:40 +05:00
Ololbu
84d5daaa03 [FicbookBridge] Add getName implementation (#1771) 2020-10-10 00:39:35 +05:00
Joseph
712f60e910 [HeiseBridge] Fix multi-page article fetching (#1767)
Fixes multi-page article fetching by adding '&seite=all' to  article URL.
2020-10-09 23:55:28 +05:00
Alexander
55015f80cf [AlbionOnlineBridge] New bridge (#1769) 2020-10-09 23:48:40 +05:00
csisoap
f90c6b5bb9 [NasaApodBridge] Fix broken image link (#1778) 2020-10-09 23:33:54 +05:00
Corentin Garcia
ff98efe8dc [core] Use Parsedown for Markdown parsing (#1783) 2020-10-09 23:29:02 +05:00
Corentin Garcia
fe166d0216 [NasaApodBridge] Fix header being parsed as item (#1586) 2020-10-07 11:16:26 +05:00
triatic
d3455dd18a [TwitterBridge] Optimise regular expression code (#1768)
* [TwitterBridge] Optimise regular expression code

Optimise regular expression search code so adding new URLs is cleaner
2020-10-05 12:07:39 +05:00
ORelio
47dc26c775 [NextINpact] Fix subtitle extraction in #LeBrief (#1780)
The bridge was taking another article abstract as subtitle for #LeBrief articles
2020-10-05 11:57:13 +05:00
Ololbu
3df2de4c6f [FicbookBridge] Fix data getting and months (#1765) 2020-09-28 14:02:40 +05:00
sarnd
01985b7af7 [TwitterBridge] URL to js file with apikey changed again (#1764) 2020-09-28 10:01:37 +05:00
Joseph
80cc88ba78 [SoundcloudBridge] Fix bridge not returning tracks (#1757)
+ Use artwork for enclosure
2020-09-25 11:43:12 +05:00
Christian Schabesberger
2bb99c4448 [NordbayernBridge] Fix images and newsblock order (#1741) 2020-09-18 10:13:31 +05:00
Jason Ghent
3a29347e60 [ParameterValidator] Ignore cache-busting param (#1723) 2020-09-14 14:01:55 +05:00
Alexander
d299adb827 [EpicgamesBridge] Add pinned posts to list (#1736) 2020-09-14 13:14:35 +05:00
Michael Bemmerl
cf606a3a6b [OtrkeyFinderBridge] Add bridge for otrkeyfinder.com (#1712) 2020-09-11 11:48:03 +05:00
Eugene Molotov
6c244f4d9b [TwitterBridge] Skip advertisment tweets (#1673) 2020-09-11 11:44:28 +05:00
AxorPL
d6f277d029 [WorldCosplayBridge] Add new bridge (#1732) 2020-09-09 17:11:19 +05:00
sysadminstory
ab8e89a97f [AllocineFRBridge] Update CSS class (#1585)
Website has change one CSS class : updated the bridge to allow parsing.
2020-09-08 10:55:21 +05:00
Joseph
747bb6ad9c [WosckerBridge] Add bridge (#1643) 2020-09-03 11:18:15 +05:00
Joseph
d33e090fe1 [MastodonBridge] Update feed URL format (#1718)
Changes feed URL from `https://instance/users/username.atom` to `https://instance/@username.rss`.
2020-09-03 10:49:19 +05:00
Christian Jonak
fec52418d5 [FM4Bridge] Add new bridge for FM4 news page (#1719) 2020-09-03 10:46:35 +05:00
ggiessen
bb51a0d212 [MarktplaatsBridge] Improvements (#1722)
- sometimes $listing->imageUrls is empty so moved after the if statement on line 91 
- added price and location info
- added function getName
2020-09-03 10:44:32 +05:00
Bob
68dd2d745f [InstagramBridge] Change TAG_QUERY_HASH (#1727) 2020-09-02 11:02:34 +05:00
ORelio
46abc18e87 [Anidex] Fix content retrieval (#1693)
Anidex uses two separate domains, anidex.info and anidex.moe
anidex.info has ddos-guard so we need to request
anidex.moe with Host header set to anidex.info
2020-08-31 22:04:56 +05:00
ORelio
e00bbe353f * [ReleasesSwitch] Switch scene releases (#1694)
Separate bridge from Releases3DS that just has a different URL.
Inherits from Releases3DS so both bridges need to be present.

*  [Releases3DS] Fix PHP notices related to IGN
2020-08-31 22:02:25 +05:00
somini
c21a805cb4 [DiarioDeNoticiasBridge]: New Bridge (#1717) 2020-08-27 10:38:51 +05:00
Simon Alberny
3b36c413e5 [MondeDiplo] Switched to HTTPS + Title and content updated (#1714) 2020-08-27 10:28:59 +05:00
ggiessen
94576c3053 [MarktplaatsBridge] 'https:' added to img src url (#1713) 2020-08-24 10:30:59 +05:00
ronansalmon
25cff9c07b [TwitterBridge] Convert plain text URLs into HTML hyperlinks (#1627) 2020-08-21 17:55:11 +05:00
ORelio
07c71b3b36 [NextINpact] Upgrade for NextINpact v7 (#1708) 2020-08-20 17:49:26 +05:00
Alexander
859053ef7a [EpicgamesBridge] New bridge (#1709) 2020-08-20 10:36:11 +05:00
triatic
73287f536b [TwitterBridge] Add retweeter to retweeted tweets (#1679) 2020-08-20 10:00:27 +05:00
sysadminstory
0b1e592a5e [ZoneTelechargement] Update URL (#1710)
The bridge now shows links to the new URL.

It keeps the old one internally to bypass the Robot protection on the
new URL.
2020-08-19 17:35:19 +05:00
jannyba
ef54a78430 [InstagramBridge] Fix "Skip reviews" checkbox description (#1702) 2020-08-16 11:23:48 +05:00
Eugene Molotov
4b8c3b9d36 [Multi] Minor improvements for my bridges (#1507)
* [DarkReading] Hide dummy articles

* [FuturaSciences] Strip inline scripts from content

* [FeedExpander] Fix PHP notice on missing uri field

(guid is valid uri AND item uri is not valid)
 => (guid is valid uri AND item uri is empty or not valid)

* [NextInpact] Fix subtitle extraction

* [Markdown] Fix images with empty replacement text

* [TheHackerNews] Fix Author name cleanup

* [LeMondeInformatique] Remove encoding conversion

Was previously needed due to actual encoding on the page
being inconsistent with encoding specified in <meta> tag

* [AnimeUltime] Remove encoding conversion

Was previously needed due to encoding on the page being incorrect

* [FuturaSciences] Fix content extraction

* [FuturaSciences] Fix unneeded unset()

* [GBAtemp] Fix tutorial mode URL extraction

* [GBAtemp] Fix tutorial mode Title extraction
2020-08-14 10:30:31 +05:00
ORelio
c642652fea [GBAtemp] Fix tutorial mode Title extraction 2020-08-12 20:08:24 +02:00
ORelio
f0e6298cab [GBAtemp] Fix tutorial mode URL extraction 2020-08-12 20:08:24 +02:00
ORelio
45e247b9d0 [FuturaSciences] Fix unneeded unset() 2020-08-12 20:08:24 +02:00
ORelio
66a009b8fb [FuturaSciences] Fix content extraction 2020-08-12 20:08:24 +02:00
ORelio
efd1abfab1 [AnimeUltime] Remove encoding conversion
Was previously needed due to encoding on the page being incorrect
2020-08-12 20:08:24 +02:00
ORelio
8b173b8874 [LeMondeInformatique] Remove encoding conversion
Was previously needed due to actual encoding on the page
being inconsistent with encoding specified in <meta> tag
2020-08-12 20:08:24 +02:00
ORelio
90e9c9962a [TheHackerNews] Fix Author name cleanup 2020-08-12 20:08:24 +02:00
ORelio
01cc32a0cc [Markdown] Fix images with empty replacement text 2020-08-12 20:08:24 +02:00
Joseph
dc36b425cd [DevToBridge] Fix bridge (#1699)
Fixes full article option not working
2020-08-12 23:07:53 +05:00
triatic
268ddf1382 [TwitterBridge] URL to js file with apikey changed (#1698)
Fixes #1697
2020-08-12 10:32:34 +05:00
ORelio
8144488a9e [FeedExpander] Fix PHP notice on missing uri field
(guid is valid uri AND item uri is not valid)
 => (guid is valid uri AND item uri is empty or not valid)
2020-08-11 14:01:44 +02:00
ORelio
062dd7f8a5 [FuturaSciences] Strip inline scripts from content 2020-08-11 14:01:44 +02:00
Nemo
be089702f0 [docker] Install memcached PHP extension from PECL (#1473)
- Adapted from https://stackoverflow.com/a/41575677
- Tested by running `php -m`
- SimpleXML now comes pre-installed with PHP, so removed the extra step
- Adds --no-install-recommends

Signed-off-by: Nemo <me@captnemo.in>
2020-08-07 18:21:28 +05:00
sarnd
c71fad4a4a [TwitterBridge] URL to js file with apikey changed. (#1686)
Twitter has changed URL scheme back again (see PR#1647 / commit 78298385d0)

This patch will try both URL schemes now and throw a specific error when neither works
2020-08-06 10:22:17 +05:00
Eugene Molotov
5be251a66e [TwitterBridge] Fetch latest tweets on hashtag or keyword query (#1674) 2020-08-02 19:40:41 +05:00
Eugene Molotov
7709b8d662 [VkBridge] Correct fallback behavior, when trying to get direct video links (#1670) 2020-07-31 15:29:18 +05:00
triatic
f5916a2f74 [TwitterBridge] apikey fetched every time (#1663)
The apikey is fetched every time because $data is not an array. Update the condition to expire the api key at the same time as the guest token.
2020-07-30 09:54:16 +05:00
Eugene Molotov
a33088ca99 [GoogleSearch] Correct parsing uri of search result (#1601) 2020-07-27 11:44:07 +05:00
somini
78facbcb83 [TwitterBridge] Fix noretweet for users (#1608)
This also removes spurious retweets.
2020-07-26 11:26:39 +05:00
Thomas
d5a75a2545 [DribbbleBridge] regex fix and CSS selector update (#1657)
* [DribbbleBridge] Fixed regular expressions for quote replacement in JSON (previously invalid JSON was created if a property value contained colons or single quotes). Also updated two CSS selectors as Dribbble's HTML has changed.

* [DribbbleBridge] Added fix for relative dates in JSON

* [DribbbleBridge] Removed redundant whitespaces
2020-07-25 08:58:42 +05:00
triatic
25698d182c [TwitterBridge] Remove unused variable 2020-07-24 13:13:21 +05:00
Corentin Garcia
9e74cc64ed [RainbowSixSiegeBridge] Fix bridge (#1587) 2020-07-24 12:56:41 +05:00
sarnd
78298385d0 [TwitterBridge] guest token is returned via body again. (#1647)
* [TwitterBridge] guest token is returned via body again. This change will try to search fot token inside header and fallback to body

* Twitter changed the URL scheme for the API
2020-07-24 12:52:27 +05:00
somini
976445b490 Improve Soundcloud bridge (#1500)
* [SoundcloudBridge] Add playlist support, migrate to `api-v2`
2020-07-05 19:49:46 +02:00
triatic
3ad126cdf2 [core] Add headers to file_get_contents (#1623)
Add response headers to file_get_contents() method. Headers are used by some bridges.
2020-06-25 12:22:05 +02:00
sarnd
e87b868307 [TwitterBridge] Fix issue #1621 @<twitter_user> failed with error 429 (#1622)
* [TwitterBridge] Fix issue #1621 @<twitter_user> failed with error 429
2020-06-25 12:21:48 +02:00
triatic
23c61f5f84 [TwitterBridge] Expire guest token by time (#1606)
* [TwitterBridge] Expire guest token by time

In addition to fetching a new guest token after 100 uses, also expire token after 5 minutes (configurable).
2020-06-23 15:14:50 +02:00
somini
22a01f1093 [Twitter] Fix Twitter bridge images and add other media types (#1595)
* Keep old URI structure

Use the username, not the user ID.

* Fix Twitter bridge images

Credit to @kinoushe

See https://github.com/RSS-Bridge/rss-bridge/issues/1562#issuecomment-639393175

* Include Videos and "Animated GIF" as twit enclosures

Credit to @kinoushe for digging into the API docs.

https://github.com/RSS-Bridge/rss-bridge/issues/1562#issuecomment-640320688

* Calculate the highest bitrate video

Include that on the enclosure.

* Appease linter

* Appease linter, again

* Remove surrounding link from videos

Add it on a smaller link besides it.

See
https://github.com/RSS-Bridge/rss-bridge/pull/1595#issuecomment-640989208

* Include video poster on the enclosures.
2020-06-10 22:39:36 +02:00
Park0
98ff5a095c [Marktplaats] New Bridge (#1575) 2020-06-09 20:21:34 +02:00
Eugene Molotov
e4c4ae8245 [MemcachedCache] loadData now returns null instead of false (#1592)
FileCache and SQLiteCache returns null on cache miss. This is important if using strict comparing (for example when using "===")
2020-06-08 11:27:19 +02:00
Lyra
124631df73 [TwitterBridge] Fix caching policy, usernames as well as images 2020-06-08 11:18:24 +02:00
Lyra
06891ae35f [TwitterBridge] Fix the bridge using a brand new API 2020-06-05 10:17:53 +02:00
Michael Bemmerl
c4422bdbb5 [Core] Fix notice of undefined offset when in detached HEAD state. (#1569) 2020-05-27 23:08:06 +02:00
floviolleau
a1dd98ff82 [LesJoiesDuCodeBridge] Fix items not loading 2020-05-27 23:04:43 +02:00
floviolleau
25f0d3b877 [TheCodingLoveBridge] Fix not loading items (#1577) 2020-05-27 23:04:03 +02:00
Damien Calesse
9a66227a79 [SensCritique] Fix search display (#1567)
- Remove movies search. It appears the website changed their movies
  displays and data cannot be easily extracted for now.
- Fix some errors on items without proper description and/or original
  title.
2020-05-20 21:52:37 +02:00
Sandro
8047041963 [Core] Include Media RSS namespace for Atom feeds
Include Media RSS namespace for Atom feeds
Fix #1511
Fix #1499
2020-05-19 10:00:12 +02:00
Joseph
fa74d3728b [GizmodoBridge] Fix bridge (#1538)
* [GizmodoBridge] Update bridge
2020-05-17 20:35:34 +02:00
Fanch
8233497611 [AirBreizhBridge] Add new bridge (#1544)
* [AirBreizhBridge] Add new bridge
2020-05-17 20:33:14 +02:00
Joseph
71745116e1 [MozillaBugTrackerBridge] Fix bridge (#1550)
* [MozillaBugTrackerBridge] Fix bridge
2020-05-17 20:33:01 +02:00
Paroleen
36fc4822dd [UnraidCommunityApplicationsBridge] Add new bridge (#1534)
* [UnraidCommunityApplicationsBridge] Add new bridge
2020-05-17 20:22:04 +02:00
Eugene Molotov
868d3f600d [VkBridge] Fix one letter bug on titles (#1555) 2020-05-17 20:21:37 +02:00
sysadminstory
f4affe1833 [AuoJMBridge] Follow Website change (#1527)
* [AuoJMBridge] Follow Website change
2020-05-17 20:05:04 +02:00
Joseph
63a4db7e86 [DownDetectorBridge] Fix bridge (#1528) 2020-05-17 20:04:37 +02:00
Joseph
f48909b84e [ASRockNewsBridge] Add Bridge (#1526)
* [ASRockNewsBridge] Add Bridge
2020-05-17 20:00:52 +02:00
Paroleen
ca88096f1f [AwwwardsBridge] New bridge (#1524)
[AwwwardsBridge] New bridge (#1524)
2020-05-17 19:58:19 +02:00
Lyra
1044952987 [MediapartBlogsBridge] Lint
[MediapartBlogsBridge] Lint
2020-05-17 19:49:00 +02:00
Eugene Molotov
119f4bdec5 [MediapartBlogsBridge] Lint 2020-05-10 17:35:21 +05:00
Lyra
e617d9f728 [MediapartBlogsBridge]: New Bridge
[MediapartBlogsBridge]: New Bridge
2020-04-03 10:02:47 +02:00
Lyra
5a43db4fb5 [FolhaDeSaoPauloBridge]: Improvements
[FolhaDeSaoPauloBridge]: Improvements
2020-04-03 09:53:42 +02:00
Lyra
badb5313b7 [NordBayernBridge] Add bridge (#1513)
* add Nordbayern bridge
2020-04-03 09:52:02 +02:00
Lyra
5eeda8dd52 Merge pull request #1515 from Dreckiger-Dan/patch-1
[HeiseBridge] add TechStage support
2020-04-03 09:50:08 +02:00
Lyra
413ae3cef6 [GithubTrendingBridge] Add bridge (#1492)
* Added GithubTrendingBridge
2020-04-03 09:48:31 +02:00
Christian Schabesberger
604d527ac7 add nordbayern bridge
fix intending
2020-04-02 12:44:10 +02:00
Dreckiger-Dan
cccd390b0f [HeiseBridge] add TechStage support 2020-03-31 23:47:57 +02:00
somini
223337d62d [FolhaDeSaoPauloBridge]: Improve URL
Remove the redirection.
2020-03-31 02:34:38 +01:00
somini
066e42e99a [FolhaDeSaoPauloBridge]: Improve HTML 2020-03-31 02:32:15 +01:00
Kirill Kotikov
fbfc82b0b7 Revert feed title 2020-03-26 21:37:19 +03:00
ORelio
00dd81a8aa [DarkReading] Hide dummy articles 2020-03-25 20:40:17 +01:00
somini
e0ac9972ee [MediapartBlogsBridge]: New Bridge
Fix #1468
2020-03-25 19:02:09 +00:00
Kirill Kotikov
f2de5aecc7 Change feed title 2020-03-24 19:07:23 +03:00
Kirill Kotikov
0fd7021030 Change cache time to 24hr (daily update time) 2020-03-23 10:35:02 +03:00
Kirill Kotikov
3ec32bb6c2 Fix title if language not set 2020-03-22 21:43:37 +03:00
Kirill Kotikov
ec7ef8f502 Update GithubTrendingBridge.php 2020-03-21 05:07:38 +03:00
Kirill Kotikov
7b73f3217f Fix page request 2020-03-21 05:01:45 +03:00
Kirill Kotikov
7c71377af0 Add additional languages + fix issues 2020-03-20 16:34:42 +03:00
Kirill Kotikov
c2559ff71f Add sdfsf 2020-03-16 19:25:28 +03:00
John Corser
366d2d66b3 [RobinhoodSnacks] Add bridge for Robinhood Snacks (#1460) 2020-02-26 23:32:57 +01:00
Lyra
7b63da522f [InstagramBridge] Use lowercase comparison when looking up user pk 2020-02-26 22:35:44 +01:00
136 changed files with 8617 additions and 7212 deletions

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

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

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

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

View File

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

View File

@@ -3,20 +3,16 @@ 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 zlib1g-dev libmemcached-dev \
&& docker-php-ext-install -j$(nproc) simplexml \
&& apt-get --yes update \
&& apt-get --yes --no-install-recommends install \
zlib1g-dev \
libmemcached-dev \
&& rm -rf /var/lib/apt/lists/* \
&& pecl install memcached \
&& docker-php-ext-enable memcached \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
&& sed -ri -e 's/(MinProtocol\s*=\s*)TLSv1\.2/\1None/' /etc/ssl/openssl.cnf \
&& 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

@@ -1,6 +1,6 @@
![RSS-Bridge](static/logo_600px.png)
===
[![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/)
[![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/) [![Actions Status](https://img.shields.io/github/workflow/status/RSS-Bridge/rss-bridge/Tests/master?label=GitHub%20Actions&logo=github)](https://github.com/RSS-Bridge/rss-bridge/actions) [![Docker Build Status](https://img.shields.io/docker/cloud/build/rssbridge/rss-bridge?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 that don't have one. It can be used on webservers or as a stand-alone application in CLI mode.
@@ -65,6 +65,7 @@ RSS-Bridge requires PHP 5.6 or higher with following extensions enabled:
- [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
- [`json`](https://secure.php.net/manual/en/book.json.php)
- [`filter`](https://secure.php.net/manual/en/book.filter.php)
- [`sqlite3`](http://php.net/manual/en/book.sqlite3.php) (only when using SQLiteCache)
Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
@@ -109,34 +110,41 @@ 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)
* [akirk](https://github.com/akirk)
* [Albirew](https://github.com/Albirew)
* [aledeg](https://github.com/aledeg)
* [alex73](https://github.com/alex73)
* [alexAubin](https://github.com/alexAubin)
* [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel)
* [arnd-s](https://github.com/arnd-s)
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
* [AxorPL](https://github.com/AxorPL)
* [ayacoo](https://github.com/ayacoo)
* [az5he6ch](https://github.com/az5he6ch)
* [azdkj532](https://github.com/azdkj532)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
* [Binnette](https://github.com/Binnette)
* [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel)
* [Chouchen](https://github.com/Chouchen)
* [ckiw](https://github.com/ckiw)
* [cn-tools](https://github.com/cn-tools)
* [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting)
* [couraudt](https://github.com/couraudt)
* [csisoap](https://github.com/csisoap)
* [cyberjacob](https://github.com/cyberjacob)
* [da2x](https://github.com/da2x)
* [Daiyousei](https://github.com/Daiyousei)
* [dawidsowa](https://github.com/dawidsowa)
* [DevonHess](https://github.com/DevonHess)
* [disk0x](https://github.com/disk0x)
* [DJCrashdummy](https://github.com/DJCrashdummy)
* [Djuuu](https://github.com/Djuuu)
@@ -144,32 +152,47 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [dominik-th](https://github.com/dominik-th)
* [Draeli](https://github.com/Draeli)
* [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
* [drego85](https://github.com/drego85)
* [drklee3](https://github.com/drklee3)
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [fanch317](https://github.com/fanch317)
* [fivefilters](https://github.com/fivefilters)
* [floviolleau](https://github.com/floviolleau)
* [fluffy-critter](https://github.com/fluffy-critter)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
* [ggiessen](https://github.com/ggiessen)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [Glandos](https://github.com/Glandos)
* [gloony](https://github.com/gloony)
* [GregThib](https://github.com/GregThib)
* [griffaurel](https://github.com/griffaurel)
* [Grummfy](https://github.com/Grummfy)
* [gsantner](https://github.com/gsantner)
* [guigot](https://github.com/guigot)
* [hollowleviathan](https://github.com/hollowleviathan)
* [hpacleb](https://github.com/hpacleb)
* [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)
* [jacquesh](https://github.com/jacquesh)
* [JasonGhent](https://github.com/JasonGhent)
* [jcgoette](https://github.com/jcgoette)
* [jdesgats](https://github.com/jdesgats)
* [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand)
* [JimDog546](https://github.com/JimDog546)
* [Jocker666z](https://github.com/Jocker666z)
* [johnnygroovy](https://github.com/johnnygroovy)
* [johnpc](https://github.com/johnpc)
* [killruana](https://github.com/killruana)
* [joni1993](https://github.com/joni1993)
* [joshcoales](https://github.com/joshcoales)
* [klimplant](https://github.com/klimplant)
* [kolarcz](https://github.com/kolarcz)
* [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc)
* [l1n](https://github.com/l1n)
@@ -178,6 +201,7 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry)
* [Leomaradan](https://github.com/Leomaradan)
* [liamka](https://github.com/liamka)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos)
@@ -188,54 +212,78 @@ https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
* [mdemoss](https://github.com/mdemoss)
* [melangue](https://github.com/melangue)
* [metaMMA](https://github.com/metaMMA)
* [mibe](https://github.com/mibe)
* [mightymt](https://github.com/mightymt)
* [mitsukarenai](https://github.com/mitsukarenai)
* [Monocularity](https://github.com/Monocularity)
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble)
* [mro](https://github.com/mro)
* [mschwld](https://github.com/mschwld)
* [mxmehl](https://github.com/mxmehl)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [Niehztog](https://github.com/Niehztog)
* [Nono-m0le](https://github.com/Nono-m0le)
* [ObsidianWitch](https://github.com/ObsidianWitch)
* [OliverParoczai](https://github.com/OliverParoczai)
* [oratosquilla-oratoria](https://github.com/oratosquilla-oratoria)
* [Ololbu](https://github.com/Ololbu)
* [ORelio](https://github.com/ORelio)
* [otakuf](https://github.com/otakuf)
* [Park0](https://github.com/Park0)
* [Paroleen](https://github.com/Paroleen)
* [PaulVayssiere](https://github.com/PaulVayssiere)
* [pellaeon](https://github.com/pellaeon)
* [PeterDaveHello](https://github.com/PeterDaveHello)
* [Peterr-K](https://github.com/Peterr-K)
* [Piranhaplant](https://github.com/Piranhaplant)
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
* [pitchoule](https://github.com/pitchoule)
* [pmaziere](https://github.com/pmaziere)
* [Pofilo](https://github.com/Pofilo)
* [prysme01](https://github.com/prysme01)
* [Qluxzz](https://github.com/Qluxzz)
* [quentinus95](https://github.com/quentinus95)
* [rakoo](https://github.com/rakoo)
* [RawkBob](https://github.com/RawkBob)
* [regisenguehard](https://github.com/regisenguehard)
* [Riduidel](https://github.com/Riduidel)
* [rogerdc](https://github.com/rogerdc)
* [Roliga](https://github.com/Roliga)
* [ronansalmon](https://github.com/ronansalmon)
* [rremizov](https://github.com/rremizov)
* [sebsauvage](https://github.com/sebsauvage)
* [shutosg](https://github.com/shutosg)
* [simon816](https://github.com/simon816)
* [Simounet](https://github.com/Simounet)
* [somini](https://github.com/somini)
* [squeek502](https://github.com/squeek502)
* [stjohnjohnson](https://github.com/stjohnjohnson)
* [Strubbl](https://github.com/Strubbl)
* [sublimz](https://github.com/sublimz)
* [sunchaserinfo](https://github.com/sunchaserinfo)
* [SuperSandro2000](https://github.com/SuperSandro2000)
* [sysadminstory](https://github.com/sysadminstory)
* [t0stiman](https://github.com/t0stiman)
* [tameroski](https://github.com/tameroski)
* [teromene](https://github.com/teromene)
* [tgkenney](https://github.com/tgkenney)
* [thefranke](https://github.com/thefranke)
* [ThePadawan](https://github.com/ThePadawan)
* [TheRadialActive](https://github.com/TheRadialActive)
* [theScrabi](https://github.com/theScrabi)
* [thezeroalpha](https://github.com/thezeroalpha)
* [TitiTestScalingo](https://github.com/TitiTestScalingo)
* [triatic](https://github.com/triatic)
* [VerifiedJoseph](https://github.com/VerifiedJoseph)
* [WalterBarrett](https://github.com/WalterBarrett)
* [wtuuju](https://github.com/wtuuju)
* [xurxof](https://github.com/xurxof)
* [yamanq](https://github.com/yamanq)
* [yardenac](https://github.com/yardenac)
* [ymeister](https://github.com/ymeister)
* [ZeNairolf](https://github.com/ZeNairolf)
Licenses
===
@@ -243,6 +291,7 @@ The source code for RSS-Bridge is [Public Domain](UNLICENSE).
RSS-Bridge uses third party libraries with their own license:
* [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`PHP Simple HTML DOM Parser`](http://simplehtmldom.sourceforge.net/) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT)
@@ -264,6 +313,6 @@ You're not social when you hamper sharing by removing feeds. You're happy to hav
We want to share with friends, using open protocols: RSS, Atom, XMPP, whatever. Because no one wants to have *your* service with *your* applications using *your* API force-feeding them. Friends must be free to choose whatever software and service they want.
We are rebuilding bridges you have wilfully destroyed.
We are rebuilding bridges you have willfully destroyed.
Get your shit together: Put RSS/Atom back in.

View File

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

View File

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

View File

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

View File

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

View File

@@ -85,7 +85,7 @@ class AllocineFRBridge extends BridgeAbstract {
self::PARAMETERS[$this->queriedContext]['category']['values']
);
foreach($html->find('div[class=col-left]', 0)->find('div[class*=video-card]') as $element) {
foreach($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
$item = array();
$title = $element->find('a[class*=meta-title-link]', 0);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -125,9 +125,11 @@ class BandcampBridge extends BridgeAbstract {
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;
if ($html->find('meta[name=title]', 0)) {
$this->feedName = $html->find('meta[name=title]', 0)->content;
} else {
$this->feedName = str_replace('Music | ', '', $html->find('title', 0)->plaintext);
}
$regex = '/band_id=(\d+)/';
if(preg_match($regex, $html, $matches) == false)

View File

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

View File

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

View File

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

View File

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

219
bridges/BukowskisBridge.php Executable file
View File

@@ -0,0 +1,219 @@
<?php
class BukowskisBridge extends BridgeAbstract
{
const NAME = 'Bukowskis';
const URI = 'https://www.bukowskis.com';
const DESCRIPTION = 'Fetches info about auction objects from Bukowskis auction house';
const MAINTAINER = 'Qluxzz';
const PARAMETERS = array(array(
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All categories' => '',
'Art' => array(
'All' => 'art',
'Classic Art' => 'art.classic-art',
'Classic Finnish Art' => 'art.classic-finnish-art',
'Classic Swedish Art' => 'art.classic-swedish-art',
'Contemporary' => 'art.contemporary',
'Modern Finnish Art' => 'art.modern-finnish-art',
'Modern International Art' => 'art.modern-international-art',
'Modern Swedish Art' => 'art.modern-swedish-art',
'Old Masters' => 'art.old-masters',
'Other' => 'art.other',
'Photographs' => 'art.photographs',
'Prints' => 'art.prints',
'Sculpture' => 'art.sculpture',
'Swedish Old Masters' => 'art.swedish-old-masters',
),
'Asian Ceramics & Works of Art' => array(
'All' => 'asian-ceramics-works-of-art',
'Other' => 'asian-ceramics-works-of-art.other',
'Porcelain' => 'asian-ceramics-works-of-art.porcelain',
),
'Books & Manuscripts' => array(
'All' => 'books-manuscripts',
'Books' => 'books-manuscripts.books',
),
'Carpets, rugs & textiles' => array(
'All' => 'carpets-rugs-textiles',
'European' => 'carpets-rugs-textiles.european',
'Oriental' => 'carpets-rugs-textiles.oriental',
'Rest of the world' => 'carpets-rugs-textiles.rest-of-the-world',
'Scandinavian' => 'carpets-rugs-textiles.scandinavian',
),
'Ceramics & porcelain' => array(
'All' => 'ceramics-porcelain',
'Ceramic ware' => 'ceramics-porcelain.ceramic-ware',
'European' => 'ceramics-porcelain.european',
'Rest of the world' => 'ceramics-porcelain.rest-of-the-world',
'Scandinavian' => 'ceramics-porcelain.scandinavian',
),
'Collectibles' => array(
'All' => 'collectibles',
'Advertising & Retail' => 'collectibles.advertising-retail',
'Memorabilia' => 'collectibles.memorabilia',
'Movies & music' => 'collectibles.movies-music',
'Other' => 'collectibles.other',
'Retro & Popular Culture' => 'collectibles.retro-popular-culture',
'Technica & Nautica' => 'collectibles.technica-nautica',
'Toys' => 'collectibles.toys',
),
'Design' => array(
'All' => 'design',
'Art glass' => 'design.art-glass',
'Furniture' => 'design.furniture',
'Other' => 'design.other',
),
'Folk art' => array(
'All' => 'folk-art',
'All categories' => 'lots',
),
'Furniture' => array(
'All' => 'furniture',
'Armchairs & Sofas' => 'furniture.armchairs-sofas',
'Cabinets & Bureaus' => 'furniture.cabinets-bureaus',
'Chairs' => 'furniture.chairs',
'Garden furniture' => 'furniture.garden-furniture',
'Mirrors' => 'furniture.mirrors',
'Other' => 'furniture.other',
'Shelves & Book cases' => 'furniture.shelves-book-cases',
'Tables' => 'furniture.tables',
),
'Glassware' => array(
'All' => 'glassware',
'Glassware' => 'glassware.glassware',
'Other' => 'glassware.other',
),
'Jewellery' => array(
'All' => 'jewellery',
'Bracelets' => 'jewellery.bracelets',
'Brooches' => 'jewellery.brooches',
'Earrings' => 'jewellery.earrings',
'Necklaces & Pendants' => 'jewellery.necklaces-pendants',
'Other' => 'jewellery.other',
'Rings' => 'jewellery.rings',
),
'Lighting' => array(
'All' => 'lighting',
'Candle sticks & Candelabras' => 'lighting.candle-sticks-candelabras',
'Ceiling lights' => 'lighting.ceiling-lights',
'Chandeliers' => 'lighting.chandeliers',
'Floor lights' => 'lighting.floor-lights',
'Other' => 'lighting.other',
'Table lights' => 'lighting.table-lights',
'Wall lights' => 'lighting.wall-lights',
),
'Militaria' => array(
'All' => 'militaria',
'Honors & Medals' => 'militaria.honors-medals',
'Other militaria' => 'militaria.other-militaria',
'Weaponry' => 'militaria.weaponry',
),
'Miscellaneous' => array(
'All' => 'miscellaneous',
'Brass, Copper & Pewter' => 'miscellaneous.brass-copper-pewter',
'Nickel silver' => 'miscellaneous.nickel-silver',
'Oriental' => 'miscellaneous.oriental',
'Other' => 'miscellaneous.other',
),
'Silver' => array(
'All' => 'silver',
'Candle sticks' => 'silver.candle-sticks',
'Cups & Bowls' => 'silver.cups-bowls',
'Cutlery' => 'silver.cutlery',
'Other' => 'silver.other',
),
'Timepieces' => array(
'All' => 'timepieces',
'Other' => 'timepieces.other',
'Pocket watches' => 'timepieces.pocket-watches',
'Table clocks' => 'timepieces.table-clocks',
'Wrist watches' => 'timepieces.wrist-watches',
),
'Vintage & Fashion' => array(
'All' => 'vintage-fashion',
'Accessories' => 'vintage-fashion.accessories',
'Bags & Trunks' => 'vintage-fashion.bags-trunks',
'Clothes' => 'vintage-fashion.clothes',
),
)
),
'sort_order' => array(
'name' => 'Sort order',
'type' => 'list',
'values' => array(
'Ending soon' => 'ending',
'Most recent' => 'recent',
'Most bids' => 'most',
'Fewest bids' => 'fewest',
'Lowest price' => 'lowest',
'Highest price' => 'highest',
'Lowest estimate' => 'low',
'Highest estimate' => 'high',
'Alphabetical' => 'alphabetical',
),
),
'language' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'English' => 'en',
'Swedish' => 'sv',
'Finnish' => 'fi'
),
),
));
const CACHE_TIMEOUT = 3600; // 1 hour
private $title;
public function collectData()
{
$baseUrl = 'https://www.bukowskis.com';
$category = $this->getInput('category');
$language = $this->getInput('language');
$sort_order = $this->getInput('sort_order');
$url = $baseUrl . '/' . $language . '/lots';
if ($category)
$url = $url . '/category/' . $category;
if ($sort_order)
$url = $url . '/sort/' . $sort_order;
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request: ' . $url);
$this->title = htmlspecialchars_decode($html->find('title', 0)->innertext);
foreach ($html->find('div.c-lot-index-lot') as $lot) {
$title = $lot->find('a.c-lot-index-lot__title', 0)->plaintext;
$relative_url = $lot->find('a.c-lot-index-lot__link', 0)->href;
$images = json_decode(
htmlspecialchars_decode(
$lot
->find('img.o-aspect-ratio__image', 0)
->getAttribute('data-thumbnails')
)
);
$this->items[] = array(
'title' => $title,
'uri' => $baseUrl . $relative_url,
'uid' => $lot->getAttribute('data-lot-id'),
'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title,
'enclosures' => array_slice($images, 1),
);
}
}
public function getName()
{
return $this->title ?: parent::getName();
}
}

View File

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

View File

@@ -1,28 +0,0 @@
<?php
class ChristianDailyReporterBridge extends BridgeAbstract {
const MAINTAINER = 'rogerdc';
const NAME = 'Christian Daily Reporter Unofficial RSS';
const URI = 'https://www.christiandailyreporter.com/';
const DESCRIPTION = 'The Unofficial Christian Daily Reporter RSS';
// const CACHE_TIMEOUT = 86400; // 1 day
public function getIcon() {
return self::URI . 'images/cdrfavicon.png';
}
public function collectData() {
$uri = 'https://www.christiandailyreporter.com/';
$html = getSimpleHTMLDOM($uri)
or returnServerError('Could not request Christian Daily Reporter.');
foreach($html->find('div.top p a,div.column p a') as $element) {
$item = array();
// Title
$item['title'] = $element->innertext;
// URL
$item['uri'] = $element->href;
$this->items[] = $item;
}
}
}

View File

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

View File

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

View File

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

166
bridges/DockerHubBridge.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
class DockerHubBridge extends BridgeAbstract {
const NAME = 'Docker Hub Bridge';
const URI = 'https://hub.docker.com';
const DESCRIPTION = 'Returns new images for a container';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(
'User Submitted Image' => array(
'user' => array(
'name' => 'User',
'type' => 'text',
'required' => true,
'exampleValue' => 'rssbridge',
),
'repo' => array(
'name' => 'Repository',
'type' => 'text',
'required' => true,
'exampleValue' => 'rss-bridge',
)
),
'Official Image' => array(
'repo' => array(
'name' => 'Repository',
'type' => 'text',
'required' => true,
'exampleValue' => 'postgres',
)
),
);
const CACHE_TIMEOUT = 3600; // 1 hour
private $apiURL = 'https://hub.docker.com/v2/repositories/';
private $imageUrlRegex = '/hub\.docker\.com\/r\/([\w]+)\/([\w-]+)\/?/';
private $officialImageUrlRegex = '/hub\.docker\.com\/_\/([\w-]+)\/?/';
public function detectParameters($url) {
$params = array();
// user submitted image
if(preg_match($this->imageUrlRegex, $url, $matches)) {
$params['context'] = 'User Submitted Image';
$params['user'] = $matches[1];
$params['repo'] = $matches[2];
return $params;
}
// official image
if(preg_match($this->officialImageUrlRegex, $url, $matches)) {
$params['context'] = 'Official Image';
$params['repo'] = $matches[1];
return $params;
}
return null;
}
public function collectData() {
$json = getContents($this->getApiUrl())
or returnServerError('Could not request: ' . $this->getURI());
$data = json_decode($json, false);
foreach ($data->results as $result) {
$item = array();
$lastPushed = date('Y-m-d H:i:s', strtotime($result->tag_last_pushed));
$item['title'] = $result->name;
$item['uid'] = $result->id;
$item['uri'] = $this->getTagUrl($result->name);
$item['author'] = $result->last_updater_username;
$item['timestamp'] = $result->tag_last_pushed;
$item['content'] = <<<EOD
<Strong>Tag</strong><br>
<p>{$result->name}</p>
<Strong>Last pushed</strong><br>
<p>{$lastPushed}</p>
<Strong>Images</strong><br>
{$this->getImages($result)}
EOD;
$this->items[] = $item;
}
}
public function getURI() {
if ($this->queriedContext === 'Official Image') {
return self::URI . '/_/' . $this->getRepo();
}
if ($this->getInput('repo')) {
return self::URI . '/r/' . $this->getRepo();
}
return parent::getURI();
}
public function getName() {
if ($this->getInput('repo')) {
return $this->getRepo() . ' - Docker Hub';
}
return parent::getName();
}
private function getRepo() {
if ($this->queriedContext === 'Official Image') {
return $this->getInput('repo');
}
return $this->getInput('user') . '/' . $this->getInput('repo');
}
private function getApiUrl() {
if ($this->queriedContext === 'Official Image') {
return $this->apiURL . 'library/' . $this->getRepo() . '/tags/?page_size=25&page=1';
}
return $this->apiURL . $this->getRepo() . '/tags/?page_size=25&page=1';
}
private function getLayerUrl($name, $digest) {
if ($this->queriedContext === 'Official Image') {
return self::URI . '/layers/' . $this->getRepo() . '/library/' .
$this->getRepo() . '/' . $name . '/images/' . $digest;
}
return self::URI . '/layers/' . $this->getRepo() . '/' . $name . '/images/' . $digest;
}
private function getTagUrl($name) {
if ($this->queriedContext === 'Official Image') {
return self::URI . '/_/' . $this->getRepo() . '?tab=tags&name=' . $name;
}
return self::URI . '/r/' . $this->getRepo() . '/tags?name=' . $name;
}
private function getImages($result) {
$html = <<<EOD
<table style="width:300px;"><thead><tr><th>Digest</th><th>OS/architecture</th></tr></thead></tbody>
EOD;
foreach ($result->images as $image) {
$layersUrl = $this->getLayerUrl($result->name, $image->digest);
$id = $this->getShortDigestId($image->digest);
$html .= <<<EOD
<tr>
<td><a href="{$layersUrl}">{$id}</a></td>
<td>{$image->os}/{$image->architecture}</td>
</tr>
EOD;
}
return $html . '</tbody></table>';
}
private function getShortDigestId($digest) {
$parts = explode(':', $digest);
return substr($parts[1], 0, 12);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<?php
class ExtremeDownloadBridge extends BridgeAbstract {
const NAME = 'Extreme Download';
const URI = 'https://www.extreme-down.ninja/';
const URI = 'https://www.extreme-down.tv/';
const DESCRIPTION = 'Suivi de série sur Extreme Download';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
@@ -81,6 +81,16 @@ class ExtremeDownloadBridge extends BridgeAbstract {
}
}
public function getURI() {
switch($this->queriedContext) {
case 'Suivre la publication des épisodes d\'une série en cours de diffusion':
return self::URI . $this->getInput('url');
break;
default:
return self::URI;
}
}
private function findLinkType($element)
{
$return = '';

67
bridges/FM4Bridge.php Normal file
View File

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

View File

@@ -0,0 +1,115 @@
<?php
class FSecureBlogBridge extends BridgeAbstract {
const NAME = 'F-Secure Blog';
const URI = 'https://blog.f-secure.com';
const DESCRIPTION = 'F-Secure Blog';
const MAINTAINER = 'simon816';
const PARAMETERS = array(
'' => array(
'categories' => array(
'name' => 'Blog categories',
'exampleValue' => 'home-security',
),
'language' => array(
'name' => 'Language',
'defaultValue' => 'en',
),
'oldest_date' => array(
'name' => 'Oldest article date',
'exampleValue' => '-2 months',
),
)
);
public function getURI() {
$lang = $this->getInput('language') or 'en';
if ($lang === 'en') {
return self::URI;
}
return self::URI . "/$lang";
}
public function collectData() {
$this->items = array();
$this->seen = array();
$this->oldest = strtotime($this->getInput('oldest_date')) ?: 0;
$categories = $this->getInput('categories');
if (!empty($categories)) {
foreach (explode(',', $categories) as $cat) {
if (!empty($cat)) {
$this->collectCategory($cat);
}
}
return;
}
$html = getSimpleHTMLDOMCached($this->getURI() . '/');
foreach ($html->find('ul.c-header-menu-desktop__list li a') as $link) {
$url = parse_url($link->href);
if (($pos = strpos($url['path'], '/category/')) !== false) {
$cat = substr($url['path'], $pos + strlen('/category/'), -1);
$this->collectCategory($cat);
}
}
}
private function collectCategory($category) {
$url = $this->getURI() . "/category/$category/";
while ($url) {
$url = $this->collectListing($url);
}
}
// n.b. this relies on articles to be ordered by date so the cutoff works
private function collectListing($url) {
$html = getSimpleHTMLDOMCached($url, 60 * 60);
$items = $html->find('section.b-blog .l-blog__content__listing div.c-listing-item');
$catName = trim($html->find('section.b-blog .c-blog-header__title', 0)->plaintext);
foreach ($items as $item) {
$url = $item->getAttribute('data-url');
if (!$this->collectArticle($url)) {
return null; // Too old, stop collecting
}
}
// Point's to 404 for non-english blog
// $next = $html->find('link[rel=next]', 0);
$next = $html->find('ul.page-numbers a.next', 0);
return $next ? $next->href : null;
}
// Returns a boolean whether to continue collecting articles
// i.e. date is after oldest cutoff
private function collectArticle($url) {
if (array_key_exists($url, $this->seen)) {
return true;
}
$html = getSimpleHTMLDOMCached($url);
$rssItem = array( 'uri' => $url, 'uid' => $url );
$rssItem['title'] = $html->find('meta[property=og:title]', 0)->content;
$dt = $html->find('meta[property=article:published_time]', 0)->content;
// Exit if too old
if (strtotime($dt) < $this->oldest) {
return false;
}
$rssItem['timestamp'] = $dt;
$img = $html->find('meta[property=og:image]', 0);
$rssItem['enclosures'] = $img ? array($img->content) : array();
$rssItem['author'] = trim($html->find('.c-blog-author__text a', 0)->plaintext);
$rssItem['categories'] = array_map(function ($link) {
return trim($link->plaintext);
}, $html->find('.b-single-header__categories .c-category-list a'));
$rssItem['content'] = trim($html->find('article', 0)->innertext);
$this->items[] = $rssItem;
$this->seen[$url] = 1;
return true;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,104 @@
<?php
class FirefoxAddonsBridge extends BridgeAbstract {
const NAME = 'Firefox Add-ons Bridge';
const URI = 'https://addons.mozilla.org/';
const DESCRIPTION = 'Returns version history for a Firefox Add-on.';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'id' => array(
'name' => 'Add-on ID',
'type' => 'text',
'required' => true,
'exampleValue' => 'save-to-the-wayback-machine',
)
)
);
const CACHE_TIMEOUT = 3600;
private $feedName = '';
private $releaseDateRegex = '/Released ([\w, ]+) - ([\w. ]+)/';
private $xpiFileRegex = '/([A-Za-z0-9_.-]+)\.xpi$/';
private $outgoingRegex = '/https:\/\/outgoing.prod.mozaws.net\/v1\/(?:[A-z0-9]+)\//';
private $urlRegex = '/addons\.mozilla\.org\/(?:[\w-]+\/)?firefox\/addon\/([\w-]+)/';
public function detectParameters($url) {
$params = array();
if(preg_match($this->urlRegex, $url, $matches)) {
$params['id'] = $matches[1];
return $params;
}
return null;
}
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
$this->feedName = $html->find('h1[class="AddonTitle"] > a', 0)->innertext;
$author = $html->find('span.AddonTitle-author > a', 0)->plaintext;
foreach ($html->find('div.AddonVersionCard-content') as $div) {
$item = array();
$item['title'] = $div->find('h2.AddonVersionCard-version', 0)->plaintext;
$item['uid'] = $item['title'];
$item['uri'] = $this->getURI();
$item['author'] = $author;
if (preg_match($this->releaseDateRegex, $div->find('div.AddonVersionCard-fileInfo', 0)->plaintext, $match)) {
$item['timestamp'] = $match[1];
$size = $match[2];
}
$compatibility = $div->find('div.AddonVersionCard-compatibility', 0)->plaintext;
$license = $div->find('p.AddonVersionCard-license', 0)->innertext;
$downloadlink = $div->find('a.InstallButtonWrapper-download-link', 0)->href;
$releaseNotes = $this->removeOutgoinglink($div->find('div.AddonVersionCard-releaseNotes', 0));
if (preg_match($this->xpiFileRegex, $downloadlink, $match)) {
$xpiFilename = $match[0];
}
$item['content'] = <<<EOD
<strong>Release Notes</strong>
<p>{$releaseNotes}</p>
<strong>Compatibility</strong>
<p>{$compatibility}</p>
<strong>License</strong>
<p>{$license}</p>
<strong>Download</strong>
<p><a href="{$downloadlink}">{$xpiFilename}</a> ($size)</p>
EOD;
$this->items[] = $item;
}
}
public function getURI() {
if (!is_null($this->getInput('id'))) {
return self::URI . 'en-US/firefox/addon/' . $this->getInput('id') . '/versions/';
}
return parent::getURI();
}
public function getName() {
if (!empty($this->feedName)) {
return $this->feedName . ' - Firefox Add-on';
}
return parent::getName();
}
private function removeOutgoinglink($html) {
foreach ($html->find('a') as $a) {
$a->href = urldecode(preg_replace($this->outgoingRegex, '', $a->href));
}
return $html->innertext;
}
}

View File

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

View File

@@ -26,8 +26,9 @@ class FolhaDeSaoPauloBridge extends FeedExpander {
$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>');
$text = strip_tags($text, '<p><b><a><blockquote><figure><figcaption><img><strong><em>');
$item['content'] = $text;
$item['uri'] = explode('*', $item['uri'])[1];
}
} else {
Debug::log('???: ' . $item['uri']);

View File

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

View File

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

View File

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

View File

@@ -33,17 +33,23 @@ class GithubIssueBridge extends BridgeAbstract {
)
);
// Allows generalization with GithubPullRequestBridge
const BRIDGE_OPTIONS = array(0 => 'Project Issues', 1 => 'Issue comments');
const URL_PATH = 'issues';
const SEARCH_QUERY_PATH = 'issues';
const SEARCH_QUERY = '?q=is%3Aissue+sort%3Aupdated-desc';
public function getName(){
$name = $this->getInput('u') . '/' . $this->getInput('p');
switch($this->queriedContext) {
case 'Project Issues':
case static::BRIDGE_OPTIONS[0]: // Project Issues
$prefix = static::NAME . 's for ';
if($this->getInput('c')) {
$prefix = static::NAME . 's comments for ';
}
$name = $prefix . $name;
break;
case 'Issue comments':
case static::BRIDGE_OPTIONS[1]: // Issue comments
$name = static::NAME . ' ' . $name . ' #' . $this->getInput('i');
break;
default: return parent::getName();
@@ -51,14 +57,14 @@ class GithubIssueBridge extends BridgeAbstract {
return $name;
}
public function getURI(){
public function getURI() {
if(null !== $this->getInput('u') && null !== $this->getInput('p')) {
$uri = static::URI . $this->getInput('u') . '/'
. $this->getInput('p') . '/issues';
if($this->queriedContext === 'Issue comments') {
$uri .= '/' . $this->getInput('i');
} elseif($this->getInput('c')) {
$uri .= '?q=is%3Aissue+sort%3Aupdated-desc';
. $this->getInput('p') . '/';
if($this->queriedContext === static::BRIDGE_OPTIONS[1]) {
$uri .= static::URL_PATH . '/' . $this->getInput('i');
} else {
$uri .= static::SEARCH_QUERY_PATH . static::SEARCH_QUERY;
}
return $uri;
}
@@ -72,19 +78,19 @@ class GithubIssueBridge extends BridgeAbstract {
. $this->getInput('u')
. '/'
. $this->getInput('p')
. '/issues/'
. '/' . static::URL_PATH . '/'
. $issue_number
. '#'
. $comment_id;
}
private function extractIssueEvent($issueNbr, $title, $comment){
private function extractIssueEvent($issueNbr, $title, $comment) {
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0);
$author = $comment->find('.author, .avatar', 0);
if ($author) {
$author = $author->plaintext;
$author = trim($author->href, '/');
} else {
$author = '';
}
@@ -95,22 +101,27 @@ class GithubIssueBridge extends BridgeAbstract {
$comment->find('.octicon', 0)->getAttribute('class')
));
$time = $comment->find('relative-time', 0);
if ($time === null) {
return;
}
foreach($comment->find('.Details-content--hidden, .btn') as $el) {
$el->innertext = '';
}
$content = $comment->plaintext;
$item = array();
$item['author'] = $author;
$item['uri'] = $uri;
$item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
$item['timestamp'] = strtotime(
$comment->find('relative-time', 0)->getAttribute('datetime')
);
$item['timestamp'] = strtotime($time->getAttribute('datetime'));
$item['content'] = $content;
return $item;
}
private function extractIssueComment($issueNbr, $title, $comment){
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->parent->id);
private function extractIssueComment($issueNbr, $title, $comment) {
$uri = $this->buildGitHubIssueCommentUri($issueNbr, $comment->id);
$author = $comment->find('.author', 0)->plaintext;
@@ -118,20 +129,23 @@ class GithubIssueBridge extends BridgeAbstract {
$comment->find('.timeline-comment-header-text', 0)->plaintext
);
$time = $comment->find('relative-time', 0);
if ($time === null) {
return;
}
$content = $comment->find('.comment-body', 0)->innertext;
$item = array();
$item['author'] = $author;
$item['uri'] = $uri;
$item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
$item['timestamp'] = strtotime(
$comment->find('relative-time', 0)->getAttribute('datetime')
);
$item['timestamp'] = strtotime($time->getAttribute('datetime'));
$item['content'] = $content;
return $item;
}
private function extractIssueComments($issue){
private function extractIssueComments($issue) {
$items = array();
$title = $issue->find('.gh-header-title', 0)->plaintext;
$issueNbr = trim(
@@ -146,41 +160,45 @@ class GithubIssueBridge extends BridgeAbstract {
if ($comment->hasClass('comment')) {
$comment = $comment->parent;
$item = $this->extractIssueComment($issueNbr, $title, $comment);
$items[] = $item;
if ($item !== null) {
$items[] = $item;
}
continue;
} else {
$comment = $comment->parent;
$item = $this->extractIssueEvent($issueNbr, $title, $comment);
$items[] = $item;
if ($item !== null) {
$items[] = $item;
}
}
}
return $items;
}
public function collectData(){
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError(
'No results for Github Issue ' . $this->getURI()
'No results for ' . static::NAME . ' ' . $this->getURI()
);
switch($this->queriedContext) {
case 'Issue comments':
case static::BRIDGE_OPTIONS[1]: // Issue comments
$this->items = $this->extractIssueComments($html);
break;
case 'Project Issues':
case static::BRIDGE_OPTIONS[0]: // Project Issues
foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
$info = $issue->find('.opened-by', 0);
$issueNbr = substr(
trim($info->plaintext), 1, strpos(trim($info->plaintext), ' ')
);
preg_match('/\/([0-9]+)$/', $issue->find('a', 0)->href, $match);
$issueNbr = $match[1];
$item = array();
$item['content'] = '';
if($this->getInput('c')) {
$uri = static::URI . $this->getInput('u')
. '/' . $this->getInput('p') . '/issues/' . $issueNbr;
. '/' . $this->getInput('p') . '/' . static::URL_PATH . '/' . $issueNbr;
$issue = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT);
if($issue) {
$this->items = array_merge(
@@ -209,7 +227,7 @@ class GithubIssueBridge extends BridgeAbstract {
$item['content'] .= "\n" . 'Comments: ' . $comment_count;
$item['uri'] = self::URI
. $issue->find('.js-navigation-open', 0)->getAttribute('href');
. trim($issue->find('.js-navigation-open', 0)->getAttribute('href'), '/');
$this->items[] = $item;
}
break;
@@ -247,7 +265,7 @@ class GithubIssueBridge extends BridgeAbstract {
$show_comments = 'off';
} break;
case 3: { // Project issues with issue comments
if($path_segments[2] !== 'issues') {
if($path_segments[2] !== static::URL_PATH) {
return null;
}
list($user, $project) = $path_segments;

View File

@@ -0,0 +1,38 @@
<?php
require_once('GithubIssueBridge.php');
class GitHubPullRequestBridge extends GithubIssueBridge {
const MAINTAINER = 'Yaman Qalieh';
const NAME = 'GitHub Pull Request';
const DESCRIPTION = 'Returns the pull request or comments of a pull request of a GitHub project';
const PARAMETERS = array(
'global' => array(
'u' => array(
'name' => 'User name',
'required' => true
),
'p' => array(
'name' => 'Project name',
'required' => true
)
),
'Project Pull Requests' => array(
'c' => array(
'name' => 'Show Pull Request Comments',
'type' => 'checkbox'
)
),
'Pull Request comments' => array(
'i' => array(
'name' => 'Pull Request number',
'type' => 'number',
'required' => true
)
)
);
const BRIDGE_OPTIONS = array(0 => 'Project Pull Requests', 1 => 'Pull Request comments');
const URL_PATH = 'pull';
const SEARCH_QUERY_PATH = 'pulls';
const SEARCH_QUERY = '?q=is%3Apr+sort%3Aupdated-desc';
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
<?php
class HackerNewsUserThreadsBridge extends BridgeAbstract {
const MAINTAINER = 'rakoo';
const NAME = 'Hacker News User Threads';
const URI = 'https://news.ycombinator.com';
const CACHE_TIMEOUT = 7200; // 2 hours
const DESCRIPTION = 'Hacker News threads for a user (at https://news.ycombinator.com/threads?id=xxx)';
const PARAMETERS = array( array(
'user' => array(
'name' => 'User',
'type' => 'text',
'required' => true,
'title' => 'User whose threads you want to see'
)
));
public function collectData(){
$url = 'https://news.ycombinator.com/threads?id=' . $this->getInput('user');
$html = getSimpleHTMLDOM($url) or returnServerError('Could not request HN.');
Debug::log('queried ' . $url);
Debug::log('found ' . $html);
$item = array();
$articles = $html->find('tr[class*="comtr"]');
$story = '';
foreach ($articles as $element) {
$id = $element->getAttribute('id');
$item['uri'] = 'https://news.ycombinator.com/item?id=' . $id;
$author = $element->find('span[class*="comhead"]', 0)->find('a[class="hnuser"]', 0)->innertext;
$newstory = $element->find('span[class*="comhead"]', 0)->find('span[class="storyon"]', 0);
if (count($newstory->find('a')) > 0) {
$story = $newstory->find('a', 0)->innertext;
}
$title = $author . ' | on ' . $story;
$item['author'] = $author;
$item['title'] = $title;
$item['timestamp'] = $element->find('span[class*="age"]', 0)->find('a', 0)->innertext;
$item['content'] = $element->find('span[class*="commtext"]', 0)->innertext;
$this->items[] = $item;
}
}
}

View File

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

114
bridges/IKWYDBridge.php Normal file
View File

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

View File

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

View File

@@ -42,7 +42,6 @@ class InternetArchiveBridge extends BridgeAbstract {
$html = defaultLinkTo($html, $this->getURI());
if ($this->getInput('content') !== 'posts') {
$detailsDivNumber = 0;
foreach ($html->find('div.results > div[data-id]') as $index => $result) {
@@ -54,7 +53,6 @@ class InternetArchiveBridge extends BridgeAbstract {
switch($result->class) {
case 'item-ia':
switch($this->getInput('content')) {
case 'reviews':
$item = $this->processReview($result);
@@ -104,7 +102,6 @@ class InternetArchiveBridge extends BridgeAbstract {
public function getName() {
if (!is_null($this->getInput('username')) && !is_null($this->getInput('content'))) {
$contentValues = array_flip(self::PARAMETERS['Account']['content']['values']);
return $contentValues[$this->getInput('content')] . ' - '
@@ -124,11 +121,10 @@ class InternetArchiveBridge extends BridgeAbstract {
}
private function processUpload($result) {
$item = array();
$collection = $result->find('a.stealth', 0);
$collectionLink = self::URI . $collection->href;
$collectionLink = $collection->href;
$collectionTitle = $collection->find('div.item-parent-ttl', 0)->plaintext;
$item['title'] = trim($result->find('div.ttl', 0)->innertext);
@@ -150,7 +146,6 @@ EOD;
}
private function processReview($result) {
$item = array();
$item['title'] = trim($result->find('div.ttl', 0)->innertext);
@@ -172,7 +167,6 @@ EOD;
}
private function processWebArchives($result) {
$item = array();
$item['title'] = trim($result->find('div.ttl', 0)->plaintext);
@@ -189,7 +183,6 @@ EOD;
}
private function processCollection($result) {
$item = array();
$title = trim($result->find('div.collection-title.C.C2', 0)->children(0)->plaintext);
@@ -209,7 +202,6 @@ EOD;
}
private function processHiddenDetails($html, $detailsDivNumber, $item) {
$description = '';
if ($html->find('div.details-ia.hidden-tiles', $detailsDivNumber)) {
@@ -237,7 +229,6 @@ EOD;
}
private function processPosts($html) {
$items = array();
foreach ($html->find('table.forumTable > tr') as $index => $tr) {
@@ -288,6 +279,7 @@ EOD;
break;
}
}
return $items;
}
}

46
bridges/ItchioBridge.php Normal file
View File

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

View File

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

View File

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

View File

@@ -352,12 +352,14 @@ class LeBonCoinBridge extends BridgeAbstract {
public function collectData(){
$url = 'https://api.leboncoin.fr/finder/search/';
$url = 'https://api.leboncoin.fr/api/adfinder/v1/search';
$data = $this->buildRequestJson();
$header = array(
'User-Agent: LBC;Android;Null;Null;Null;Null;Null;Null;Null;Null',
'User-Agent: LBC;Android;10;SAMSUNG;phone;0aaaaaaaaaaaaaaa;wifi;8.24.3.8;152437;0',
'Content-Type: application/json',
'X-LBC-CC: 7',
'Accept: application/json,application/hal+json',
'Content-Length: ' . strlen($data),
'api_key: ' . self::$LBC_API_KEY
);

View File

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

View File

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

73
bridges/MallTvBridge.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<?php
class NyaaTorrentsBridge extends BridgeAbstract {
class NyaaTorrentsBridge extends FeedExpander {
const MAINTAINER = 'ORelio';
const NAME = 'NyaaTorrents';
@@ -50,6 +50,11 @@ class NyaaTorrentsBridge extends BridgeAbstract {
'name' => 'Keyword',
'description' => 'Keyword(s)',
'type' => 'text'
),
'u' => array(
'name' => 'User',
'description' => 'User',
'type' => 'text'
)
)
);
@@ -58,74 +63,45 @@ class NyaaTorrentsBridge extends BridgeAbstract {
return self::URI . 'static/favicon.png';
}
public function collectData() {
public function collectData(){
$this->collectExpandableDatas(
self::URI . '?page=rss&s=id&o=desc&'
. http_build_query(array(
'f' => $this->getInput('f'),
'c' => $this->getInput('c'),
'q' => $this->getInput('q'),
'u' => $this->getInput('u')
)), 20);
}
// Build Search URL from user-provided parameters
$search_url = self::URI . '?s=id&o=desc&'
. http_build_query(array(
'f' => $this->getInput('f'),
'c' => $this->getInput('c'),
'q' => $this->getInput('q')
));
protected function parseItem($newItem){
$item = parent::parseItem($newItem);
// Retrieve torrent listing from search results, which does not contain torrent description
$html = getSimpleHTMLDOM($search_url)
or returnServerError('Could not request Nyaa: ' . $search_url);
$links = $html->find('a');
$results = array();
foreach ($links as $link)
if (strpos($link->href, '/view/') === 0 && !in_array($link->href, $results))
$results[] = $link->href;
if (empty($results) && empty($this->getInput('q')))
returnServerError('No results from Nyaa: ' . $url, 500);
//Convert URI from torrent file to web page
$item['uri'] = str_replace('/download/', '/view/', $item['uri']);
$item['uri'] = str_replace('.torrent', '', $item['uri']);
//Process each item individually
foreach ($results as $element) {
if ($item_html = getSimpleHTMLDOMCached($item['uri'])) {
//Limit total amount of requests
if(count($this->items) >= 20) {
break;
}
//Retrieve full description from page contents
$item_desc = str_get_html(
markdownToHtml(html_entity_decode($item_html->find('#torrent-description', 0)->innertext))
);
$torrent_id = str_replace('/view/', '', $element);
//Ignore entries without valid torrent ID
if ($torrent_id != 0 && ctype_digit($torrent_id)) {
//Retrieve data for this torrent ID
$item_uri = self::URI . 'view/' . $torrent_id;
//Retrieve full description from torrent page
if ($item_html = getSimpleHTMLDOMCached($item_uri)) {
//Retrieve data from page contents
$item_title = str_replace(' :: Nyaa', '', $item_html->find('title', 0)->plaintext);
$item_desc = str_get_html(markdownToHtml($item_html->find('#torrent-description', 0)->innertext));
$item_author = extractFromDelimiters($item_html->outertext, 'href="/user/', '"');
$item_date = intval(extractFromDelimiters($item_html->outertext, 'data-timestamp="', '"'));
//Retrieve image for thumbnail or generic logo fallback
$item_image = $this->getURI() . 'static/img/avatar/default.png';
foreach ($item_desc->find('img') as $img) {
if (strpos($img->src, 'prez') === false) {
$item_image = $img->src;
break;
}
}
//Build and add final item
$item = array();
$item['uri'] = $item_uri;
$item['title'] = $item_title;
$item['author'] = $item_author;
$item['timestamp'] = $item_date;
$item['enclosures'] = array($item_image);
$item['content'] = $item_desc;
$this->items[] = $item;
//Retrieve image for thumbnail or generic logo fallback
$item_image = $this->getURI() . 'static/img/avatar/default.png';
foreach ($item_desc->find('img') as $img) {
if (strpos($img->src, 'prez') === false) {
$item_image = $img->src;
break;
}
}
$element = null;
//Add expanded fields to the current item
$item['enclosures'] = array($item_image);
$item['content'] = $item_desc;
}
$results = null;
return $item;
}
}

View File

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

View File

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

View File

@@ -123,17 +123,25 @@ class PikabuBridge extends BridgeAbstract {
}
}
$title = $post->find('.story__title-link', 0);
$title_element = $post->find('.story__title-link', 0);
$title = $title_element->plaintext;
$community_link = $post->find('.story__community-link', 0);
// adding special marker for "Maybe News" section
// these posts are fake
if (!is_null($community_link) && $community_link->getAttribute('href') == '/community/maybenews') {
$title = '[' . $community_link->innertext . '] ' . $title;
}
$item = array();
$item['categories'] = $categories;
$item['author'] = $post->find('.user__nick', 0)->innertext;
$item['title'] = $title->plaintext;
$item['title'] = $title;
$item['content'] = strip_tags(
backgroundToImg($post->find('.story__content-inner', 0)->innertext),
'<br><p><img><a>
');
$item['uri'] = $title->href;
$item['uri'] = $title_element->href;
$item['timestamp'] = strtotime($time->getAttribute('datetime'));
$this->items[] = $item;
}

View File

@@ -0,0 +1,45 @@
<?php
class PresidenciaPTBridge extends BridgeAbstract {
const NAME = 'Presidência da República Portuguesa';
const URI = 'https://www.presidencia.pt';
const DESCRIPTION = 'Presidência da República Portuguesa | Mensagens';
const MAINTAINER = 'somini';
const PT_MONTH_NAMES = array(
'janeiro',
'fevereiro',
'março',
'abril',
'maio',
'junho',
'julho',
'agosto',
'setembro',
'outubro',
'novembro',
'dezembro');
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI() . '/atualidade/mensagens')
or returnServerError('Could not load content');
foreach($html->find('#atualidade-list article.card-block') as $element) {
$item = array();
$link = $element->find('a', 0);
$etitle = $link->find('h2', 0);
$edts = $element->find('p', 1);
$edt = html_entity_decode($edts->innertext, ENT_HTML5);
$item['title'] = $etitle->innertext;
$item['uri'] = self::URI . $link->href;
$item['description'] = $element;
$item['timestamp'] = str_ireplace(
array_map(function($name) { return ' de ' . $name . ' de '; }, self::PT_MONTH_NAMES),
array_map(function($num) { return sprintf('-%02d-', $num); }, range(1, sizeof(self::PT_MONTH_NAMES))),
$edt);
$this->items[] = $item;
}
}
}

View File

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

View File

@@ -25,7 +25,7 @@ class RadioMelodieBridge extends BridgeAbstract {
$picture = array();
// Get the Main picture URL
$picture[] = $this->rewriteImage($article->find('div[id=pictureTitleSupport]', 0)->find('img', 0)->src);
$picture[] = self::URI . $article->find('div[id=pictureTitleSupport]', 0)->find('img', 0)->src;
$audioHTML = $article->find('audio');
// Add the audio element to the enclosure

View File

@@ -12,8 +12,8 @@ class RainbowSixSiegeBridge extends BridgeAbstract {
}
public function collectData(){
$dlUrl = 'https://www.ubisoft.com/api/updates/items?categoriesFilter=all';
$dlUrl = $dlUrl . '&limit=6&mediaFilter=all&skip=0&startIndex=undefined&locale=en-us';
$dlUrl = 'https://www.ubisoft.com/api/updates/items?locale=en-us&categoriesFilter=all';
$dlUrl = $dlUrl . '&limit=6&mediaFilter=news&skip=0&startIndex=undefined&tags=BR-rainbow-six%20GA-siege';
$jsonString = getContents($dlUrl) or returnServerError('Error while downloading the website content');
$json = json_decode($jsonString, true);
@@ -27,34 +27,7 @@ class RainbowSixSiegeBridge extends BridgeAbstract {
$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);
$content = $thumbnail . '<br />' . markdownToHtml($jsonItem['content']);
$item = array();
$item['uri'] = $uri;

View File

@@ -1,12 +1,22 @@
<?php
class RedditBridge extends FeedExpander {
const MAINTAINER = 'leomaradan';
class RedditBridge extends BridgeAbstract {
const MAINTAINER = 'dawidsowa';
const NAME = 'Reddit Bridge';
const URI = 'https://www.reddit.com/';
const DESCRIPTION = 'Reddit RSS Feed fixer';
const URI = 'https://www.reddit.com';
const DESCRIPTION = 'Return hot submissions from Reddit';
const PARAMETERS = array(
'global' => array(
'score' => array(
'name' => 'Minimal score',
'required' => false,
'type' => 'number',
'exampleValue' => 100,
'title' => 'Filter out posts with lower score'
)
),
'single' => array(
'r' => array(
'name' => 'SubReddit',
@@ -22,19 +32,221 @@ class RedditBridge extends FeedExpander {
'exampleValue' => 'selfhosted, php',
'title' => 'SubReddit names, separated by commas'
)
),
'user' => array(
'u' => array(
'name' => 'User',
'required' => true,
'title' => 'User name'
),
'comments' => array(
'type' => 'checkbox',
'name' => 'Comments',
'title' => 'Whether to return comments',
'defaultValue' => false
)
)
);
public function collectData(){
public function detectParameters($url) {
$parsed_url = parse_url($url);
switch($this->queriedContext) {
case 'single': $subreddits[] = $this->getInput('r'); break;
case 'multi': $subreddits = explode(',', $this->getInput('rs')); break;
if ($parsed_url['host'] != 'www.reddit.com' && $parsed_url['host'] != 'old.reddit.com') return null;
$path = explode('/', $parsed_url['path']);
if ($path[1] == 'r') {
return array(
'r' => $path[2]
);
} elseif ($path[1] == 'user') {
return array(
'u' => $path[2]
);
} else {
return null;
}
}
public function getIcon() {
return 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png';
}
public function getName() {
if ($this->queriedContext == 'single') {
return 'Reddit r/' . $this->getInput('r');
} elseif ($this->queriedContext == 'user') {
return 'Reddit u/' . $this->getInput('u');
} else {
return self::NAME;
}
}
public function collectData() {
$user = false;
$comments = false;
switch ($this->queriedContext) {
case 'single':
$subreddits[] = $this->getInput('r');
break;
case 'multi':
$subreddits = explode(',', $this->getInput('rs'));
break;
case 'user':
$subreddits[] = $this->getInput('u');
$user = true;
$comments = $this->getInput('comments');
break;
}
foreach ($subreddits as $subreddit) {
$name = trim($subreddit);
$this->collectExpandableDatas("https://www.reddit.com/r/$name/.rss");
$values = getContents(self::URI . ($user ? '/user/' : '/r/') . $name . '.json')
or returnServerError('Unable to fetch posts!');
$decodedValues = json_decode($values);
foreach ($decodedValues->data->children as $post) {
if ($post->kind == 't1' && !$comments) {
continue;
}
$data = $post->data;
if ($data->score < $this->getInput('score')) {
continue;
}
$item = array();
$item['author'] = $data->author;
$item['uid'] = $data->id;
$item['timestamp'] = $data->created_utc;
$item['uri'] = $this->encodePermalink($data->permalink);
$item['categories'] = array();
if ($post->kind == 't1') {
$item['title'] = 'Comment: ' . $data->link_title;
} else {
$item['title'] = $data->title;
$item['categories'][] = $data->link_flair_text;
$item['categories'][] = $data->pinned ? 'Pinned' : null;
$item['categories'][] = $data->spoiler ? 'Spoiler' : null;
}
$item['categories'][] = $data->over_18 ? 'NSFW' : null;
$item['categories'] = array_filter($item['categories']);
if ($post->kind == 't1') {
// Comment
$item['content']
= htmlspecialchars_decode($data->body_html);
} elseif ($data->is_self) {
// Text post
$item['content']
= htmlspecialchars_decode($data->selftext_html);
} elseif (isset($data->post_hint) ? $data->post_hint == 'link' : false) {
// Link with preview
if (isset($data->media)) {
// Reddit embeds content for some sites (e.g. Twitter)
$embed = htmlspecialchars_decode(
$data->media->oembed->html
);
} else {
$embed = '';
}
$item['content'] = $this->template(
$data->url,
$data->thumbnail,
$data->domain
) . $embed;
} elseif (isset($data->post_hint) ? $data->post_hint == 'image' : false) {
// Single image
$item['content'] = $this->link(
$this->encodePermalink($data->permalink),
'<img src="' . $data->url . '" />'
);
} elseif (isset($data->is_gallery) ? $data->is_gallery : false) {
// Multiple images
$images = array();
foreach ($data->gallery_data->items as $media) {
$id = $media->media_id;
$type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u';
$src = $data->media_metadata->$id->s->$type;
$images[] = '<figure><img src="' . $src . '"/></figure>';
}
$item['content'] = implode('', $images);
} elseif ($data->is_video) {
// Video
// Higher index -> Higher resolution
end($data->preview->images[0]->resolutions);
$index = key($data->preview->images[0]->resolutions);
$item['content'] = $this->template(
$data->url,
$data->preview->images[0]->resolutions[$index]->url,
'Video'
);
} elseif (isset($data->media) ? $data->media->type == 'youtube.com' : false) {
// Youtube link
$item['content'] = $this->template(
$data->url,
$data->media->oembed->thumbnail_url,
'YouTube');
} elseif (explode('.', $data->domain)[0] == 'self') {
// Crossposted text post
// TODO (optionally?) Fetch content of the original post.
$item['content'] = $this->link(
$this->encodePermalink($data->permalink),
'Crossposted from r/'
. explode('.', $data->domain)[1]
);
} else {
// Link WITHOUT preview
$item['content'] = $this->link($data->url, $data->domain);
}
$this->items[] = $item;
}
}
}
private function encodePermalink($link) {
return self::URI . implode(
'/',
array_map('urlencode', explode('/', $link))
);
}
private function template($href, $src, $caption) {
return '<a href="' . $href . '"><figure><figcaption>'
. $caption . '</figcaption><img src="'
. $src . '"/></figure></a>';
}
private function link($href, $text) {
return '<a href="' . $href . '">' . $text . '</a>';
}
}

View File

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

View File

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

View File

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

261
bridges/ReutersBridge.php Normal file
View File

@@ -0,0 +1,261 @@
<?php
class ReutersBridge extends BridgeAbstract
{
const MAINTAINER = 'hollowleviathan, spraynard, csisoap';
const NAME = 'Reuters Bridge';
const URI = 'https://reuters.com/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns news from Reuters';
private $feedName = self::NAME;
/**
* Wireitem types allowed in the final story output
*/
const ALLOWED_WIREITEM_TYPES = array(
'story',
'headlines'
);
/**
* Wireitem template types allowed in the final story output
*/
const ALLOWED_TEMPLATE_TYPES = array(
'story',
'headlines'
);
const PARAMETERS = array(
array(
'feed' => array(
'name' => 'News Feed',
'type' => 'list',
'title' => 'Feeds from Reuters U.S/International edition',
'values' => array(
'Aerospace and Defense' => 'aerospace',
'Business' => 'business',
'China' => 'china',
'Energy' => 'energy',
'Entertainment' => 'chan:8ym8q8dl',
'Environment' => 'chan:6u4f0jgs',
'Health' => 'chan:8hw7807a',
'Lifestyle' => 'life',
'Markets' => 'markets',
'Politics' => 'politics',
'Science' => 'science',
'Special Reports' => 'special-reports',
'Sports' => 'sports',
'Tech' => 'tech',
'Top News' => 'home/topnews',
'UK' => 'chan:61leiu7j',
'USA News' => 'us',
'Wire' => 'wire',
'World' => 'world',
)
)
)
);
/**
* Performs an HTTP request to the Reuters API and returns decoded JSON
* in the form of an associative array
* @param string $feed_uri Parameter string to the Reuters API
* @return array
*/
private function getJson($feed_uri)
{
$uri = "https://wireapi.reuters.com/v8$feed_uri";
$returned_data = getContents($uri);
return json_decode($returned_data, true);
}
/**
* Takes in data from Reuters Wire API and
* creates structured data in the form of a list
* of story information.
* @param array $data JSON collected from the Reuters Wire API
*/
private function processData($data)
{
/**
* Gets a list of wire items which are groups of templates
*/
$reuters_allowed_wireitems = array_filter(
$data, function ($wireitem) {
return in_array(
$wireitem['wireitem_type'],
self::ALLOWED_WIREITEM_TYPES
);
}
);
/*
* Gets a list of "Templates", which is data containing a story
*/
$reuters_wireitem_templates = array_reduce(
$reuters_allowed_wireitems,
function (array $carry, array $wireitem) {
$wireitem_templates = $wireitem['templates'];
return array_merge(
$carry,
array_filter(
$wireitem_templates, function (
array $template_data
) {
return in_array(
$template_data['type'],
self::ALLOWED_TEMPLATE_TYPES
);
}
)
);
},
array()
);
return $reuters_wireitem_templates;
}
private function getArticle($feed_uri)
{
// This will make another request to API to get full detail of article and author's name.
$rawData = $this->getJson($feed_uri);
$reuters_wireitems = $rawData['wireitems'];
$processedData = $this->processData($reuters_wireitems);
$first = reset($processedData);
$article_content = $first['story']['body_items'];
$authorlist = $first['story']['authors'];
$category = $first['story']['channel']['name'];
$image_list = $first['story']['images'];
$content_detail = array(
'content' => $this->handleArticleContent($article_content),
'author' => $this->handleAuthorName($authorlist),
'category' => $category,
'images' => $this->handleImage($image_list),
);
return $content_detail;
}
private function handleImage($images) {
$img_placeholder = '';
foreach($images as $image) { // Add more image to article.
$image_url = $image['url'];
$image_caption = $image['caption'];
$img = "<img src=\"$image_url\">";
$img_caption = "<figcaption style=\"text-align: center;\"><i>$image_caption</i></figcaption>";
$figure = "<figure>$img \t $img_caption</figure>";
$img_placeholder = $img_placeholder . $figure;
}
return $img_placeholder;
}
private function handleAuthorName($authors) {
$author = '';
$counter = 0;
foreach ($authors as $data) {
//Formatting author's name.
$counter++;
$name = $data['name'];
if ($counter == count($authors)) {
$author = $author . $name;
} else {
$author = $author . "$name, ";
}
}
return $author;
}
private function handleArticleContent($contents) {
$description = '';
foreach ($contents as $content) {
$data;
if(isset($content['content'])) {
$data = $content['content'];
}
switch($content['type']) {
case 'paragraph':
$description = $description . "<p>$data</p>";
break;
case 'heading':
$description = $description . "<h3>$data</h3>";
break;
case 'infographics':
$description = $description . "<img src=\"$data\">";
break;
case 'inline_items':
$item_list = $content['items'];
$description = $description . '<p>';
foreach ($item_list as $item) {
if($item['type'] == 'text') {
$description = $description . $item['content'];
} else {
$description = $description . $item['symbol'];
}
}
$description = $description . '</p>';
break;
case 'p_table':
$description = $description . $content['content'];
break;
}
}
return $description;
}
public function getName() {
return $this->feedName;
}
public function collectData()
{
$reuters_feed_name = $this->getInput('feed');
if(strpos($reuters_feed_name, 'chan:') !== false) {
// Now checking whether that feed has unique ID or not.
$feed_uri = "/feed/rapp/us/wirefeed/$reuters_feed_name";
} else {
$feed_uri = "/feed/rapp/us/tabbar/feeds/$reuters_feed_name";
}
$data = $this->getJson($feed_uri);
$reuters_wireitems = $data['wireitems'];
$this->feedName = $data['wire_name'] . ' | Reuters';
$processedData = $this->processData($reuters_wireitems);
// Merge all articles from Editor's Highlight section into existing array of templates.
$top_section = reset($processedData);
if ($top_section['type'] == 'headlines') {
$top_section = array_shift($processedData);
$articles = $top_section['headlines'];
$processedData = array_merge($articles, $processedData);
}
foreach ($processedData as $story) {
$item['uid'] = $story['story']['usn'];
$article_uri = $story['template_action']['api_path'];
$content_detail = $this->getArticle($article_uri);
$description = $content_detail['content'];
$author = $content_detail['author'];
$images = $content_detail['images'];
$item['categories'] = array($content_detail['category']);
$item['author'] = $author;
if (!(bool) $description) {
$description = $story['story']['lede']; // Just in case the content doesn't have anything.
} else {
$item['content'] = "$description $images";
}
$item['title'] = $story['story']['hed'];
$item['timestamp'] = $story['story']['updated_at'];
$item['uri'] = $story['template_action']['url'];
$this->items[] = $item;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
<?php
class SymfonyCastsBridge extends BridgeAbstract {
const NAME = 'SymfonyCasts Bridge';
const URI = 'https://symfonycasts.com/';
const DESCRIPTION = 'Follow new updates on symfonycasts.com';
const MAINTAINER = 'Park0';
const CACHE_TIMEOUT = 3600;
public function collectData() {
$html = getSimpleHTMLDOM('https://symfonycasts.com/updates/find')
or returnServerError('Unable to get page.');
$dives = $html->find('div');
/* @var simple_html_dom $div */
foreach ($dives as $div) {
$id = $div->getAttribute('data-mark-update-id-value');
$type = $div->find('h5', 0);
$title = $div->find('span', 0);
$dateString = $div->find('h5.font-gray', 0);
$href = $div->find('a', 0);
$url = 'https://symfonycasts.com' . $href->getAttribute('href');
$item = array(); // Create an empty item
$item['uid'] = $id;
$item['title'] = $title->innertext;
$item['timestamp'] = $dateString->innertext;
$item['content'] = $type->plaintext . '<a href="' . $url . '">' . $title . '</a>';
$item['uri'] = $url;
$this->items[] = $item; // Add item to the list
}
}
}

View File

@@ -21,6 +21,18 @@ class TelegramBridge extends BridgeAbstract {
private $itemTitle = '';
private $backgroundImageRegex = "/background-image:url\('(.*)'\)/";
private $detectParamsRegex = '/^https?:\/\/t.me\/(?:s\/)?([\w]+)$/';
public function detectParameters($url) {
$params = array();
if(preg_match($this->detectParamsRegex, $url, $matches) > 0) {
$params['username'] = $matches[1];
return $params;
}
return null;
}
public function collectData() {
@@ -39,8 +51,8 @@ class TelegramBridge extends BridgeAbstract {
$item = array();
$item['uri'] = $this->processUri($messageDiv);
$item['content'] = html_entity_decode($this->processContent($messageDiv), ENT_QUOTES);
$item['title'] = html_entity_decode($this->itemTitle, ENT_QUOTES);
$item['content'] = $this->processContent($messageDiv);
$item['title'] = $this->itemTitle;
$item['timestamp'] = $this->processDate($messageDiv);
$item['enclosures'] = $this->enclosures;
$author = trim($messageDiv->find('a.tgme_widget_message_owner_name', 0)->plaintext);
@@ -120,6 +132,12 @@ class TelegramBridge extends BridgeAbstract {
$messageDiv->find('div.tgme_widget_message_text.js-message_text', 0)->plaintext
);
}
if ($messageDiv->find('div.tgme_widget_message_document', 0)) {
$message .= 'Attachments:';
foreach ($messageDiv->find('div.tgme_widget_message_document') as $attachments) {
$message .= $attachments->find('div.tgme_widget_message_document_title.accent_color', 0);
}
}
if ($messageDiv->find('a.tgme_widget_message_link_preview', 0)) {
$message .= $this->processLinkPreview($messageDiv);

View File

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

View File

@@ -0,0 +1,51 @@
<?php
class TheFarSideBridge extends BridgeAbstract {
const NAME = 'The Far Side Bridge';
const URI = 'https://www.thefarside.com';
const DESCRIPTION = 'Returns the daily dose';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array();
const CACHE_TIMEOUT = 3600; // 1 hour
public function collectData() {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request: ' . self::URI);
$div = $html->find('div.tfs-page-container__cows', 0);
$item = array();
$item['uri'] = $html->find('meta[property="og:url"]', 0)->content;
$item['title'] = $div->find('h3', 0)->innertext;
$item['timestamp'] = $div->find('h3', 0)->innertext;
$item['content'] = '';
foreach($div->find('div.card-body') as $index => $card) {
$image = $card->find('img', 0);
$imageUrl = $image->attr['data-src'];
// Images are downloaded to bypass the hotlink protection.
$image = getContents($imageUrl, array('Referer: ' . self::URI))
or returnServerError('Could not request: ' . $imageUrl);
// Encode image as base64
$imageBase64 = base64_encode($image);
$caption = '';
if ($card->find('figcaption', 0)) {
$caption = $card->find('figcaption', 0)->innertext;
}
$item['content'] .= <<<EOD
<figure>
<img title="{$caption}" src="data:image/jpeg;base64,{$imageBase64}"/>
<figcaption>{$caption}</figcaption>
</figure>
<br/>
EOD;
}
$this->items[] = $item;
}
}

View File

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

View File

@@ -12,7 +12,7 @@ class TheYeteeBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request The Yetee.');
$div = $html->find('.hero-col');
$div = $html->find('.module_timed-item.is--full');
foreach($div as $element) {
$item = array();
@@ -21,16 +21,15 @@ class TheYeteeBridge extends BridgeAbstract {
$title = $element->find('h2', 0)->plaintext;
$item['title'] = $title;
$author = trim($element->find('div[class=credit]', 0)->plaintext);
$author = trim($element->find('.module_timed-item--artist a', 0)->plaintext);
$item['author'] = $author;
$uri = $element->find('div[class=controls] a', 0)->href;
$item['uri'] = static::URI . $uri;
$item['uri'] = static::URI;
$content = '<p>' . $element->find('section[class=product-listing-info] p', -1)->plaintext . '</p>';
$photos = $element->find('a[class=js-modaal-gallery] img');
$content = '<p>' . $title . ' by ' . $author . '</p>';
$photos = $element->find('a.img');
foreach($photos as $photo) {
$content = $content . "<br /><img src='$photo->src' />";
$content = $content . "<br /><img src='$photo->href' />";
$item['enclosures'][] = $photo->src;
}
$item['content'] = $content;

155
bridges/TwitScoopBridge.php Normal file
View File

@@ -0,0 +1,155 @@
<?php
class TwitScoopBridge extends BridgeAbstract {
const NAME = 'TwitScoop Bridge';
const URI = 'https://www.twitscoop.com';
const DESCRIPTION = 'Returns trending Twitter topics by country';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(
array(
'country' => array(
'name' => 'Country',
'type' => 'list',
'values' => array(
'Worldwide' => 'worldwide',
'Algeria' => 'algeria',
'Argentina' => 'argentina',
'Australia' => 'australia',
'Austria' => 'austria',
'Bahrain' => 'bahrain',
'Belarus' => 'belarus',
'Belgium' => 'belgium',
'Brazil' => 'brazil',
'Canada' => 'canada',
'Chile' => 'chile',
'Colombia' => 'colombia',
'Denmark' => 'denmark',
'Dominican Republic' => 'dominican-republic',
'Ecuador' => 'ecuador',
'Egypt' => 'egypt',
'France' => 'france',
'Germany' => 'germany',
'Ghana' => 'ghana',
'Greece' => 'greece',
'Guatemala' => 'guatemala',
'India' => 'india',
'Indonesia' => 'indonesia',
'Ireland' => 'ireland',
'Israel' => 'israel',
'Italy' => 'italy',
'Japan' => 'japan',
'Jordan' => 'jordan',
'Kenya' => 'kenya',
'Korea' => 'korea',
'Kuwait' => 'kuwait',
'Latvia' => 'latvia',
'Lebanon' => 'lebanon',
'Malaysia' => 'malaysia',
'Mexico' => 'mexico',
'Netherlands' => 'netherlands',
'New Zealand' => 'new-zealand',
'Nigeria' => 'nigeria',
'Norway' => 'norway',
'Oman' => 'oman',
'Pakistan' => 'pakistan',
'Panama' => 'panama',
'Peru' => 'peru',
'Philippines' => 'philippines',
'Poland' => 'poland',
'Portugal' => 'portugal',
'Puerto Rico' => 'puerto-rico',
'Qatar' => 'qatar',
'Russia' => 'russia',
'Saudi Arabia' => 'saudi-arabia',
'Singapore' => 'singapore',
'South Africa' => 'south-africa',
'Spain' => 'spain',
'Sweden' => 'sweden',
'Switzerland' => 'switzerland',
'Thailand' => 'thailand',
'Turkey' => 'turkey',
'Ukraine' => 'ukraine',
'United Arab Emirates' => 'united-arab-emirates',
'United Kingdom' => 'united-kingdom',
'United States' => 'united-states',
'Venezuela' => 'venezuela',
'Vietnam' => 'vietnam',
)
),
'limit' => array(
'name' => 'Topics',
'type' => 'number',
'title' => 'Number of trending topics to return. Max 50',
'defaultValue' => 20,
)
)
);
const CACHE_TIMEOUT = 900; // 15 mins
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
$updated = $html->find('time', 0)->datetime;
$trends = $html->find('div.trends', 0);
$limit = $this->getInput('limit');
if ($limit > 50 || $limit < 1) {
$limit = 50;
}
foreach($trends->find('ol.items > li') as $index => $li) {
$number = $index + 1;
$item = array();
$name = rtrim($li->find('span.trend.name', 0)->plaintext, '&nbsp');
$tweets = str_replace(' tweets', '', $li->find('span.tweets', 0)->plaintext);
$tweets = str_replace('<', '', $tweets);
$item['title'] = '#' . $number . ' - ' . $name . ' (' . $tweets . ' tweets)';
$item['uri'] = 'https://twitter.com/search?q=' . rawurlencode($name);
if ($tweets === '10K') {
$tweets = 'less than 10K';
}
$item['content'] = <<<EOD
<strong>Rank</strong><br>
<p>{$number}</p>
<Strong>Topic</strong><br>
<p>{$name}</p>
<Strong>Tweets</strong><br>
<p>{$tweets}</p>
EOD;
$item['timestamp'] = $updated;
$this->items[] = $item;
if (count($this->items) >= $limit) {
break;
}
}
}
public function getURI() {
if (!is_null($this->getInput('country'))) {
return self::URI . '/' . $this->getInput('country');
}
return parent::getURI();
}
public function getName() {
if (!is_null($this->getInput('country'))) {
$parameters = $this->getParameters();
$values = array_flip($parameters[0]['country']['values']);
return $values[$this->getInput('country')] . ' - TwitScoop';
}
return parent::getName();
}
}

View File

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

View File

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

View File

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

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