1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-15 21:14:07 +02:00

Compare commits

...

582 Commits

Author SHA1 Message Date
Dag
6c0e186d3f fix(jornaln): Array to string conversion at lib/BridgeAbstract.php li… (#3523)
* fix(jornaln): Array to string conversion at lib/BridgeAbstract.php line 320

* yup
2023-07-11 16:54:59 +02:00
Dag
c9a861e259 fix(cache): bug in prior refactor (#3520) 2023-07-09 15:24:29 +02:00
Dag
dfe78fb379 fix: various small fixes (#3519) 2023-07-09 10:08:30 +02:00
Dag
f0a504bb9a fix(FeedMerge): allow xml parse failure too (#3518) 2023-07-08 23:36:36 +02:00
Dag
7881c87bed fix: various small fixes (#3517)
* fix(asrocknews): Trying to get property src of non-object

Trying to get property 'src' of non-object at bridges/ASRockNewsBridge.php line 37

* refactor(http): tweak max redirs config

* fix(tiktok)

* fix(gizmodo)

* fix(craig)

* fix(nationalg)

* fix(roadandtrack)

* fix(etsy)
2023-07-08 23:21:55 +02:00
Dag
1a529fac46 fix(soundcloud): bug in prior cache refactor (#3516) 2023-07-08 22:53:23 +02:00
Dag
adc38e65d9 docs: add install method using composer (#3514) 2023-07-08 17:07:43 +02:00
Dag
0b95dc2d4f chore: synchronize composer.lock (#3513)
$ composer update
Loading composer repositories with package information
Updating dependencies
Info from https://repo.packagist.org: #StandWithUkraine
Lock file operations: 0 installs, 12 updates, 6 removals
  - Removing phpdocumentor/reflection-common (2.2.0)
  - Removing phpdocumentor/reflection-docblock (5.3.0)
  - Removing phpdocumentor/type-resolver (1.6.1)
  - Removing phpspec/prophecy (v1.15.0)
  - Removing symfony/polyfill-ctype (v1.25.0)
  - Removing webmozart/assert (1.10.0)
  - Upgrading doctrine/instantiator (1.4.1 => 1.5.0)
  - Upgrading myclabs/deep-copy (1.11.0 => 1.11.1)
  - Upgrading nikic/php-parser (v4.13.2 => v4.16.0)
  - Upgrading phpunit/php-code-coverage (9.2.15 => 9.2.26)
  - Upgrading phpunit/phpunit (9.5.20 => 9.6.9)
  - Upgrading sebastian/comparator (4.0.6 => 4.0.8)
  - Upgrading sebastian/diff (4.0.4 => 4.0.5)
  - Upgrading sebastian/environment (5.1.4 => 5.1.5)
  - Upgrading sebastian/exporter (4.0.4 => 4.0.5)
  - Upgrading sebastian/recursion-context (4.0.4 => 4.0.5)
  - Upgrading sebastian/type (3.0.0 => 3.2.1)
  - Upgrading squizlabs/php_codesniffer (3.6.2 => 3.7.2)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 29 installs, 0 updates, 0 removals
  - Installing sebastian/version (3.0.2): Extracting archive
  - Installing sebastian/type (3.2.1): Extracting archive
  - Installing sebastian/resource-operations (3.0.3): Extracting archive
  - Installing sebastian/recursion-context (4.0.5): Extracting archive
  - Installing sebastian/object-reflector (2.0.4): Extracting archive
  - Installing sebastian/object-enumerator (4.0.4): Extracting archive
  - Installing sebastian/global-state (5.0.5): Extracting archive
  - Installing sebastian/exporter (4.0.5): Extracting archive
  - Installing sebastian/environment (5.1.5): Extracting archive
  - Installing sebastian/diff (4.0.5): Extracting archive
  - Installing sebastian/comparator (4.0.8): Extracting archive
  - Installing sebastian/code-unit (1.0.8): Extracting archive
  - Installing sebastian/cli-parser (1.0.1): Extracting archive
  - Installing phpunit/php-timer (5.0.3): Extracting archive
  - Installing phpunit/php-text-template (2.0.4): Extracting archive
  - Installing phpunit/php-invoker (3.1.1): Extracting archive
  - Installing phpunit/php-file-iterator (3.0.6): Extracting archive
  - Installing theseer/tokenizer (1.2.1): Extracting archive
  - Installing nikic/php-parser (v4.16.0): Extracting archive
  - Installing sebastian/lines-of-code (1.0.3): Extracting archive
  - Installing sebastian/complexity (2.0.2): Extracting archive
  - Installing sebastian/code-unit-reverse-lookup (2.0.3): Extracting archive
  - Installing phpunit/php-code-coverage (9.2.26): Extracting archive
  - Installing phar-io/version (3.2.1): Extracting archive
  - Installing phar-io/manifest (2.0.3): Extracting archive
  - Installing myclabs/deep-copy (1.11.1): Extracting archive
  - Installing doctrine/instantiator (1.5.0): Extracting archive
  - Installing phpunit/phpunit (9.6.9): Extracting archive
  - Installing squizlabs/php_codesniffer (3.7.2): Extracting archive
Generating autoload files
25 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
2023-07-08 17:07:35 +02:00
Dag
61b307a9f9 fix(tiktok): feed item link (#3511)
* fix(tiktok): feed item link

* fix(tiktok): support entire url, for convenience
2023-07-08 17:07:20 +02:00
Dag
341649a8a4 fix: tweak the defaultly enabled bridges (#3510)
* fix: tweak the defaultly enabled bridges

enabled_bridges[] = FeedMerge
enabled_bridges[] = Filter
enabled_bridges[] = GettrBridge
enabled_bridges[] = MastodonBridge
enabled_bridges[] = Reddit
enabled_bridges[] = RumbleBridge
enabled_bridges[] = SoundcloudBridge
enabled_bridges[] = Telegram
enabled_bridges[] = ThePirateBay
enabled_bridges[] = Twitch
enabled_bridges[] = Twitter
enabled_bridges[] = Vk
enabled_bridges[] = XPathBridge
enabled_bridges[] = Youtube
enabled_bridges[] = YouTubeCommunityTabBridge

* add feed reducer too

* add tiktok too
2023-07-08 17:07:08 +02:00
Dag
91976f7d56 fix(file cache): acquire lock before writing (#3509) 2023-07-08 17:06:49 +02:00
Dag
8b996e3056 refactor: display action (#3508) 2023-07-08 17:06:33 +02:00
Dag
c1c8304fc0 refactor: dont create multiple instances of the cache (#3504) 2023-07-08 17:03:12 +02:00
Dag
b594ad2de3 fix: discard empty lines in whitelist.txt (#3507) 2023-07-07 11:25:36 +02:00
User123698745
ef0b86968c [PicnobBridge] fix missing images (#3506)
* [PicnobBridge] fix missing images

* [PicnobBridge] handle invalid relative date (e.g.: 'Just now')

* [PicnobBridge] fix code indent
2023-07-07 08:16:45 +02:00
somini
d49ea235f0 [JornalDeNoticiasBridge]: Remove bridge (#3505)
This is now broken, it was replaced by a React monstrosity. The API
doesn't even let me read the post data, it demands authentication.
Bummer.

Better remove it now, since it's worthless.
2023-07-07 08:14:31 +02:00
Dag
46f0e97c73 refactor: remove useless actual args to clearstatcache (#3503) 2023-07-06 19:39:40 +02:00
Dag
5e22459eb6 fix: remove unnecessary calls to purgeCache (#3502) 2023-07-06 18:52:19 +02:00
Dag
965d7d44c5 feat(sqlite cache): add config options (#3499)
* refactor: sqlite cache

* refactor

* feat: add config options to sqlite cache

* refactor
2023-07-06 15:59:38 +02:00
Dag
21c8d8775e fix: bug in #3442 (#3498)
* fix: bug in #3442

* docs: add inline doc on how to enable all bridges
2023-07-06 15:20:56 +02:00
Dag
caac7f572c refacor: improve cache interface (#3492)
* fix: proper typehint on setScope

* refactor: type hint setKey()

* typehint
2023-07-06 15:10:30 +02:00
Dag
f8801d8cb3 feat: add system config enable_maintenance_mode (#3497) 2023-07-06 15:09:44 +02:00
Dag
8f9147458d fix(gatesnotes): fix #3493 (#3494) 2023-07-06 03:36:01 +02:00
Dag
a9fd3b9e61 fix(CacheInterface): logic bug in getTime (#3491)
* fix(CacheInterface): logic bug in getTime

* test
2023-07-05 17:37:21 +02:00
Dag
18e1597361 fix(ph): consent cookie (#3490) 2023-07-05 17:06:23 +02:00
Patrick
e9af41d666 Add bridges for JohannesBlick Steinfeld, OM Online and UsesTech (#3489)
* Add bridges for JohannesBlick Steinfeld, OM Online and UsesTech

* Fixed linit alert
2023-07-05 16:43:59 +02:00
Dag
354317d010 fix(rumble): add timestamp to items (#3485) 2023-07-05 05:41:33 +02:00
Dag
84501cfc00 feat: add health check action (#3484) 2023-07-05 05:41:20 +02:00
Dag
82c22bd2b5 fix(feedmerge): allow a single feed to break, and dont break the whole bridge (#3476) 2023-07-05 05:41:01 +02:00
Dag
a21d496bc7 feat: add default arg to Configuration::getConfig (#3331) 2023-07-05 05:33:22 +02:00
Dag
cf920694d5 fix(futurasciences): fix broken bridge (#3488) 2023-07-04 23:34:24 +02:00
Thomas
bf0d771367 [YouTubeCommunityTabBridge] Fix getURI implementation
Previously the undefined property "feedUri" was accessed here, always
causing a fallback to the parent class
2023-07-04 17:03:30 +02:00
Dag
48385777b4 fix: php notices (#3482)
* fix(furaffinity): notice

* fox(releases3ds): remove references to non-existing vars
2023-07-03 10:48:33 +02:00
Dag
d8bc015efc fix: php notices (#3479)
* fix(jornaln): A non well formed numeric value encountered

fixes

A non well formed numeric value encountered at bridges/JornalNBridge.php line 89

* fix(reuters): fix notice
2023-07-03 00:39:01 +02:00
Eugene Molotov
0f14a0f6ee [YandexZenBridge] Add image to post (#3478) 2023-07-02 22:56:45 +02:00
Thomas
eb2b4747ae [YouTubeCommunityTabBridge] Add timestamps (#3477)
As YouTube doesn't provide precise dates for community posts, an
increasing multiple of 60 seconds is subtracted from each timestamp.
This ensures that the original order is always preserved, even if there
are multiple posts with the same date (e.g. "1 month ago").
2023-07-02 22:56:08 +02:00
Dag
bf73372d7f fix: dont be case-sensitive on env vars (#3475) 2023-07-02 06:47:21 +02:00
Dag
748fc9fd65 fix: various small notice fixes (#3474)
* fix(patreon): php notice

* fix(pepperbridge): php notice

* fix(ebay): php notice

* fix(tiktok): php notice

* fix(yandex): fix notice

* fix(justwatch): notice

* lint
2023-07-02 06:40:25 +02:00
Dag
372880b5ef fix: file cache tweaks (#3470)
* fix: improve file cache

* fix(filecache): log when unserialize fails
2023-06-30 22:31:19 +02:00
rmscoelho
cc91ee1e37 [JornalNBridge] getName() fix (#3456)
* [JornalNBridge] getName() fix

* [JornalNBridge] feed fixes
2023-06-30 15:54:53 +02:00
Arnav Jain
fece9ed344 [NotAlwaysBridge] Add new categories, remove duplicate header, and social meta (#3463)
* [NotAlwaysBridge] add new tags

* [NotAlwaysBridge] Remove duplicate header and social meta

* [NotAlwaysBridge] Add space to fix lint issues
2023-06-30 15:50:54 +02:00
Bocki
b6a263037a [core] Fix prtester for optgroups (#3467) 2023-06-30 15:41:00 +02:00
Mynacol
410ef85618 [GolemBridge] Strip <script> tags
Golem articles referencing their podcast contain a JavaScript reference
to Podigee. We don't want JS, so we strip it.

Example page: https://www.golem.de/news/podcast-besser-wissen-von-schlangenoel-und-sicherheit-2306-175185.html
2023-06-28 16:01:32 +02:00
Dag
8eabdbe5f8 fix(Twitter): properly find time line entries when ordering is inconsistent (#3461) 2023-06-27 16:11:41 +02:00
António Pereira
bd6f56383c [PresidenciaPTBridge]: Fix timestamp search (#3459) 2023-06-26 17:52:32 +02:00
ORelio
d4bc63ee98 [TheHackerNews] Update content extraction (#3458) 2023-06-25 19:01:57 +02:00
rmscoelho
1b02d4f49b [CorreioDaFeiraBridge] cache timeout + getName fixes (#3453)
* [CorreioDaFeiraBridge] cache timeout fix

* [CorreioDaFeiraBridge] cache timeout fix

* [CorreioDaFeiraBridge] getName() fix
2023-06-22 07:27:52 +02:00
rmscoelho
a4ed52ca30 [VideoCardzBridge] cache timeout fix + getName fixes (#3454)
* [VideoCardzBridge] cache timeout fix

* [VideoCardzBridge] getName() + title fix
2023-06-22 07:27:30 +02:00
rmscoelho
1769399da8 [ABolaBridge] cache timeout fix + getName fixes (#3455)
* [ABolaBridge] cache timeout fix

* [ABolaBridge] fix timestamp and image alt null

* [ABolaBridge] formatting fixes

* [ABolaBridge] getName() fix
2023-06-22 07:27:01 +02:00
rmscoelho
12ba6154f9 [JornalNBridge] cache timeout fix (#3452)
* [JornalNBridge] cache timeout fix

* [JornalNBridge] cache timeout fix
2023-06-21 11:15:36 +02:00
rmscoelho
8e35ebf482 [New Bridge] Jornal N (Portuguese local newspaper) (#3451)
* [New Bridge] Jornal N (Portuguese local newspaper)

* [JornalNBridge] formatting fixes
2023-06-21 05:17:11 +02:00
rmscoelho
61130e89b4 [ABolaBridge] timestamp (#3448)
* [ABolaBridge] timestamp

* [ABolaBridge] formatting fixes
2023-06-21 05:15:01 +02:00
rmscoelho
ebebb886c5 [ABolaBridge] feed fixes (#3446)
* [ABolaBridge] category name and url fixes

* [ABolaBridge] "Mercado" feed fix; feed name fix; img fix;

* [ABolaBridge] formatting fix
2023-06-20 17:26:55 +02:00
Matt Connell
6eaa31b999 [New Bridge] WYMT news bridge (#3444)
* feat: add WYMT bridge

* fix: phpcs error
2023-06-20 15:13:41 +02:00
rmscoelho
1d3888f22a [VideoCardzBridge] category name and url fixes (#3447)
* [VideoCardzBridge] category name and url fixes

* [VideoCardzBridge] error fixes

* [VideoCardzBridge] formatting fix

* [VideoCardzBridge] cache timeout removal
2023-06-20 15:01:41 +02:00
rmscoelho
60be4cdebd [CorreioDaFeiraBridge] adding timestamps; fixing categories; (#3445)
* [New Bridge] Correio da Feira (regional newspaper)

* [CorreioDaFeiraBridge] adding timestamp; fixing name

* [CorreioDaFeiraBridge] formatting fixes
2023-06-20 12:46:24 +02:00
rmscoelho
5a0bacbd8a [New Bridge] Videocardz.com bridge (#3442)
* [New Bridge] Videocardz.com Bridge

* [New Bridge] Videocardz.com Bridge

* [Videocardz.com] cache timeout increase

* [VideoCardzBridge] cache timeout change

* [VideoCardzBridge] formatting

* [VideoCardBridge] formatting fixes

* [VideoCardzBridge] formatting fixes
2023-06-20 12:45:50 +02:00
rmscoelho
0c808dc3a1 [New Bridge] Bridge for sports website A Bola (#3441)
* [New Bridge] Bridge for sports website A Bola

* [ABolaBridge] add thumbnail

* [ABolaBridge] formatting

* [ABolaBridge] formatting fixes

* [ABolaBridge] formatting fixes

* [ABolaBridge] formatting fixes
2023-06-20 12:45:34 +02:00
rmscoelho
98b72b2c5c [New Bridge] Correio da Feira (regional newspaper) (#3443) 2023-06-20 05:57:22 +02:00
Thomas
54d626d5cd [XPathAbstract] Use baseURI to fix relative links (if available) (#3439) 2023-06-17 17:53:00 +02:00
somini
1e470ef341 [PresidenciaPTBridge]: Fix title search (#3438)
This was changed on the site itself, in the last few days.
2023-06-17 06:13:09 +02:00
Dag
0a8fe57003 feat: enable bridges using env var (#3428)
* refactor: bridgefactory, add tests

* refactor: move defaultly enabled bridges to config

* refactor

* refactor

* feat: add support for enabling bridges with env var
2023-06-11 03:16:03 +02:00
Nick McCarthy
d9490c6518 GoogleScholarV2Bridge (#3415)
* Added google scholar v2 bridge with more functionality

* Corrected Sort By interpretation (this is weird on Googles part)

* Remove some debug statements

* Merged GoogleScholarBridge and GoogleScholarV2Bridge into GoogleScholarBridge with two contexts.

* Left V2 in Bridge Name

* Lint

* Update GoogleScholarBridge.php

* Update GoogleScholarBridge.php

* Lint.

* ;
2023-06-10 18:35:04 +02:00
Jisagi
eb799e59a6 [NyaaTorrentsBridge] Add custom fields (#3420)
* Update NyaaTorrentsBridge.php

* lint

* lint #2

* Sir Lint the Third

* Add torrent id to custom fields

* Proposed improvements
2023-06-10 18:28:00 +02:00
Eugene Molotov
ec1a3f4fe3 [YoutubeBridge] Unassign maintainer (#3431) 2023-06-10 18:27:49 +02:00
Eugene Molotov
80376830c5 [VkBridge] Handle some secondary attachments (#3430) 2023-06-10 18:27:32 +02:00
Shikiryu
e859497d6a [PicalaBridge] Fix article without image (#3429)
Co-authored-by: Clement Desmidt <clement@desmidt.fr>
2023-06-09 17:30:11 +02:00
Dag
ca351edbfe test: use correct path for bridges (#3427) 2023-06-08 23:44:26 +02:00
Dag
8f9eaae338 chore: fix ci (#3426) 2023-06-08 23:37:36 +02:00
Dag
fbaf26e8bf fix(html_format): add spacing below date if author is missing (#3425)
* small ui tweak

* remove unused <div>

* refactor: rename method

* refactor: inline const

* refactor
2023-06-08 23:04:16 +02:00
Simon Alberny
95071d0134 Add RemixAudioBridge (#3424) 2023-06-07 22:37:38 +02:00
Simon Alberny
3f8165207e Add RainLoopBridge (#3423) 2023-06-07 22:36:51 +02:00
Simon Alberny
08be0ad7a5 Add GoAccessBridge (#3422) 2023-06-07 22:36:21 +02:00
Simon Alberny
6fa1f349d9 Add AllocineFRSortiesBridge (#3421) 2023-06-07 22:35:54 +02:00
July
54957d2a03 [GameBananaBridge] Load all full quality screenshots (#3419)
Replaces the low quality preview images used previously
2023-06-06 20:00:55 +02:00
Tone
819e453064 remove newsletter ad from finanzflussBridge (#3417)
* remove newsletter ad

* whitespace
2023-06-02 20:28:29 +02:00
Dag
1636a84c25 fix(spotify): use non-predictable cache key (#3330)
* refactor

* fix(spotify): use non-predictable cache key
2023-06-02 20:22:28 +02:00
Dag
ee498eadf9 fix: move debug mode to config (#3324)
* fix: move debug mode to config

* fix: also move debug_whitelist to .ini config

* fix: move logic back to Debug class

* docs

* docs

* fix: disable debug mode by default

* fix: restore previous behavior for alerts

* fix: center-align alert text
2023-06-02 20:22:09 +02:00
Ryan Stafford
c5cd229445 [YoutubeBridge] Set icon (#3416) 2023-06-01 21:26:47 +02:00
July
845a8f7936 [MangaDexBridge] Add option to add chapter images to entries (#3412) 2023-05-28 18:23:01 +02:00
aysilu-kitsune
2f0784c287 added my instance (#3413) 2023-05-28 18:21:44 +02:00
piyushpaliwal
227c7b8968 Sleeper.com Alerts. Fixes #2234 (#3411)
* Sleeper.com Alerts. Fixes #2234

* fix: linter issue
2023-05-28 01:31:45 +02:00
July
01f731cfa4 [GameBananaBridge] Create new bridge (#3410) 2023-05-26 18:19:34 +02:00
mrnoname1000
87b9f2dd94 [core] Fix XPathAbstract while working around Simple HTML DOM bug (#3408) 2023-05-21 21:06:35 +02:00
Dag
f803ffa79a fix: ArgumentCountError: DOMDocument::getElementsByTagName() expects exactly 1 argument, 2 given, #3406 (#3407) 2023-05-21 19:59:39 +02:00
mrnoname1000
b5dbec4cc1 [AllSidesBridge] New bridge (#3405) 2023-05-20 04:15:56 +02:00
mrnoname1000
3e0d024888 [core] Fix defaultLinkTo for simple_html_dom objects (#3404) 2023-05-20 00:02:17 +02:00
Dag
cfe81ab2ac fix: Call to a member function setAttribute() on int, #3402 (#3403) 2023-05-19 16:05:52 +02:00
mrnoname1000
096c3bca73 [XPathAbstract] Fix relative links in fetched HTML (#3401)
* [core] Make defaultLinkTo compatible with DOMDocument

* [XPathAbstract] Fix relative links in fetched HTML
2023-05-18 13:50:50 +02:00
Tone
ecd717cf58 removing a-collapse (#3394)
it is only used for ads for their magazine
e.g.: https://www.heise.de/news/Eventtipps-fuer-Fotografen-und-Fotografiebegeisterte-9010049.html?seite=all
2023-05-12 23:41:08 +02:00
Tone
0c540b4637 added script to deleted elements in CaschyBridge (#3391)
* added script to deleted elements

Now it works much better with included content like twitter, e.g. in this article:
https://stadt-bremerhaven.de/1password-mit-android-14-wird-man-passkeys-in-chrome-und-apps-unterstuetzen/

* Update CaschyBridge.php

* Update CaschyBridge.php
2023-05-11 21:25:13 +02:00
mrnoname1000
d0f7f5e2d8 [New Bridge] FiderBridge (#3378)
* [core] Add config parameter to markdownToHtml

* [FiderBridge] New bridge
2023-05-11 21:24:12 +02:00
Alexandre Alapetite
e99e026fa8 Use standard Docker logs (#3333)
Instead of storing logs inside the container (where then cannot easily be seen not rotated), consider using the standard Docker approach of writing to standard output
https://docs.docker.com/config/containers/logging/
2023-05-11 01:44:11 +02:00
Dag
50865d5741 fix: add additional cloudflare title (Glassdoor specific) (#3342) 2023-05-11 01:33:38 +02:00
July
dc4134ed1d [ScribbleHubBridge] Add CloudFlare error handling (#3361)
* [ScribbleHubBridge] Set html defaultLinkTo

* [ScrubbleHubBridge] Add CloudFlare error handling
2023-05-11 01:33:21 +02:00
mrnoname1000
c6c4b3a24f [XPathAbstract] Fix encoding on feed title (#3365) 2023-05-11 01:32:01 +02:00
mrnoname1000
63dc500ae0 [XPathAbstract] Save HTML for entry content (#3366) 2023-05-11 01:31:34 +02:00
vincentvd1
723768c828 Add bridge for Magellantv articles (#3368)
* [MagellantvBrdige] added first version

* [MagellantvBridge]  cleanup, added tags and fixed bugs

* [MagellantvBridge] fix linting issues

* [MagellantvBridge] more linting fixes

* [MagellantvBridge] removed tabs
2023-05-11 01:30:25 +02:00
Tone
e7bda080b4 added iframe to $bad (#3380)
iframe can't be rendered in feed reader, so we can delete it
2023-05-10 22:14:34 +02:00
Joseph
8fd677f4ae [GithubTrendingBridge] Fix items (#3381) 2023-05-10 22:14:21 +02:00
Dag
88f646cf12 fix(TwitterBridge): trim screen name before passing it to twitter client (#3389) 2023-05-10 21:59:50 +02:00
Dag
49d105fd70 fix(TwitterBridge): remove ampersand from screen name, api dont like it (#3388) 2023-05-10 21:55:47 +02:00
Dag
ff49c9f731 fix(TwitterBridge): repair fetching of tweets by username (#3385)
* feat: alpha version of new twitter bridge

* fix: refetch guest_token if expired

* fix: purge cache

* fix: safeguards

* fix

* fix: two notices

* fix

* fix: use factory to create cache

* fix: fail properly instead of die()
2023-05-10 21:45:44 +02:00
Max
c628f99928 use lowercase 2023-05-10 18:40:33 +02:00
Tone
f26808d22c added article categories for CaschyBridge (#3379)
* added article categories

* whitespace
2023-05-08 16:21:39 +02:00
Tone
a1b6bca581 added article categories for GolemBridge (#3377)
* added article categories for GolemBridge

* tabs are bad, spaces good

* fixed duplicate categories on multi-page articles
2023-05-08 16:21:03 +02:00
Tone
ec091fb747 fixed authors and added categories for HeiseBridge (#3376) 2023-05-07 12:33:45 +02:00
mrnoname1000
887f4bbe15 [BugzillaBridge] Explicitly request JSON (#3364) 2023-04-27 19:24:29 +02:00
Paul Prechtel
212c56fde5 [HeiseBridge] Handle heise+ articles better (#3358)
- Stop parsing paywalled heise+ articles, as they had garbage content
  and anyways not the full article.
- Link to archive.today to access the full article without account.
  (Automatically getting the full article from archive.ph was not feasible
  b/c of captchas and problems extracting the actual content)
2023-04-20 23:02:08 +02:00
sysadminstory
00e716d84d [PepperBridgeAbstract] Fix "no results" check (#3357)
CSS class for "no results" text has changed, so the bridge has been
updated accordingly.
2023-04-20 11:22:53 +02:00
July
f0c96008bc [ScribbleHubBridge] Create new bridge (#3353)
* [ScribbleHubBridge] Create new bridge

* [ScribbleHubBridge] Improve 'Series' filtering

* [ScribbleHubBridge] Properly fetch feed name

* [ScribbleHubBridge] Fix feed name and set feed URI

* [ScribbleHubBridge] Fix linting violations with phpcbf

* [ScribbleHubBridge] Properly handle html encoding in titles
2023-04-19 20:35:04 +02:00
Eugene Molotov
343fd36671 [core] Remove hardcoded maximum duration of 24 hours in loadCacheValue (#3355) 2023-04-19 17:53:35 +02:00
Paul Prechtel
a4a7473abb [Docs] Fix link to SimpleHTMLDOM documentation (#3354) 2023-04-19 17:51:55 +02:00
Paul Prechtel
4068668de9 [ZeitBridge] Re-add paywall workaround (#3352)
Additionally to the Googlebot User-Agent, a Googlebot IP address has to
be used. For now, we can use `X-Forwarded-For` for this.
2023-04-18 18:41:40 +02:00
Eugene Molotov
7c4591c550 [VkBridge] Add detectParameters (#3351) 2023-04-18 18:41:11 +02:00
Paul Prechtel
0718fdc829 [ZeitBridge] Revert User-Agent (#3350)
The Googlebot User-Agent is no longer sufficient to circumvent the
paywall.
2023-04-17 15:33:14 +02:00
Dawid Wróbel
7eca527160 [eBayBridge] New bridge (#3349)
Fixes #3268
2023-04-15 18:40:49 +02:00
triatic
f1c54d5d55 [TwitterV2.md] Update to reflect price change to Twitter API (#3347) 2023-04-14 21:32:13 +02:00
Korytov Pavel
1ed7bdcddf [InternationalInstituteForStrategicStudiesBridge] Repair and improve bridge (#3338)
* [InternationalInstituteForStrategicStudiesBridge] Repair and improve bridge

* [InternationalInstituteForStrategicStudiesBridge] Fix lint
2023-04-08 22:09:07 +02:00
Paroleen
8486c0f8ca [SpotifyBridge] Add podcasts feed (#3329)
Co-authored-by: Matteo Parolin <matteoparolin99@gmail.com>
2023-03-24 20:34:51 +01:00
Eugene Molotov
249133204e [UI] Remove excessive top margin in all pages and logo in HTML feed preview (#3326) 2023-03-24 13:44:34 +01:00
Eugene Molotov
c8af9f9055 [VkBridge] Make timestamps more accurate (#3325) 2023-03-22 20:32:15 +01:00
Dag
9bb04ba848 Prepare 2023-03-21 release (#3323)
* fix: upgrade version string in php code
2023-03-22 19:32:19 +01:00
realansgar
307f5865c0 [ARDAudiothekBridge] fix feed icon not showing in RSS feeds (#3274)
* [ARDAudiothekBridge] fix feed icon not showing in RSS feeds

* [ARDAudiothekBridge] Fix lint errors
2023-03-21 18:24:28 +01:00
DRogueRonin
36e98e8481 docs: add docker development environment example (#3319) 2023-03-20 19:13:08 +01:00
Dag
347a0e9a3d fix: patch simple_html_dom, #3309 (#3310) 2023-03-20 19:12:13 +01:00
Dag
4c3ebb312d feat: improve error handling ux (#3298)
* feat: improve error handling ux

* feat: add error messages for failed xml parsing
2023-03-20 19:11:51 +01:00
Dag
9e9a697b8b feat: add config option "path" for file cache (#3297) 2023-03-20 19:10:01 +01:00
Miika Launiainen
4e616c7092 [YorushikaBridge] Replace YouTube embeds with YouTube link (#3321) 2023-03-19 12:50:04 +01:00
toineenzo
fbe7cc11ec Add more countries to App Store Bridge (#3246)
* Added more countries

* Fixed Brazil typo

* Update AppleAppStoreBridge.php

Removed whitespace line 52 for lint fix
2023-03-18 19:55:23 +01:00
sysadminstory
23fb5819cd [FreeTelechargerBridge] New bridge (#3318)
* [FreeTelechargerBridge] New bridge

New bridge

* [FreeTelechargerBridge ] Fix CACHE_TIMEOUT value

Fixed CACHE_TIMEOUT value
2023-03-16 09:35:49 +01:00
vdbhb59
dc8ce20482 Update 06_Public_Hosts.md (#3311)
Added my instance of public hosted RSS-Bridge host.
2023-03-14 15:55:14 +01:00
mad-reyk
224cce08a8 [Sfeed] Fixed category separator and random white spaces (#3308) 2023-03-12 15:21:21 +01:00
mad-reyk
c1f446fd19 [Sfeed] Added new format (#3306)
* [Sfeed] Added new format

* [Sfeed] Spaces instead of tabs

* [Sfeed] Move all global functions to class and fix phpcs warnings
2023-03-12 00:13:27 +01:00
Corentin Garcia
19fc2dc100 [GatesNotesBridge] Fix bridge (fix #3294) (#3305) 2023-03-11 23:26:22 +01:00
Dag
2c94791bcd fix: skip yt json if absent, fix #3301 (#3302) 2023-03-11 20:06:01 +01:00
Dag
1ffb2df46d New bridge (#3300)
Create rss feed from wallpapers published on erowall.com.

Allow fetching n latest wallpapers sorted by date, views, downloads and
tags.

Co-authored-by: Kurz Junge <kurz.junge.0xa@tutanota.com>
2023-03-11 01:41:02 +01:00
Miika Launiainen
dc9530b405 [YorushikaBridge] Created the bridge (#3299) 2023-03-09 18:36:51 +01:00
Bocki
90bf5518cb [Core] Activate live linting in codespaces (#3293)
* [Core] Add live linting to devcontainer

* Deactivate lint on type
2023-03-08 18:39:50 +01:00
Bocki
783160e715 [nginx] Add ipv6 listener (#3292) 2023-03-07 23:59:22 +01:00
Bocki
0a114c02c2 [Docu] Clarify docker instructions (#3291) 2023-03-07 23:58:21 +01:00
Bocki
2abdc7588a [Docu] Add documentation for Codespaces (#3289)
* [Docu] Add documentation for Codespaces

* Adapt PR line
2023-03-07 21:34:02 +01:00
Bocki
84e0135959 [Core] github codespaces setup (#3287) 2023-03-07 17:10:36 +01:00
Korytov Pavel
f7200756c3 [InternationalInstituteForStrategicStudiesBridge] Add bridge (#3286)
* [InternationalInstituteForStrategicStudiesBridge] Add bridge

* [InternationalInstituteForStrategicStudiesBridge] Fix lint errors
2023-03-07 17:03:50 +01:00
sysadminstory
b8ad49c562 [ExtremeDownload] Remove Bridge (#3285)
The Website has been taken down, this bridge is not needed anymore.
2023-03-07 01:02:51 +01:00
Dag
058e792b8f feat: add filecache config to enable/disable real purge (#3263)
* refactor: cachefactory

* feat: add filecache config to enable/disable real purge

* test: fix test
2023-03-06 21:50:40 +01:00
Dag
007f2b2d8a feat: sanitize root folder also in php error messages (#3262) 2023-03-06 21:47:25 +01:00
Dag
a01c1f6ab0 fix: disallow usage of default password (#3284) 2023-03-06 20:43:44 +01:00
Bocki
f0e5ef0fc5 [Various] getKey replacements and docu (#3283)
* [Various] getKey replacements and docu

* more bridges and fix to the abstract

* linting

* revert bandcampdaily. doing more than i thought
2023-03-06 20:01:51 +01:00
Tone
b40714079f Create FinanzflussBridge.php (#3282)
* Create finanzflussBridge.php

new bridge for finanzfluss.de

* Pascal case

* Rename finanzflussBridge.php to FinanzflussBridge.php

* Update FinanzflussBridge.php

more spaces!
2023-03-05 23:45:45 +01:00
Simon
180c332406 Update 06_Public_Hosts.md (#3280)
Change location for bridge.easter.fr
2023-03-05 00:37:42 +01:00
Joseph
8c4dbb32de [DockerHubBridge] Display compressed image size in items (#3279)
* [DockerHubBridge] Display compressed image size in items

* [DockerHubBridge] lint

* [DockerHubBridge] Use format_bytes()
2023-03-04 17:33:28 +01:00
Ololbu
5ab949ca55 [FicbookBridge] Fix new lines in content (#3278)
* [FicbookBridge] Fix new lines in content

Sets `$stripRN` in `getSimpleHTMLDOMCached` to `false` and replace new line to `br` through `str_replace()`.

* [FicbookBridge] Add space after comma
2023-03-04 16:12:46 +01:00
Bocki
f3f98a117c [Core] Add getKey function (#3275)
* [Core] Add getKey function
2023-03-02 13:25:57 +01:00
Bocki
f0d8cfd4d4 [JustWatchBridge] New bridge (#3273)
* [JustWatchBridge] New bridge
2023-03-01 20:24:01 +01:00
Korytov Pavel
4aed05c7b6 [TldrTechBridge] Add AI section (#3272) 2023-02-28 17:28:33 +01:00
Mynacol
4450e9b973 Let curl select the default HTTP version (#3249)
This essentially reverts b042412416,
as YouTube seems to have fixed their servers.
At least I was able to query the YouTube endpoint around 150 times with
CURL_HTTP_VERSION_2TLS recently. They even advertise HTTP/3 support with
an `alt-svc` HTTP header now.

This unsets CURLOPT_HTTP_VERSION to let curl decide
on the version. This would support all curl versions and opens the
possibility for HTTP/3, but leads to inconsistent behavior depending
on the underlying curl version.

We don't set CURL_HTTP_VERSION_NONE explicitly, as it is always the curl
default and opens the path to let individual bridges override the HTTP
version where necessary.

Alternatively, setting CURL_HTTP_VERSION_2TLS explicitly would lead to consistent behavior
regardless of the curl version, but might uncover old curl bugs before the
developers enabled HTTP/2 by default.
Additionally, that requires at least PHP 7.0.7 (we require PHP 7.4
already) and curl 7.47.0 [1], released on Jan 27 2016 [2].

See also the discussion on https://github.com/RSS-Bridge/rss-bridge/pull/3249

[1] https://www.php.net/manual/curl.constants.php
[2] https://curl.se/docs/releases.html
2023-02-22 17:48:39 +01:00
sysadminstory
7783e4133d [PicnobBridge] Update description & uid (#3267)
- Description now allows HTML and relative URLs are rewritten
- Post ID in the post URL is used as UID in the feed
2023-02-21 00:56:17 +01:00
Erisa A
4cfe69abb4 Update public instance (#3266) 2023-02-20 12:53:23 +01:00
Predä
db437b6326 [PicukiBridge] Add source field (#3265)
* [PicukiBridge] Add source_url field

* Simplify the regex matching.

* Add picuki source for feeds
2023-02-19 01:35:28 +01:00
Tone
5d4247dded Update CaschyBridge.php (#3261)
prevents the bug #3259 that quote-blocks will be displayed multiple times
2023-02-15 22:20:22 +01:00
Dag
286790727b fix: throw exception when outbox not found, #3255 (#3260) 2023-02-15 21:42:05 +01:00
Dag
c27a300e02 test: add failing mastodon test (#3255)
* fix: refactor cache factory

* test: add failing test

* add null cache
2023-02-15 21:22:37 +01:00
Joseph
787b4d7cad [DockerHubBridge] Add tag filter option (#3258)
* [DockerHubBridge] Add tag filter option

* [DockerHubBridge] Add example value

* [DockerHubBridge] lint

* [DockerHubBridge] Fix

* Update DockerHubBridge.php

* [DockerHubBridge] Make repo required

* [DockerHubBridge] Add filter example value for user images
2023-02-15 20:15:38 +01:00
Dag
17fcc72b09 chore: upgrade to php 8 in the Dockerfile, fix #3191 (#3256) 2023-02-13 22:21:07 +01:00
Joseph
67f72bfa5d [TelegramBridge] Support new username URL format (#3257)
* [TelegramBridge] Support new username URL format

* [TelegramBridge] Fix text error
2023-02-13 22:19:41 +01:00
sysadminstory
06a4bc4b45 [DealabsBridge-HotUKDealsBridge-MydealsBridge] (#3253)
Fix CSS class for the deal temperature
2023-02-10 06:16:01 +01:00
SergioFLS
ed36c8cbcd [ItchioBridge] add error for password pages (#3252) 2023-02-08 19:50:21 +01:00
sravan
4dc6158d5c fix badge (#3251) 2023-02-08 19:48:54 +01:00
Mynacol
38e832daae [GolemBridge] Remove link from author (#3248)
Fixes #3224.
2023-02-08 19:44:01 +01:00
Eugene Molotov
91f91ba621 [VkBridge] Follow site changes (#3244) 2023-02-07 15:10:43 +01:00
Dag
7f1b32f390 feat: add a proper feed item uid when the bridge errors out (#3237)
* refactor: move function to class

* fix: use the computed bridge name as cache key

* refactor: extract method

* fix: set a feed item uid on errors

* docs

* fix: remove year from uid
2023-02-02 22:53:01 +01:00
Dag
c97e1923ae docs (#3238) 2023-02-02 01:30:29 +01:00
Dag
e01e031e3a fix: # and / in filter bridge (#3236) 2023-02-01 20:11:20 +01:00
Bocki
1fe55314be [Core] Update GH Actions (#3235)
* [Core] Update GH Actions

* fix dockerbuild

* add empty lines at the end of files
2023-01-31 22:11:59 +01:00
Binnette
af6aedd536 php 7.4 (or higher) (#3232) 2023-01-30 22:42:39 +01:00
Sébastien MB
51952c1db2 [DevToBridge] Allow subsribe to username (#3218) 2023-01-30 11:59:46 +01:00
Justin Goette
69290c8e55 feat: [FilterBridge] Add URI/URL filter option to FilterBridge (#3212) 2023-01-29 20:13:29 +01:00
sysadminstory
57e79e7f1b [RadioMelodieBridge] Fix article content (#3230)
- remove the social network share section ath the beginning and and the
  end of the article
- removte the HTML formated article tilthe from the content
- limite the author picture size to 60 px
2023-01-29 19:45:55 +01:00
somini
b21806a0b3 [TheRedHandFiles]: New Bridge (#3229)
This blog had a feed, is updated irregularly, and someone disabled the
feed on the Wordpress config on purpose for some reason. :(
2023-01-28 07:39:59 +01:00
Mynacol
e44e458617 [HeiseBridge] add all RSS feeds from heise.de (#3223)
Add all feeds from https://www.heise.de/news-extern/news.html with
(mostly) their original name.

The existing ones moved from e.g. https://www.heise.de/newsticker/heise-atom.xml
to https://www.heise.de/rss/heise-atom.xml.

Some feeds were commented out, as they do not provide full-text feeds
for multiple reasons.
2023-01-27 02:54:08 +01:00
Korytov Pavel
e76ddf9192 [TldrTechBridge] Add bridge (#3222)
* [TldrTechBridge] Add bridge

* [TldrTechBridge] Fix lint

* [TldrTechBridge] Fix lint
2023-01-23 19:22:13 +01:00
sysadminstory
e1e340f650 [PicnobBridge] New Bridge (#3221)
Add new bridge
- some image can not be displayed in the feed because of the CORP policy
2023-01-22 16:00:18 +01:00
Dag
c06e471ae9 feat: add new bridge for picarto.tv (#3220) 2023-01-20 22:48:06 +01:00
Dag
04090bd84a docs (#3217) 2023-01-17 21:21:32 +01:00
Tone
b9af53beff New Bridge for Caschys Blog (#3215) 2023-01-17 17:06:49 +01:00
Tone
077fc4bc3c Update HeiseBridge.php (#3214)
loading smaller images instead the original ones with a size of around 20Mb
2023-01-17 17:01:09 +01:00
sysadminstory
eb94107c15 [RadioMelodie] Fix bridge (#3204)
This fix follows the change in the CSS of the website.
2023-01-03 19:26:56 +01:00
Eugene Molotov
eab82f0a26 [VkBridge] Add video thumbnails (#3206) 2023-01-03 19:26:05 +01:00
Justin Goette
b58f2ed338 docs: Fix formatting (#3205) 2023-01-03 19:25:36 +01:00
csisoap
5d8ed2df51 [ReutersBridge] Only include main headline from Top News feed (#3199) 2022-12-31 07:05:25 +01:00
Justin Goette
910ccd3ad8 docs: Fix typo (#3196) 2022-12-28 00:34:49 +01:00
csisoap
2a69c585b8 [YoutubeBridge] fix blank page when duration limit used (#3192) 2022-12-18 08:23:18 +01:00
Dag
936ae8cca3 various fixes (#3190)
* fix: Call to a member function parent() on null

* fix: notice

fixes Trying to get property plaintext of non-object at bridges/WikiLeaksBridge.php line 96

* fix: CommonDreamsBridge
2022-12-13 21:04:57 +01:00
Dag
a13c4624fb feat: custom http ua in AO3, fix #3188 (#3189)
* refactor

* feat: custom http ua in AO3, #3188
2022-12-13 09:53:42 +01:00
Jan Justi
933be15a77 [docker-entrypoint.sh] Add custom formats (#3179) 2022-12-10 21:25:25 +01:00
Štěpán Škorpil
b4f9d27424 Added StreamCzBridge (#3187)
* Added StreamCzBridge

* Fixed codestyle on StreamCzBridge
2022-12-10 15:02:11 +01:00
Ololbu
7d5698a75f [TapasBridge] New bridge (#3184)
* [TapasBridge] New bridge

* [TapasBridge] Delete comment

* [TapasBridge] Delete context

* [TapasBridge] Delete context again

* [TapasBridge] Convert double quotes do single quotes

* [TapasBridge] Fix some lint errors

* [TapasBridge] Fix indentation style

* [TapasBridge] Fix some lint errors
2022-12-09 11:05:08 +01:00
Dag
ec1b9a110d fix: radiofrance, #3077 (#3186) 2022-12-08 21:36:28 +01:00
Dag
69518e95c1 feat: add tags to codeberg bridge, fix #3177 (#3185)
* refactor

* feat: add tags to codeberg bridge, fix #3177
2022-12-08 21:21:17 +01:00
Julien Papasian
59d3011c86 Add Nautiljon bridge (#3181) 2022-12-08 20:48:10 +01:00
Eugene Molotov
fdf380bccd [VkBridge] Remove junky 'Show more' button (#3176) 2022-12-03 04:58:25 +01:00
Steve Joerger
0423e1f268 Issue 3174 - Add docker-compose instructions to main README.md (#3175)
* Added instructions to use docker-compose copied from wiki with extra step of docker-compose command.

* Added header to differentiate docker-compose section. Added missing step to browse to url.
2022-12-03 04:56:37 +01:00
Dag
55b294665c fix: remove duplicate apt install in dockerfile (#3172) 2022-12-02 03:43:10 +01:00
Niehztog
7cf64daac9 Adds new Bridge for kitso.io episode feed (#3169)
* Adds new Bridge for kitso.io episode feed

* linter fixes, changed item titles

* more linter fixes

* fix bridge uri

* linter fixes

* added sorting of items by timestamp, added parameters to spicify show

* trying to fix linter again

* linter doesnt like tenary operators

* fix whitespace
2022-11-30 00:59:24 +01:00
Juan Jose Pablos
ca667d731c Update VproTegenlichtBridge.php (#3168)
Fix lint error
2022-11-29 15:19:16 +01:00
TinyWiFi
d38cd05bea Added vern.cc Public Instance (#3164)
done on behalf of ~vern admins (https://vern.cc/admins)
2022-11-27 17:10:57 +01:00
vincentvd1
9863204fa3 Feat: Add bridge for Vpro tegenlicht (#3162)
* [VproTegenlichtBridge.php] Created bridge

* [VproTegenlichtBridge.php] Added fetch exception
2022-11-25 20:39:56 +01:00
Dag
592ea8faa4 fix: TheHackerNewsBridge (#3159) 2022-11-22 19:09:49 +01:00
ORelio
f53e0e4bee [DarkReading] Convert pictures to plain images (#3158)
Convert <picture> to <img> element
2022-11-22 18:42:31 +01:00
ORelio
d592e2cb15 [Core] Add html/convertLazyLoading (+ document stripRecursiveHTMLSection) (#3157)
* [core] Add html/convertLazyLoading($dom)

Looks for lazy-loading attributes such as 'data-src' and converts
them back to regular ones such as 'src', easier for RSS readers.
It also converts <picture> elements to plain <img> elements.

* [core] Document html/stripRecursiveHTMLSection()

Add documentation for that function (no code changes).

* [WordPressBridge] Use convertLazyLoading()

* [WordPressBridge] Unwrap image figures

<img> inside <figure> may not display on RSS readers.
This converts them back to <img>, without losing caption if present.

* [ZDNet] Convert lazy loading images

* [code] html/stripRecursiveHTMLSection: Fix typo
2022-11-20 12:41:59 +01:00
Eugene Molotov
2f7f13d9fe [core] Implement SetBridgeCacheAction to store bridge's cache remotely (#3156) 2022-11-20 12:41:38 +01:00
Eugene Molotov
8990aeb9f4 [core] Merge GET and POST args into request array (#3155) 2022-11-20 12:41:20 +01:00
Dag
745a7ba122 fix: TheHackerNewsBridge (#3154) 2022-11-19 00:25:31 +01:00
Dag
88766e6fde fix: produce smaller log records for http exceptions (#3153) 2022-11-18 21:36:06 +01:00
Dag
4ac2feb392 docs: improve docs (#3152) 2022-11-17 18:51:37 +01:00
Dag
ebb82849e9 fix: notice in telegram (#3151)
* refactor: telegram

* fix: notice in telegram
2022-11-17 18:06:56 +01:00
Dag
3ee2c7f918 feat: new bridge RumbleBridge (#3150) 2022-11-17 18:06:35 +01:00
Dag
130212fba5 feat: new list of default bridges (#3148) 2022-11-17 18:06:19 +01:00
Dag
2e6facd863 docs: more screenshots (#3149) 2022-11-17 18:05:44 +01:00
Dawid Wróbel
b017f75767 [OLXBridge] new bridge (#2944)
* [OLXBridge] new bridge

* [OLXBridge] option to limit to shipping offers only

* [OLXBridge] set the feed title according to search query

* [OLXBridge] Fix PHP notices

* [OLXBridge] Remove trailing slash from the URL

* [OLXBridge] filter out the imposed additional search categories

* [OLXBridge] limit search to 'new' OLX platform variants

* [OLXBridge] Parse date, add ID, description

Deep-crawl all results. Penalty is low, as we were doing this for almost all of the results, anyway, yet it allows to obtain a unique ID, an uncomplicated Date string and a description.

Requires ext-intl for parsing the date according to locale.

* [OLXBridge] Parse date, add ID, description

Deep-crawl all results. Penalty is low, as we were doing this for almost all of the results, anyway, yet it allows to obtain a unique ID, an uncomplicated Date string and a description.

Requires ext-intl for parsing the date according to locale.

* [OLXBridge] Images are optional, handle appropriately

* [OLXBridge] handle the ID coming from sibling auto-moto portal

* [OLXBridge] handle the photos coming from sibling auto-moto portal

* [OLXBridge] use meta property to find img URL

* [OLXBridge] handle the date coming from sibling auto-moto portal

* [OLXBridge] use simplified syntax to retrieve content attribute value

* [OLXBridge] handle the description coming from sibling auto-moto portal

* [OLXBridge] fix phpcs complaints

* [OLXBridge] add categories

* [OLXBridge] handle the categories coming from sibling auto-moto portal

* [OLXBridge] hint image MIME type

OLX images have no obvious extension

* [OLXBridge] Fix content formatting

* [OLXBridge] URL is pattern-checked, so no need to check again

* [OLXBridge] return actual search query as URI
2022-11-17 17:57:05 +01:00
Dawid Wróbel
0726cce426 [AllegroBridge]: new bridge (#2942)
* [AllegroBridge]: new bridge

* [AllegroBridge] set feed name

* [AllegroBridge] fix notices

* [AllegroBridge] add images as attachments

* [AllegroBridge] cleanup code

* [AllegroBridge] add UID

* [AllegroBridge] add categories

* [AllegroBridge] fix pretty formatting

* [AllegroBridge] fix notice when offerExtraInfo is not found

* [AllegroBridge] add support for session cookie to prevent rate limiting

* [AllegroBridge] return inputted URL as URI

* [AllegroBridge] fix phpcs warning
2022-11-17 05:13:33 +01:00
Austin Huang
e788e14baa [MastodonBridge] Add support for various platforms (#3133)
* [MastodonBridge] Add support for various platforms

* [MastodonBridge] satisfy the lint
2022-11-16 18:05:01 +01:00
Dag
a5779d30b5 feat: add max file size to http responses (#3140) 2022-11-16 17:56:26 +01:00
Dawid Wróbel
c2c88e9876 Dockerfile: Use libcurl-impersonate in place of libcurl (#2941)
Fixes #2547
2022-11-15 17:32:44 +01:00
Dag
dbab225fd2 fix: Call to a member function find() on bool (#3146)
* fix: Call to a member function find() on bool

Happens when defaultLinkTo() is passed the empty string.

* fix: prevent exception in defaultLinkTo() when passed the empty string

* refactor
2022-11-15 03:01:27 +01:00
Dag
001427243c feat: system alert message (#3139) 2022-11-15 00:32:04 +01:00
Dag
95c199c2eb fix: various php notices (#3145)
* fix: notice

* fix: Trying to get property content of non-object at bridges/PcGamerBridge.php line 36

* fix: better exception message

* fix: strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior
2022-11-15 00:30:51 +01:00
Joseph
734a5868b8 [FirefoxAddonsBridge] Fix removal of link redirects (#3144)
* [FirefoxAddonsBridge] Fix removable of link redirects

* Update FirefoxAddonsBridge.php
2022-11-14 22:56:12 +01:00
Paroleen
80f9871c9e [AwwwardsBridge] Fix sites parsing (#3141)
* [AwwwardsBridge] Fix sites parsing

* [AwwwardsBridge] Fix phpcs issue
2022-11-11 18:33:28 +01:00
Dag
b64f8f2a09 fix: various fixes (#3136) 2022-11-08 21:17:32 +01:00
quickwick
00ff0890bb Add item dimensions (width x height) and source URL (if it exists in JSON) (#3135) 2022-11-08 18:43:51 +01:00
Dag
86c3a969b8 feat: add admin telegram contact config (#3134) 2022-11-07 18:36:52 +01:00
Dag
2ef98b299f refactor: extract frontpage to template (#3130)
Also introduce usage of Response object
2022-11-07 18:22:54 +01:00
MarKoeh
fe59cbabc9 [ARDAudiothekBridge] added bridge ARDAudiothek.de (#3132)
* [ARDAudiothekBridge] added bridge ARDAudiothek.de

ARD, the union of Germany's regional public-service broadcasters, operates a video and an audio streaming service. The video streaming service is ARDMediathek, for which a bridge already exists. The audio streaming service is ARDAudiothek. This commit adds initial support for ARDAudiothek. It currently supports turning shows to feeds.

* [ARDAudiothekBridge]  fixed code style

Sorry. Forgot spaces surrounding the concatenation symbol
2022-11-04 19:03:12 +01:00
Alexander
aa14d4aafb [SteamAppNewsBridge] new bridge (#3126)
* [SteamAppNewsBridge] new bridge

* [SteamAppNewsBridge] test fixes
2022-11-03 21:43:33 +01:00
Dag
75772f58e2 fix: review news was not properly extracted (#3131)
Fix #3129
2022-11-03 21:42:26 +01:00
John S Long
400e137673 [DiscogsBridge] Add optional image, if personal access token is configured (#3083) 2022-11-03 20:33:43 +01:00
ORelio
4520ab6835 [WordPressBridge] Improve content extraction (#3125)
* [WordPressBridge] Improve content extraction

 - Pick up currently unmaintained bridge
 - Allow Custom item limit and lower default limit from 20 to 10
 - Allow Custom content selector for blogs with non-standard templates (#2173)
 - Remove content selector made for one specific blog (#2173 - can be a custom selector now)
 - Add '.article-content' class in the set of default selectors
 - Improve lazy-loading conversion

* [WordPressBridge] Fix phpcs issues
2022-10-31 20:59:19 +01:00
Dag
7250940a05 fix: prtester css replacement (#3123) 2022-10-29 12:25:45 +02:00
Eugene Molotov
8779c09e89 [PikabuBridge] Remove html elements from feed item author (#3122) 2022-10-29 12:14:12 +02:00
Dag
23f8c81646 refactor/fix: css organization and error rendering (#3117)
* fix: php notice

* refactor/feat: merge HtmlFormat.css into style.css

Also improve ux of error rendering.

* fix: center-align footer text
2022-10-29 10:46:37 +02:00
Dag
1b45a53402 fix: make filecache be case-sensitive on key (#3113) 2022-10-29 10:27:26 +02:00
Dag
e027bd9274 fix: improve FeedExpander (#3103)
* fix: improve FeedExpander

Include the first libxml error in exception.

Give better error message if trying to parse the empty string.

Log all libxml errors if debug mode is enabled.

* error handling and logging tweak
2022-10-29 10:27:02 +02:00
Dag
f9cd397900 Update README.md (#3121) 2022-10-29 10:21:51 +02:00
Dag
1f576312ea feat: in debug mode, include part of http response in exception message (#3090) 2022-10-29 08:42:50 +02:00
Justin Goette
85b87b9597 docs: Document returnFull parameter of getContents (#3120) 2022-10-29 03:30:49 +02:00
joaomqc
bef6fc5cbd [SpotifyBridge] Add playlists feed (#3116)
* [SpotifyBridge] Add playlists feed

* fix formatting

* remove whitespace

* merge artist and playlist feeds

* fix lint errors
2022-10-27 20:02:01 +02:00
Dag
314d4c7a3f fix: quickfix for heise, #3118 (#3119) 2022-10-27 19:59:45 +02:00
Dag
52af2ae34c fix: php errors (notices) (#3115) 2022-10-26 00:47:45 +02:00
Juan Jose Pablos
8795cb252f A bit of info on cookies. I need to expand more. (#3111)
* add a bit of info about cookies

* Use markdown to look nice

* Update FacebookBridge.md

Fix url
2022-10-25 14:32:47 +02:00
Dag
cf7896aeef fix: forever loop in golem (#3114) 2022-10-25 14:30:01 +02:00
ORelio
05f2fb5ec7 [FeedExpander] Decode HTML entities in title (#3110)
Feed item title may contain HTML entities that we need to decode,
else they are encoded twice when generating the expanded feed.
2022-10-20 18:26:43 +02:00
Eugene Molotov
d483bf2b81 [core] Implement bearer token authentication (#3043) 2022-10-19 18:39:35 +02:00
Korytov Pavel
5b53e76477 [ScientificAmericanBridge] Add bridge (#3109)
* [ScientificAmericanBridge] Add bridge

* [ScientificAmericanBridge] Fix lint errors and timeout
2022-10-19 18:34:11 +02:00
Juan Jose Pablos
6ae40496c8 docs: add facebook bridge page 2022-10-18 19:35:53 +02:00
Dag
58e321c915 fix: flickr (#3104) 2022-10-17 00:38:57 +02:00
Dag
37f1ab726b fix: various bug fixes (#3102)
* fix: Undefined offset: 4

* fix: Trying to access array offset on value of type bool

* fix: Undefined variable: photo at bridges/TelegramBridge.php line 287

* fix: Trying to get property innertext of non-object at bridges/ZDNetBridge.php line 186

* fix: Undefined index: Category at bridges/UnraidCommunityApplicationsBridge.php line 42

* fix: Undefined index: fullUrl at bridges/EuronewsBridge.php line 61
2022-10-16 20:26:33 +02:00
Dag
ffbc107687 Improve logging and error handling (#3059)
* refactor: logging and errror handling
2022-10-16 17:55:43 +02:00
Dag
e21394d2d3 refactor: html format (#3062)
* refactor: html format

Fix a few small bugs too

* fix

* fix

* trigger build

* striptags instead of encode title
2022-10-16 12:03:57 +02:00
John S Long
78fa03238c [MastodonBridge] Add support for GoToSocial (#3098)
* [MastodonBridge] Add support for GoToSocial

GoToSocial expects URLs in HTTP Signatures to include the query string; Mastodon
does not. To provide support for both types of ActivityPub services, define a
new parameter for signature types, defaulting to Mastodon's format.

This change also introduces auto-resolution of linked objects, which GoToSocial
uses instead of including content directly in a user's outbox.

* [MastodonBridge] Fix lint failure
2022-10-12 21:43:09 +02:00
Harvey Christian Pacleb
4d8e40e746 [HonkaiImpactSeaBridge] Add bridge (#3084) 2022-10-12 21:29:38 +02:00
floviolleau
3ea7d46837 [PanneauPocketBridge] enhancements (#2940)
* [PanneauPocketBridge] small fixes

* [PanneauPocketBridge] rename variable

* [PanneauPocketBridge] remove function call

Co-authored-by: Florent VIOLLEAU <florent.violleau@samsic.fr>
2022-10-12 21:10:58 +02:00
An | Anton Röhm
7895fa895f fix "cht on matrix" button (again) (#3101) 2022-10-12 20:48:34 +02:00
John S Long
5e664d9b2b [Dockerfile] Enable opcache for improved performance (#3097)
Opcache is a PHP extension that caches the bytecode PHP converts each script
into to reduce the work that needs to happen each request.
2022-10-09 21:14:24 +02:00
Korytov Pavel
56a8c521c2 [EconomistBridge] Fix bridge (#3095) 2022-10-08 20:05:17 +02:00
Christian Schabesberger
79f6ec5733 hide Nordbayern+ articles (#3094) 2022-10-08 15:34:26 +02:00
Dag
0a2e31e1f7 docs: sort public instances (#3093) 2022-10-06 22:12:55 +02:00
Dag
e65fd7c822 fix: remove debug line (#3092) 2022-10-06 21:05:50 +02:00
Wouter Koch
c4c2acab98 Add NOS Nieuws & Sport Bridge (#3069)
* Add NOS Nieuws & Sport Bridge

* Change classname to reflect filename (NOSBridge)
2022-10-05 19:41:21 +02:00
joshinat0r
8b7b32d516 [RedditBrige] Fix old feed URLs (#3087)
* flair filter

* syntax

* fix multi & user feeds

* '

* dont replace ,

* fix old reddit feeds
2022-10-05 19:35:02 +02:00
Dag
5d18852108 fix: more verbose error in fb (#3089) 2022-10-05 19:30:42 +02:00
Dag
44e5bf5338 docs: point to offical feed in qnap (#3088) 2022-10-05 19:21:02 +02:00
somini
8b91921a70 [AsahiShimbunAJWBridge]: Fix title extraction (#3085) 2022-10-04 21:23:49 +02:00
Nicolas Delsaux
1232de5744 Add new bridge RadioFranceBridge, fix #3077 (#3082)
As far as I understand, this supports all radio france sub-pages listing podcast and/or shows (tested with my two favorites shows, so not a very professionnal test).
We use here the data model provided by Radio France, which includes all data in an easily usable format.
2022-10-03 06:55:24 +02:00
John S Long
6b83bf25fd [RoosterTeethBridge] Add episode description, optional episode image (#3080) 2022-10-02 19:38:33 +02:00
joshinat0r
30d964b356 [RedditBridge] Search for specific flairs (#3067)
* flair filter

* syntax

* fix multi & user feeds

* '

* dont replace ,
2022-10-02 07:34:20 +02:00
John S Long
8dcc21a871 [RoosterTeethBridge] Add new channels (#3076) 2022-10-02 06:48:07 +02:00
John S Long
7252a89914 [LWNprevBridge] Fix article content parsing (#3078) 2022-10-02 06:46:04 +02:00
An | Anton Röhm
3198a48589 Fix broken "chat on matrix" button (#3075) 2022-10-01 08:43:34 +02:00
Corentin Garcia
7c96334e3b [GithubSearchBridge] Add programming language (#3074) 2022-10-01 08:42:50 +02:00
Corentin Garcia
c4d95558af [GenshinImpactBridge] fix missing articles (#3073) 2022-10-01 08:42:02 +02:00
Corentin Garcia
8ac5045963 [TheGuardianBridge] Fix missing article content (fix #3032) (#3072) 2022-10-01 08:41:19 +02:00
Corentin Garcia
4a21855e5c [EliteDangerousGalnetBridge] Fix missing news (#3071) 2022-10-01 08:40:57 +02:00
Tobias Alexander Franke
1ffd9ee61a [GoogleScholarBridge] Follow authors of scientific publications. (#3066)
* [GoogleScholarBridge] Follow authors of scientific publications.

* [GoogleScholarBridge] Fix linting.

* [GoogleScholarBridge] Fix more linting issues.

* [GoogleScholarBridge] Use author field in extracted page and drop publisher
2022-09-25 22:22:49 +02:00
Tobias Alexander Franke
faf63269a1 [BinanceBridge] Adapt code to new JSON structure and fetch full article (#3065)
* [BinanceBridge] Add new bridge

* [BinanceBridge] TravisCI fixes

* [BinanceBridge] PR fixes

* [BinanceBridge] Fix for Binance blog: Pull JSON data instead of HTML

* [BinanceBridge] Fix double quotes

* [BinanceBridge] Remove announcements category (because of Cloudflare)

* [BinanceBridge] Simplify code to bare minimum

* [BinanceBridge] Adapt code to new JSON structure and fetch full article

* [BinanceBridge] Fix linting issues
2022-09-25 19:19:35 +02:00
Dag
8cc5e44be6 fix: use parlers new api (#3061) 2022-09-24 00:02:19 +02:00
Dag
aacba5b1a8 fix: too strict url validation in feed item (#3058)
Urls such as https://example.com/réponse were rejected

Fix https://github.com/RSS-Bridge/rss-bridge/issues/3018#issuecomment-1254159203
2022-09-21 23:07:56 +02:00
Mynacol
9d871e8a45 [ZeitBridge] Add bridge for zeit.de (#3056)
* [ZeitBridge] Add bridge for zeit.de

New bridge expanding the feeds of zeit.de to full-text ones.
Circumvents cookie banners and Z+ premium article paywalls.

* [ZeitBridge] Formatting
2022-09-21 22:24:11 +02:00
Dag
aabbeef743 docs: add a better bridge example in readme (#3057) 2022-09-21 22:20:51 +02:00
Mynacol
8d8fe66aab [HeiseBridge] Parser rewrite (#3054)
* [HeiseBridge] Parser rewrite

This rewrite is more readable and consistent than the previous one.

Additionally, this removes unwanted elements, largely recommendations
for other articles.
Furthermore, it increases the image quality by using the original
picture link instead of the compressed ones.

* [HeiseBridge] Formatting
2022-09-21 21:31:43 +02:00
Eugene Molotov
3d9fead463 [ActionFactory] Allow camel-case action names (#3044)
Dash symbol is used to convert dash-seperated string to camel-cased string
2022-09-20 18:17:57 +02:00
Eugene Molotov
2db523a37a [VkBridge] Handle empty posts before fixing image links (#3052) 2022-09-20 18:07:57 +02:00
Eugene Molotov
6cd8b90d28 [VkBridge] Follow changes on HTTP redirection (#3051)
When visiting canonical link like https://vk.com/club1,
VK returns redirection response to non-canical link,
which raises "Unexpected redirect location" exception.

This patch removes path check in order to handle this situation
2022-09-20 18:07:31 +02:00
Tobias Zulauf
f660c16ca6 [TwitterV2Bridge] noexternallink option to remove external stuff from the content_html output (#3041)
* [TwitterV2Bridge] noexternallink option to remove external stuff from the content_html output

* Update bridges/TwitterV2Bridge.php

* Update bridges/TwitterV2Bridge.php
2022-09-20 18:07:18 +02:00
Eugene Molotov
a12152e8a5 [VkBridge] Code cleanup (#3047)
- Remove .page_album_link patch, since VK already patched its renderer
- Remove non working code, that tries to get clean video links
2022-09-19 19:22:41 +02:00
Dag
55cc74c816 feat: new bridge pokemonnews (#3042)
* feat: new bridge pokemonnews

fix #3040

* fix
2022-09-17 01:29:40 +02:00
Eugene Molotov
11220ef373 [VkBridge] Photo fixes (#3039)
This commit fixes following issues:

- Photos from user profile wall started appearing as blured
- On posts with photo collection small thumbnails are shown
2022-09-15 22:17:10 +02:00
quickwick
a1e229a7e1 feat: check EZTV mirrors for available site to query (#3036) 2022-09-15 17:47:57 +02:00
Sébastien SAUVAGE
925caff5dd Merge pull request #3037 from RSS-Bridge/sebsauvage-remove-public-host-sebsauvage.net
Removing public instance on sebsauvage.net
2022-09-15 11:35:15 +02:00
Sébastien SAUVAGE
0bf41a5726 Removing public instance on sebsauvage.net 2022-09-15 11:33:16 +02:00
Dag
6f7be67a8c fix: broken bridge abcnews (#3033)
Fix #3031
2022-09-13 19:00:51 +02:00
Dag
2d272117cc fix: dont error out for invalid env names (#3030) 2022-09-12 23:14:11 +02:00
Dag
5a9336df12 fix: wrong accept header in mastodonbridge (#3025)
Fixes bug introduced by afcc38786e because of
the default Accept header which caused xml to be returned.

Fix #3024
2022-09-10 07:38:09 +02:00
Dag
94ae098ef5 fix: various fixes (#3023)
* improve twitch error message

* fix worldcosplay notice

* fix: add new video image to telegram

* fix: reuters

* fix: formula1

* twitter
2022-09-09 20:18:50 +02:00
Dag
6ac347d5ac fix: add workaround for NYTBridge antibot (#3022) 2022-09-08 19:54:09 +02:00
Dag
27b3d7c34e feat: improve logging and error handling (#2994)
* feat: improve logging and error handling

* trim absolute path from file name

* fix: suppress php errors from xml parsing

* fix: respect the error reporting level in the custom error handler

* feat: dont log error which is produced by bots

* ignore error about invalid bridge name

* upgrade bridge exception from warning to error

* remove remnants of using phps builin error handler

* move responsibility of printing php error from logger to error handler

* feat: include url in log record context

* fix: always include url in log record contect

Also ignore more non-interesting exceptions.

* more verbose httpexception

* fix

* fix
2022-09-08 19:07:57 +02:00
Dag
5578a735d9 feat: allow more feeds in FeedMerge (#3021)
Fix #3011
2022-09-08 18:44:15 +02:00
Dag
e63e3d072c feat: new bridge QnapBridge (#3020) 2022-09-08 18:28:36 +02:00
Eugene Molotov
70ba6c5b53 [VkBridge] Manually handle redirects (#3017)
Some redirects are legit, some redirects lead to "Too many requests" page
2022-09-07 03:02:23 +02:00
Eugene Molotov
bbbd599bc8 fix: do not throw exception on 301-303 http responses (#3014) 2022-09-06 15:40:20 +02:00
Dag
0dab51e26f fix: php errors (#3013)
* fix: php error in tiktok bridge

* fix: notice in craigslist

* fix: php notice in wordpress bridge

* feat: improve ux in telegram bridge
2022-09-06 00:14:20 +02:00
Eugene Molotov
8033a5f461 [VkBridge] Fix photo URI retrieving (#3010)
VK stopped filling JSON structure containing information for generating URI to full size photo
2022-09-05 23:41:41 +02:00
Dag
8ea9472300 feat: improve exception message when xml parsing fails (#3009) 2022-09-05 14:26:11 +02:00
Dag
a2c7865226 fix: exclude mastodon boosts when told to do so (#3007) 2022-09-05 05:58:18 +02:00
quickwick
36f64a3258 feat: add preview for external urls in twitterv2 (#3006)
Fix #2430
2022-09-05 05:31:36 +02:00
Dag
51f6ec8dc1 Update README.md (#3005) 2022-09-05 02:32:06 +02:00
quickwick
8ffc002e53 fix: truncated retweet text in twitter bridge(#3004)
Fix #2988
2022-09-05 02:04:09 +02:00
Dag
cca11174e1 fix: php error in ViceBridge (#3003)
Fixes Undefined variable: article at bridges/ViceBridge.php line 37
2022-09-04 22:58:58 +02:00
Dag
9d16e81e17 fix: php error PHP Notice: Undefined index: author (#3002) 2022-09-04 19:07:04 +02:00
Dag
5e09a14acc fix: include git tag in version (#3000)
Also include os and php version in github issue template.
2022-09-04 07:21:57 +02:00
Dag
693d6edfbf fix: php7.3 parser error in Configuration.php (#2999)
Dont fail a basic lint so that the proper error message can be shown when on unsupported versions.
2022-09-04 04:50:01 +02:00
Dag
57d5aa45f7 fix: php notice in eztvbridge (#2998)
* fix: php notice in eztvbridge

Fixes Undefined property: stdClass::$torrents

* lint
2022-09-04 04:35:21 +02:00
Dag
b8f73618c1 fix: include playlist when processing soundcloud items (#2997) 2022-09-04 03:50:40 +02:00
Dag
f40ed566be fix: absolute urls for images in pixivbridge (#2993) 2022-09-02 20:35:17 +02:00
Dag
3c2353c0ec fix: bug in previous refactor (#2992)
fix #2991
2022-08-31 18:16:19 +02:00
Dag
97808abca1 refactor: rename rssbridge.php to bootstrap.php (#2987)
Fix #2986
2022-08-27 23:01:06 +02:00
Korytov Pavel
1ca4dd69f7 [InstituteForTheStudyOfWarBridge] Do not put HTML tags into title (#2985) 2022-08-25 19:48:16 +02:00
Korytov Pavel
c079dbb521 [InstituteForTheStudyOfWarBridge] Add bridge (#2984) 2022-08-25 19:22:19 +02:00
Eugene Molotov
43ad54dba0 [PikabuBridge] Skip sponsored posts (#2983)
* [PikabuBridge] Skip sponsored posts

Sponsored posts appear very rarely in html code.
But when they appear, they always have different url that results junk feed.

One of the example is [1]. After visiting it, you will be redirected to [2] that is marked as
"Партнёрский материал" in Russian, or "Sponsored post" in English.

[1] https://pikabu.ru/story/a_mla_posa_m_memu_seyla_otorathche_idomikhlenonoikhmyav_sseyla_otoratazoed__9388770?from=cpm
[2] https://pikabu.ru/story/kakim_dolzhen_byit_vash_noutbuk_9388770

* lint

Co-authored-by: Dag <me@dvikan.no>
2022-08-25 18:09:06 +02:00
Eugene Molotov
fd0d5350be [RutubeBridge] Include timestamp and author in feed (#2982) 2022-08-24 14:49:54 +02:00
Dag
5165ea265d fix: case-insensitive config from env, fix #2935 (#2973)
* refactor

* fix: case-sensitive config from env, fix #2935

* lowercase all config section and keys

* test: add test for case-insensitivity
2022-08-23 21:19:53 +02:00
Bocki
edbafc6144 [Teefury] Fix occasional problem (#2974) 2022-08-20 21:11:27 +02:00
Dag
0de1694371 refactor: extract index.php to RssBridge.php (#2961)
* refactor: extract entry point into class

* refactor: js

* refactor: extract frontpage action
2022-08-18 22:52:01 +02:00
Christian Schabesberger
372eccd7b2 [Nordbayern] Update regions (#2966) 2022-08-13 19:41:05 +02:00
Bocki
e99cbf21b2 [DailyShirts] Add daily shirt sites (#2962) 2022-08-12 14:51:38 +02:00
Miika Launiainen
08c1f55f4a Created Hanime bridge (#2958)
* Created Hanime bridge

* Moved cover image from enclosures to content as dvikan suggested
2022-08-10 23:46:17 +02:00
Dag
6253538342 docs: revamp README (#2954) 2022-08-06 23:22:50 +02:00
Dag
502799a74c feat: use bridge description and short name in search (#2952)
* refactor: search.js

* feat: use bridge description and short name in search

* fix bug in previous merge commit

Also reformat string from tabs to spaces
2022-08-06 23:12:30 +02:00
Dag
eef45d4e8d fix: TypeError (0): setlocale(): Argument #1 ($category) must be of type int, string given (#2951)
This was upgraded from a warning to an error in php 8.
2022-08-06 22:46:49 +02:00
Dag
2bbce8ebef refactor: general code base refactor (#2950)
* refactor

* fix: bug in previous refactor

* chore: exclude phpcompat sniff due to bug in phpcompat

* fix: do not leak absolute paths

* refactor/fix: batch extensions checking, fix DOS issue
2022-08-06 22:46:28 +02:00
Dag
b042412416 fix: force HTTP 1.1 in curl requests (#2949)
Since curl 7.62.0 the default option is: CURL_HTTP_VERSION_2TLS

Before that the default used to be: CURL_HTTP_VERSION_1_1

Fix #2947
2022-08-05 11:46:32 +02:00
Eugene Molotov
205f0a7239 [RutubeBridge] Fix regex for retreiving reduxState (#2955)
Before this commit regex captured window.reduxState value until first semicolon.
This is incorrect since it produces invalid json, if semicolon is
also somethere in the middle of stringified json.

After this commit regex will capture window.reduxState value until last semicolon.
2022-08-05 11:45:50 +02:00
Dag
3984427f44 feat: include os and php version in github issue body (#2948) 2022-08-03 17:05:13 +02:00
Dag
ecb486794b refactor: use static values for cache scope
This fixes a future problem when code is placed under a namespace because `get_class($bridge)` will then return e.g. `RssBridge\Bridge\TwitterBridge` instead of the the current value `TwitterBridge`.

Also a bit refactoring of `Configuration.php`.
2022-08-02 15:03:54 +02:00
Loïc Fürhoff
a0a0d5235b Remove MAINTAINER (#2946) 2022-07-31 21:42:40 +02:00
Dag
afcc38786e fix: use default headers in getContents() (#2927) 2022-07-31 04:21:56 +02:00
Dag
cd0ca7f645 fix: change default curl user agent (#2926) 2022-07-31 03:58:07 +02:00
Dag
0a060b2ad6 [Gab] feat: add new bridge GabBridge (#2920) 2022-07-31 03:52:27 +02:00
Jan Tojnar
5b5f3b4254 Do not use constants for configuration (#2938)
* docs: Do not use constant names when referring to config options

The options are customizable using a config file and no longer hardcoded in index.php since 8ac8e08abf

* Do not use constants for configuration

Since <8ac8e08abf>, they are just set to the configuration object values.
2022-07-24 19:26:12 +02:00
Dawid Wróbel
499d5c2b77 [Amazon & AmazonPriceTracker] Add Poland (#2930) 2022-07-21 20:41:15 +02:00
llamasblade
2c63d5707d [HytaleBridge] Improve bridge performance (#2928) 2022-07-21 20:33:00 +02:00
llamasblade
9b0f8095c2 [YandexZenBridge] Fix feed title if username not specified (#2922) 2022-07-13 12:08:11 +02:00
llamasblade
1294d3b953 [YandexZenBridge] Add bridge (#2921) 2022-07-13 01:08:05 +02:00
llamasblade
64c8d4ad37 [HytaleBridge] Improve bridge contents (#2912)
Co-authored-by: BuildTools <unconfigured@null.spigotmc.org>
2022-07-10 21:06:41 +02:00
Dag
5e52ecc3f8 test: add new test for Configuration (#2915) 2022-07-10 20:05:27 +02:00
Dag
c33f84fcc2 fix: disallow non-strings in GET parameters (#2908) 2022-07-10 19:50:51 +02:00
Dag
003ab58514 [FurAffinity] fix: errror (#2887)
They changed parts of the dom.

Fix error:
Error: Call to a member function find() on null

Fixes #2868
2022-07-10 19:48:37 +02:00
Loïc Fürhoff
87f8571ccf [Mailman2] Add bridge (#2877) 2022-07-10 19:40:03 +02:00
floviolleau
f1319f5b2b [PanneauPocket] add new bridge (#2823) 2022-07-10 19:36:01 +02:00
Dag
40dc0a2e5f [Euronews] fix: use correct url (#2916)
The non-www domain has a tls config error.
2022-07-09 22:50:03 +02:00
Bocki
e89329b2c6 [core] Fix daux generation (#2914) 2022-07-09 20:33:07 +02:00
Jan Tojnar
e07a94d480 Normalize some method calls (#2911)
The methods were called as static even though they were not.
2022-07-09 08:13:07 +02:00
Dag
a966213cd7 refactor: inject the action params via its execute method (#2907) 2022-07-08 21:06:14 +02:00
Jan Tojnar
22c10941dc docs: Update directory structure description (#2906)
- The css directory was moved in 1a4c3f4418
2022-07-08 21:00:32 +02:00
Dag
abc4af43b3 feat: improve error handling (#2902) 2022-07-08 20:39:13 +02:00
Dag
c992bcc8bf [AssociatedPressNews] fix: prepend lead photo to items (#2905) 2022-07-08 18:42:45 +02:00
Jan Tojnar
f672902896 Add .git-blame-ignore-revs file (#2903)
This will ignore coding style change commits in GitHub’s blame UI.

Same thing can be achieved locally using either `git blame --ignore-revs-file .git-blame-ignore-revs`
or `git config blame.ignoreRevsFile .git-blame-ignore-revs`

https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt
https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
2022-07-08 17:00:34 +02:00
Dag
abfc6b4633 feat: introduce template engine (#2899) 2022-07-08 14:17:25 +02:00
Jan Tojnar
951092eef3 Fix coding style missed by phpbcf (#2901)
$ composer require --dev friendsofphp/php-cs-fixer

$ echo >.php-cs-fixer.dist.php "<?php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__);

$rules = [
    '@PSR12' => true,
    // '@PSR12:risky' => true,
    '@PHP74Migration' => true,
    // '@PHP74Migration:risky' => true,
    // buggy, duplicates existing comment sometimes
    'no_break_comment' => false,
    'array_syntax' => true,
    'lowercase_static_reference' => true,
    'visibility_required' => false,
    // Too much noise
    'binary_operator_spaces' => false,
    'heredoc_indentation' => false,
    'trailing_comma_in_multiline' => false,
];

$config = new PhpCsFixer\Config();

return $config
    ->setRules($rules)
    // ->setRiskyAllowed(true)
    ->setFinder($finder);

"

$ vendor/bin/php-cs-fixer --version
PHP CS Fixer 3.8.0 BerSzcz against war! by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 8.1.7

$ vendor/bin/php-cs-fixer fix
$ rm .php-cs-fixer.cache
$ vendor/bin/php-cs-fixer fix
2022-07-08 13:00:52 +02:00
Jan Tojnar
dbf8c5b7ae refactor(BridgeFactory): make methods only accept valid class names (#2897)
This moves the responsibility for getting a valid class name
to the users of BridgeFactory, avoiding the repeated sanitation.
Improper use can also be checked statically.
2022-07-08 12:54:23 +02:00
sal0max
20bf2aa4fe [ExplosmBridge] merge ExplosmBridge and CyanideAndHappinessBridge (#2844) 2022-07-08 00:23:29 +02:00
llamasblade
f887ce8f63 [HytaleBridge] Add bridge (#2900) 2022-07-07 15:12:35 +02:00
Tokariew
ea45717a28 [Instagram] fix: add ds_user_id (#2881)
Fix #2876
2022-07-07 12:08:21 +02:00
Jan Tojnar
d107f8ed30 Improve Factory variable names (#2895) 2022-07-06 12:14:04 +02:00
Bocki
e3dad86bca [core] prtester fix for optgroups (#2896) 2022-07-06 11:26:53 +02:00
Joseph
6c52e9bbc6 [TelegramBridge] Support telegram.me in detect params regex (#2891) 2022-07-06 03:48:49 +02:00
Jan Tojnar
e254dfbb9c ci: Fix PHPCompatibility again (#2892)
The fix in 66568e3a39 prevented an error
when installing phpcompatibility/php-compatibility but there was still
a warning before that when installing dealerdirect/phpcodesniffer-composer-installer.
With Composer 2.3.9, this is now an error too, so we need to move
the config change before that: https://getcomposer.org/changelog/2.3.9
2022-07-06 03:39:58 +02:00
Jan Tojnar
b444fa71f5 docs: Update requirements
This was forgotten in 8365a7a34d.
2022-07-06 03:34:37 +02:00
Jan Tojnar
ab6aca3163 lib/Configuration: Remove redundant comment
It was just getting out of sync:

- Minimum PHP version was bumped in 8365a7a34d
- Cache directory permission check was removed in 8e2b65556f
- Whitelist permission check was removed in d4e867f240
2022-07-06 03:34:37 +02:00
Jan Tojnar
7ee942621d composer: Update lockfile
composer.json was modified in 8365a7a34d
but the changes were not propagated to the lockfile,
resulting in warnings on every installation.
2022-07-06 03:34:37 +02:00
Dag
192fc0ee9b [FeedMerge] feat: remove duplicates (#2888)
Fix #2855
2022-07-05 15:39:00 +02:00
Dag
321ec7c8c1 refactor: move cache logic into the factory (#2884) 2022-07-05 13:20:01 +02:00
Dag
5b9b579652 refactor: remove unused class (#2883) 2022-07-05 10:50:39 +02:00
Dag
e918bda735 chore: introduce CONTRIBUTORS.md (#2839) 2022-07-04 07:33:23 +02:00
sal0max
7d941c2898 [Flaschenpost] Add bridge (#2808) 2022-07-04 07:29:22 +02:00
Dag
4f75591060 Reformat codebase v4 (#2872)
Reformat code base to PSR12

Co-authored-by: rssbridge <noreply@github.com>
2022-07-01 15:10:30 +02:00
Jan Tojnar
66568e3a39 ci: Fix PHPCompatibility (#2873)
> For additional security you should declare the allow-plugins config with a list of packages names that are allowed to run code. See https://getcomposer.org/allow-plugins
> You have until July 2022 to add the setting. Composer will then switch the default behavior to disallow all plugins.

Oops, it is July now.
2022-07-01 15:02:04 +02:00
Dag
9f2f1e526d [Instructables] refactor: conform to PSR2 (#2870) 2022-07-01 02:19:47 +02:00
Patrick Collins
2c7a9d7c45 [MangaDex] improve date handling (#2864)
prioritize new chapters rather than just edited ones.
should avoid batch-renaming drowning out new chapters, and existing items being re-sorted in the feed when they're edited.
documentation here: https://api.mangadex.org/docs/dates/
2022-06-26 08:50:51 +02:00
Dag
5076d09de6 refactor: prepare for PSR2 (#2859) 2022-06-24 18:29:35 +02:00
Dag
d2313bddcc feat: print which bridge is being processed (#2860) 2022-06-24 14:40:17 +02:00
Jan Tojnar
b1ae7603bf ci: Install PHPUnit as a Composer dependency (#2857)
Now that we dropped support for deprecated PHP versions,
we can use the same PHPUnit version on all supported PHP version.
Let’s install it as a Composer dependency to have the same
PHPUnit version on the CI as on developers’ computers.
2022-06-24 12:00:58 +02:00
KamaleiZestri
556f0b4237 [NewgroundsBridge] Add Bridge (#2849) 2022-06-24 11:59:24 +02:00
Yaman Qalieh
1e7ad6ed51 [PixivBridge] Remove backwards compatability hack (#2846)
There is no need for this since the context is guessed if it's not
provided.
2022-06-24 11:31:24 +02:00
Dag
92782e6c34 [GoogleSearch] feat: add verbatim option (#2858)
The verbatim option is the same as wrapping the query in quotes e.g. "rss-bridge".
2022-06-24 11:18:27 +02:00
Yaman Qalieh
1cfcacbbeb [BugzillaBridge] Add new bridge (#2825) 2022-06-24 11:04:49 +02:00
Yaman Qalieh
7d31d32750 [core] Fix bugs in release helper and document (#2819) 2022-06-24 11:02:33 +02:00
Yaman Qalieh
abb4c17a0d bridges: Update maintainer (#2856) 2022-06-22 13:07:54 -04:00
Dag
a166899633 [FeedMerge] fix bugs (#2854)
* [FeedMerge] fix: sort items by timestamp descending

* [FeedMerge] fix: fetch 10 most recent items

This fixes a bug where the bridge e.g. fetched 10 items from the first feed and then nothing from the rest
2022-06-22 18:34:05 +02:00
Dag
7dc3449207 [KilledByGoogle] fix: broken enclosure url (#2852)
* [KilledByGoogle] fix: broken enclosure url

The previous enclosure url was HTTP 404.

Also add link to items.

* fix: close unclosed <a> tag
2022-06-22 18:33:48 +02:00
Dag
bde00447f1 [Telegram] fix: remove all enclosures except for videos (#2850) 2022-06-22 18:33:21 +02:00
Dag
9d5f59e2db [GoogleSearch] fix: improve bridge (#2848)
Sort properly by date.
Fix php errors.
Improve the date parsing logic.
Improve the content parsing by also including those with · as separator
2022-06-22 18:32:54 +02:00
Dag
e7aebb223d fix: catch everything (#2837)
Improve ux.
2022-06-22 18:32:22 +02:00
Dag
ee80f4918e refactor: action (#2836) 2022-06-22 18:30:37 +02:00
Dag
fad0dbb6ef refactor: fix exception handling (#2835)
* refactor: fix exception handling

The removed catch is never uses in php versions above 7.
The need for multiple catch statements like this is to support both php 5 and 7.

* remove traces of old exception handling

* add typehints

* dont treat exception code 0 specially
2022-06-22 18:30:06 +02:00
Dag
07927008eb refactor: CacheFactory (#2834) 2022-06-22 18:29:28 +02:00
Dag
b7b9378484 refactor: ActionFactory (#2833) 2022-06-22 18:28:07 +02:00
Dag
af5648d928 refactor: FormatFactory (#2832) 2022-06-22 18:27:20 +02:00
Austin Huang
e9b8a1f9f9 [Mastodon] Use ActivityPub outbox for Mastodon (et al.) feed (#2756)
* Use ActivityPub outbox for Mastodon (et al.) feed

closes #2754

* Better description for Mastodon bridge

I mean I could rename it to ActivityPub bridge if the maintainer so pleases

* [Mastodon] Please the lint

* [Mastodon] address feedback

* [Mastodon] fix link, address spelling case bug

* refactor

* [Mastodon] add username cache, fix try-catch, rename

* [Mastodon] shorten description to satisfy the lint

* [Mastodon] address feedback

* [Mastodon] support Secure Mode instances

* [Mastodon] add config documentation

* [Mastodon] update docs

Co-authored-by: Dag <me@dvikan.no>
2022-06-20 19:11:46 -04:00
Dag
8365a7a34d chore: bump required php version to 7.4 (#2838)
* chore: require min php 7.4

* Revert "feat: backport php 7.3 functions (#2803)"

This reverts commit 6df5a4bc14.

* [BandcampDaily] use array_key_first

* hard fail on php versions below 7.4

* update phpcompat linter
2022-06-19 21:45:33 +02:00
Joseph
192dc4dae2 [TikTokBridge] Add bridge (#2828) 2022-06-18 21:59:39 -04:00
Korytov Pavel
c78c1254a8 [NovayaGazetaEuropeBridge] Add bridge (#2827) 2022-06-18 21:57:40 -04:00
Yaman Qalieh
65e6d9f454 [XPathAbstract] Improve Media Url regex (#2845) 2022-06-19 02:19:32 +02:00
Gilles
ce63d8a706 [YGGTorrentBridge] Changed base URL and one parameter name (#2826) 2022-06-17 23:06:59 -04:00
Eugene Molotov
d0bea1627e [InstagramBridge] Fix incorrect cache timeout calculation (#2840)
It is expected that getCacheTimeout returns integer. In fact
it returned boolean value which lead to situation, where Instagram feeds
were not cached.
2022-06-18 03:02:31 +02:00
Dag
c6ba3e5280 [Parler] fix: use new api endpoint (#2831)
They also modified the json structure.
2022-06-17 21:18:29 +02:00
Jan Tojnar
10eb1c9a95 [FormatAbstract]: Ensure sanitizeHtml is given string (#2791)
Sometimes `Item::getContent` returns `null`, in which case `sanitizeHtml`
would pass it to `str_replace`, which would raise `E_DEPRECATED` on PHP 8.1.
2022-06-17 20:46:15 +02:00
Yaman Qalieh
9ac494b350 [AstrophysicsDataSystemBridge] Add bridge (#2796) 2022-06-17 20:00:31 +02:00
Joseph
c6100d95ca [UberNewsroomBridge] Add more regions & use region name from JSON (#2817) 2022-06-14 22:18:16 -04:00
Yaman Qalieh
33e3d9b596 [Kanali6Bridge] Add bridge (#2798) 2022-06-14 20:08:55 -04:00
Yaman Qalieh
9503f9ad7f [Release] 2022-06-14 (#2818) 2022-06-14 09:47:12 -04:00
Yaman Qalieh
3e2423d86b [MsnMondeBridge] Fix bridge (#2813) 2022-06-14 09:45:46 -04:00
Yaman Qalieh
90e0504da5 [Shimmie2Bridge] Fix bridge (#2814) 2022-06-14 09:45:35 -04:00
Yaman Qalieh
4b3b1ca163 [KununuBridge] Fix bridge for default parameters (#2816) 2022-06-14 09:45:01 -04:00
Yaman Qalieh
5b93bba1a3 [ZenodoBridge] Fix bridge (#2815) 2022-06-14 09:44:26 -04:00
Dag
b8786da137 Update 06_Public_Hosts.md (#2812)
Modify url to use https.
2022-06-10 17:19:45 +02:00
Kristian Salonen
166ead902d [Configuration.php] Update the version name to dev.2022-06-10 (#2811) 2022-06-10 15:05:34 +02:00
sal0max
de279de762 [CyanideAndHappiness] Add bridge (#2807) 2022-06-10 07:29:01 +02:00
Jan Tojnar
347f9a3eda [contents] Add MIME type for mp3 (#2809)
Without this, format tests fail on systems without `/etc/mime.types`.
2022-06-09 22:41:10 -04:00
Jan Tojnar
1af6cbeb1e [XML formats] Ensure elements are connected to DOM before further manipulation (#2806)
We are setting xmlns attributes at the root element but PHP would
still attach redundant ones to the DOM elements created with `createElementNS`.
That was because PHP reconciles namespace attributes when appending elements to DOM
but since we previously only attached the elements after all children were attached,
the reconciliation algorithm was not able to see the root element’s attributes.

To fix this, let’s attach each element to its parent immediately after it is created.
2022-06-09 18:33:23 +02:00
Yaman Qalieh
37f211a37e Add Laravel framework license 2022-06-09 12:13:07 -04:00
Yaman Qalieh
bea0595e5c Add php-urljoin license 2022-06-09 12:13:07 -04:00
Dag
6df5a4bc14 feat: backport php 7.3 functions (#2803)
* feat: backport php 7.3 functions

* fix: add license

* fix: formatting

* fix: add note in README regarding license
2022-06-09 18:00:51 +02:00
Yaman Qalieh
3927ecd822 [UsenixBridge] Add bridge (#2800) 2022-06-09 16:56:52 +02:00
pubak42
1b0a6f2813 [VixenBridge] New bridge (#2763) 2022-06-09 16:53:26 +02:00
Yaman Qalieh
8f0d90f653 [PixivBridge] Fix tags (#2799) 2022-06-08 23:05:56 -04:00
Yaman Qalieh
037d5866ca [BandcampDailyBridge] Fix list duplicates (#2795) 2022-06-08 19:37:06 -04:00
Yaman Qalieh
75c4c9f256 Revert "[Usenix] Add new bridge for USENIX (usenix.org) publications (#2772)" (#2793)
This reverts commit baa4ea8338.
2022-06-08 00:44:37 -04:00
Dag
baa4ea8338 [Usenix] Add new bridge for USENIX (usenix.org) publications (#2772)
Currently only supporting the ;login: publication.
2022-06-08 05:43:56 +02:00
Jan Tojnar
12ddee4054 tests/Formats: Simplify by using a base class (#2779)
There is a lot of redundancy. Let’s not repeat ourselves.

Unfortunately, since we do not install PHPUnit as a project dependency on CI,
it does not use the composer’s PSR-4 autoloader and the tests are unable to find
the `BaseFormatTest` class.

Until we resolve that, let’s load the class explicitly.
2022-06-08 02:17:32 +02:00
Dag
6582a66a2d Revert "Update tests.yml (#2788)" (#2792)
This reverts commit aa32040bd4.
2022-06-08 02:16:06 +02:00
sysadminstory
a4785370fa [DealabsBridge-HotUKDealsBridge-MydealsBridge-PepperBridgeAbstract] Fix (#2789)
the date handling

The deal posting date logic was wrong, and leaded to warnings and
notice. Now, only the feed with the deal sorted by date contains date
(the feed sorted by hottest deal does not contain a date anymore,
because there are no deal date in this case).
2022-06-07 23:55:15 +02:00
Dag
aa32040bd4 Update tests.yml (#2788) 2022-06-07 23:33:16 +02:00
Jan Tojnar
44e8007d9c tests: Use PSR-4-style namespaces (#2778)
We cannot yet switch to namespaces for RSS-Bridge itself but for tests we are not limited by BC.
It does not actually do anything since PHPUnit will search for the test files without the help of the autoloader but it still makes the directory cleaner.
2022-06-07 23:22:33 +02:00
Jan Tojnar
90d22f0d80 [{Atom,Mrss}Format]: Generate using DomDocument (#2771)
* [AtomFormat]: Generate using DomDocument

This will escape the HTML content for us as needed.

* [MrssFormat]: Generate using DomDocument

This will escape the HTML content for us as needed.
2022-06-07 23:22:03 +02:00
Jan Tojnar
fb501652d5 Formats: Remove display & related method (#2776)
Format should not be responsible for sending HTTP response.
2022-06-07 18:05:33 +02:00
Joseph
e85932b1a5 [BrutBridge] Fix bridge (#2787) 2022-06-07 18:05:03 +02:00
Korytov Pavel
53f9970403 [EuronewsBridge] Add bridge (#2786) 2022-06-07 10:25:20 -04:00
Jan Tojnar
19ad2584da [NFLRUSBridge] Remove byte-order-mark (#2777)
With UTF-8 byte-order mark in the file, the `ListActionTest::testOutput`
would fail after converting tests to PSR-4 namespaces:

    invalid JSON output: Syntax error
    Failed asserting that null is not null.

This is because ListAction::execute tries to create the bridge objects
and, when the files containing the bridge classes are not loaded yet,
the autoloader starts including them. Since this happens after output
buffering has begun, any text in the PHP file before the `<?php` tag
such as the BOM will end up in the buffer to be parsed by `json_decode`.

Previously, it worked by chance thanks to some other test including the file
before `ListActionTest`. With the restructuring, `Actions\ListActionTest`
will run sooner and become responsible for triggering the autoloader.

To prevent this in the future, I also disallowed BOM in the coding style.
2022-06-07 04:59:22 +02:00
somini
190c95fa62 [PCGWNewsBridge]: New Bridge (#2785) 2022-06-06 01:02:15 +02:00
Dag
678e5d9866 [NeuviemeArt] Exterminate dead bridge (#2784)
They moved to https://www.bubblebd.com/9emeart

Fixes #2774
2022-06-05 22:56:54 +02:00
Yaman Qalieh
4787eb3799 [WordPressMadaraBridge] Add Bridge (#2782) 2022-06-05 14:40:43 -04:00
Yaman Qalieh
a863234474 [MangaDexBridge] add chapter search context (#2783) 2022-06-05 14:28:05 -04:00
Korytov Pavel
4260be26a2 [EconomistWorldInBriefBridge] indent with tabs instead of spaces (#2781) 2022-06-05 18:40:48 +02:00
Mynacol
713d06ba08 [GitlabIssueBridge] Code cleanup (#2780)
- Rename parseMRDescription() -> parseMergeRequestDescription()
- Move parseMergeRequestDescription() below parseIssueDescription()
- Inline getProjectURI()
2022-06-05 18:39:54 +02:00
Korytov Pavel
7256d1138b [EconomistWorldInBriefBridge] Add bridge (#2765) 2022-06-05 17:16:11 +02:00
Dag
71310d2c5a [OsmAndBlog] Remove bridge (#2775)
They revamped their page. The feed has been returning a single item
for some time.

Their blog can be followed at:
https://osmand.net/blog/atom.xml
2022-06-05 03:51:37 +02:00
Dag
92d813fbea [NotAlways] fix: broken url (#2773)
The /all url now actually points to a specific item.
I think we want the frontpage for this.

Fixes:
Fatal error: Uncaught Error: Call to a member function find() on null in NotAlwaysBridge.php:37
2022-06-05 02:56:51 +02:00
Mynacol
3f896f9465 [GitlabIssueBridge] Add bridge (#2760)
* [GitlabIssueBridge] new bridge

This tracks issue comments on arbitrary gitlab projects.

* [GitlabIssueBridge] Prepare for Merge Request support + fixes

- Proper UIDs
- Default bridge name fixed
- Fix cache identifiers
- Add TODOs

* [GitlabIssueBridge] creation timestamp preferred

And prefer original author over editor.

* [GitlabIssueBridge] Do not add date to item title

Prettier without it.

* [GitlabIssueBridge] Support Merge Requests

This bridge can now generate feeds for Merge Requests.

* [GitlabIssueBridge] typo

* [GitlabIssueBridge] Fix Img src attr in comments

* [GitlabIssueBridge] Fix function call

* [GitlabIssueBridge] Fix test

Use gitlab.com if no h parameter was given.
Fixes a phpunit test.

* [GitlabIssueBridge] linting

* [GitlabIssueBridge] Add MR support to description

* [GitlabIssueBridge] Move function collectData

* [GitlabIssueBridge] rm single-use class constants

* [GitlabIssueBridge] Remove manual caching

Just depend on rss-bridges built-in caching.
2022-06-04 23:59:10 +02:00
Shikiryu
b7e1dc1ab1 [KhinsiderBridge] fix RSS because of the new layout (#2767)
* [KhinsiderBridge] fix RSS because of the new layout

* [KhinsiderBridge] fix phpcs
2022-06-04 22:41:37 +02:00
sysadminstory
8e41887393 [DealabsBridge-HotUKDealsBridge-MydealsBridge] Fix example values (#2766)
Added real example values for discussion to allow automatic testing.

Updated keywords example value to be sure there will be some results.
2022-06-04 22:40:20 +02:00
Mynacol
8865521b3b [GolemBridge] Remove image galleries (#2761)
Do not add all images of the image gallery, but only the preselected one.

Often, the same gallery is used multiple times with different preselected
images. The previous implementation always added all images of the
gallery, cluttering the article. This patch only adds the preselected one.

The no-js link wrapping around the gallery leads to a 403 Forbidden
page, so linking that doesn't work to really support galleries.
2022-06-04 22:27:24 +02:00
Austin Huang
8172d10bb5 [Amazon & AmazonPriceTracker] Add Turkey, close #2665 (#2758)
* [AmazonPriceTracker] Add Turkey, close #2665

* [Amazon] Add Sweden & Turkey

consistent with price tracker
2022-06-04 21:59:52 +02:00
Park0
299ad87168 [Marktplaats] #2553 example values added (#2752)
For automation tests example values are needed
2022-06-04 21:06:38 +02:00
Dag
d60d8313d0 fix: type error in function call (#2769)
Fixes:
Argument 2 passed to getContents() must be of the type array, int given
2022-06-04 21:05:43 +02:00
Yaman Qalieh
1fd2f37bb4 [PixivBridge] Fix 404 for fullsize novel images (#2751) 2022-06-04 20:53:10 +02:00
Dag
04b1609ce0 docs: refactor table of public instances (#2749) 2022-06-04 20:52:10 +02:00
Christian Schabesberger
2fa24e780b Fix nordbayern (#2730) 2022-06-04 20:50:16 +02:00
quickwick
3b04e318ae [SlusheBridge] New bridge (#2700) 2022-06-04 20:10:07 +02:00
Alexandre Alapetite
05cd1c0b67 [Core] Add expose to dockerfile (#2762) 2022-05-30 20:05:42 +02:00
Dag
cb05cacd6a fix: add 429 to status codes (#2757) 2022-05-27 15:25:12 +02:00
Bocki
4d18312604 [Core] Prtester fix list fix (#2753) 2022-05-25 20:26:39 +02:00
Joseph
85e5ce2679 [UberNewsroomBridge] Add bridge (#2748) 2022-05-25 09:43:18 +02:00
Bocki
7afc577e97 [core] Fix nested selection lists (#2750) 2022-05-25 09:38:53 +02:00
KamaleiZestri
462319344b [CubariBridge] New Bridge (#2747) 2022-05-24 13:34:40 +02:00
Dag
5cc34b884a [core] Improve getContents docs (#2742) 2022-05-22 21:27:23 -04:00
KamaleiZestri
dd025894e9 [PillowfortBridge] Modify example value for Pillowfort Bridge (#2746) 2022-05-22 15:30:45 -04:00
Dag
1d0a0b927b fix: use accept header when fetching feed (#2737)
* fix: use accept header when fetching feed

* fix: include atom too, and reuse constants from format classes

* add a catch all accept header
2022-05-18 00:18:33 +02:00
Dag
4007afdcf5 Merge branch 'autoloading' into master 2022-05-17 23:59:18 +02:00
Dag
7d00b0c5df fix: include http code in exception (#2726) 2022-05-17 23:47:12 +02:00
Dag
0212c4790f fix: connectivityaction (#2725) 2022-05-17 23:46:37 +02:00
Kingsley Yung
7a87a09fc5 [YouTubeCommunityTab] Fix error occuring when YouTube returns non-English webpage. (#2739) 2022-05-17 09:35:16 +02:00
sysadminstory
1e3f5f3ad3 [PepperBridge] Update CSS selectors (#2740)
Updated some CSS selectors to follow the website change
2022-05-17 09:34:03 +02:00
Yaman Qalieh
f709778b28 [MydealsBridge] Fix Example value (#2728) 2022-05-14 08:04:21 -04:00
Yaman Qalieh
f4a0711b62 docker: fix find error (#2733) 2022-05-14 07:18:58 -04:00
Dag
4d069fcf99 remove unnecessary includes 2022-05-13 09:35:25 +02:00
Dag
f00f90328d refactor: extract class PepperBridgeAbstract 2022-05-13 09:29:56 +02:00
Yaman Qalieh
bb6d553dd5 Revert "refactor: remove unnecesary includes" (#2723)
This reverts commit fd449be4eb.
2022-05-12 16:28:03 -04:00
Joseph
97b513823d [HaveIBeenPwnedBridge] Fix item URIs (#2724) 2022-05-12 22:16:34 +02:00
Eric G
e01f0bcaf2 [GiteaBridge] Rewrite to decouple from Gogs and add contexts (#2718) 2022-05-12 22:15:03 +02:00
Yaman Qalieh
e5829d37b6 [HaveIBeenPwnedBridge] Use API to get Data (#2720) 2022-05-12 21:53:03 +02:00
Yaman Qalieh
73b1a6a7aa [FDroidRepoBridge] Add F-Droid Repo Bridge 2022-05-12 09:37:11 -04:00
Yaman Qalieh
e07fac777a core: Enable zip extension 2022-05-12 09:37:11 -04:00
Dag
fd449be4eb refactor: remove unnecesary includes 2022-05-12 15:15:09 +02:00
Dag
829fc6cca2 docker: Switch to nginx in docker image (#2721)
Co-authored-by: Yaman Qalieh <ybq987@gmail.com>
2022-05-11 20:19:25 -04:00
Dag
fcc3707210 refactor: swap the order of sprintf values 2022-05-11 22:37:59 +02:00
Dag
d5e9dbf47d refactor: restore some useful comments 2022-05-11 22:35:03 +02:00
User123698745
96a63a8e81 [PicukiBridge] fix image not displaying (#2717) 2022-05-10 19:53:29 +02:00
quickwick
9110b70f07 [TwitterV2Bridge] Properly include quoted tweets (#2713) 2022-05-10 09:41:12 +02:00
Yaman Qalieh
6547ed0c04 [docs] Add documentation for html.php functions (#2714) 2022-05-10 09:37:53 +02:00
Dag
8982995445 refactor: remove unused method 2022-05-09 23:32:45 +02:00
Dag
76084cdcca fix: logic bug in limiting 2022-05-09 21:02:21 +02:00
Dag
a28dca2c9d chore: upgrade phpunit 7 => 9
Upgraded with:

composer require -W --dev phpunit/phpunit:^9
2022-05-09 20:52:02 +02:00
sysadminstory
51f0d046d0 [AllocineFRBridge] Automatically find the last season for every show (#2709)
The bridge now finds the last season URI by itself, and don't rely on
static URL stored in the bridge itself.
2022-05-08 17:22:39 +02:00
Christian Schabesberger
fb2ed95368 Fix nordbayern (#2708)
* fix newspaper thumbnails are shown again

* show article teaser on top of title image for NN
2022-05-08 16:37:53 +02:00
Mynacol
36d11fd06e [XenForoBridge] Fix error if message is < 70 chars (#2707)
At the time of writing, this occurs on the following thread:
https://forum.xda-developers.com/t/optimized-lineageos19-1-v4-0-23apr.4426575/

Fixes the following error:
ValueError: strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack) in ./rss-bridge/bridges/XenForoBridge.php:272
Stack trace:
0 ./rss-bridge/bridges/XenForoBridge.php(272): strpos()
1 ./rss-bridge/bridges/XenForoBridge.php(146): XenForoBridge->extractThreadPostsV2()
2 ./rss-bridge/actions/DisplayAction.php(134): XenForoBridge->collectData()
3 ./rss-bridge/index.php(24): DisplayAction->execute()
4 {main}
2022-05-08 16:25:01 +02:00
quickwick
d107592094 Don't hide quoting tweets when 'hide retweets' is selected (#2706) 2022-05-08 16:22:31 +02:00
Yaman Qalieh
0ce71d561d [PixivBridge] [UnsplashBridge] Fix deprecated null (#2705) 2022-05-08 16:17:26 +02:00
Dag
f5a51038cc fix: error when passing null values
This bug was introduced by me when refactoring the http client.

Fixes:

Fatal error: Uncaught TypeError: Argument 2 passed to getContents() must be of the type array, null given
2022-05-08 04:42:24 +02:00
Yaman Qalieh
3476b06ee0 [MangaDexBridge] Exclude external chapters (#2703) 2022-05-08 04:22:33 +02:00
Yaman Qalieh
158ee41be4 [AtomFormat] Remove redundant fallback content (#2702) 2022-05-08 04:21:32 +02:00
sysadminstory
37843e8777 [RadioMelodieBridge] Fix date parsing (#2701)
The date is now correctly parsed for every month in the year (There are months mane in french that are 3 letters long
2022-05-08 04:19:06 +02:00
Mynacol
56e991122b [GolemBridge] Add golem.de bridge (#2696) 2022-05-08 04:08:55 +02:00
Dag
5d77d14f9d feat: add retry logic to the http client (#2692)
* refactor: extract http client

* feat: add retry logic to http client
2022-05-08 03:58:57 +02:00
Dag
0c7a7f320f refactor: BridgeFactory (#2691) 2022-05-08 03:58:42 +02:00
Dag
b2f1d051fc fix: don't bork upstream with http status code -1 (#2690) 2022-05-08 03:57:46 +02:00
Dag
641e2eedf5 test: exclude Pixiv for a particular test 2022-05-08 03:55:24 +02:00
Binnette
bc773a49f8 Full rewrite of bridge DeveloppezDotCom (#2689) 2022-05-08 03:38:33 +02:00
Yaman Qalieh
410daee1d5 [PixivBridge] Add User context (#2650) 2022-05-08 02:46:57 +02:00
Christian Schabesberger
adeaede930 [NordbayernBridge] Fix Bridge (#2675) 2022-05-02 19:06:30 +02:00
Dag
9b82ff352d fix: Fatal error: Uncaught ArgumentCountError 2022-05-01 21:35:52 +02:00
Nemo
31455b6838 [npci] Adds new NPCI Bridge (#2651) 2022-04-29 00:01:18 +02:00
Dag
63b08f7da9 Update app.json 2022-04-26 22:43:13 +02:00
Dag
61cfbe6c53 Update app.json 2022-04-26 22:41:34 +02:00
quickwick
4c26950b71 [TwitterV2Bridge] Fix empty object check (#2673) 2022-04-26 12:11:26 +02:00
pirnz
9dc31dfcfa [AsahiShimbunAJWBridge] Updated Asia section links (#2671) 2022-04-26 12:10:10 +02:00
Dag
db8462e6fa chore: add scripts section to composer.json (#2684) 2022-04-26 01:59:50 +02:00
Dag
19a8165fc6 docs: add host to public instances (#2685) 2022-04-26 01:41:04 +02:00
Dag
0ef298f9cc refactor: add php autoloader (#2655) 2022-04-26 00:57:59 +02:00
Corentin Garcia
b090b17bbf [RobinhoodSnacksBridge] fix bridge (#2676) 2022-04-26 00:53:18 +02:00
sysadminstory
ca749e7bad [ZoneTelechargement] Remove bridge (#2678)
Website announced the shutdown
2022-04-25 20:01:39 +02:00
Patrick Collins
e1c898848f [contents.php] Fix incorrect reference to UnexpectedResponseException's responseBody. (#2677) 2022-04-23 10:04:56 +05:00
Eric G
46a356b0b2 [GogsBridge] Add protocol to examplevalue (#2668) 2022-04-18 22:31:26 +02:00
Joseph
fe042305e4 [GoogleSearchBridge] Update bridge (#1869) 2022-04-16 23:16:38 +02:00
Loïc Fürhoff
669e92357a [Arte7Bridge] Exclude trailers and sort by v2 (#2664) 2022-04-16 23:08:27 +02:00
Nemo
1dec457b7b docu: Add back rss-bridge.bb8.fun (#2666) 2022-04-16 23:05:26 +02:00
TotalCaesar659
a38951b911 general: Update URLs to HTTPS (#2667) 2022-04-16 23:03:15 +02:00
Dag
b11f1368bf Update 06_Public_Hosts.md 2022-04-15 22:51:30 +02:00
Dag
1a698b3554 fix: remove dead public instances
https://rss-bridge.bb8.fun 404
https://myrss4fun.xyz not serving rss-bridge
https://rssbridge.fossdaily.xyz Curl failed for "https://rssbridge.fossdaily.xyz": Could not resolve host: rssbridge.fossdaily.xyz (6)
https://bridge.noisebridge.info 502
https://rss-bridge.esmailelbob.xyz 502
2022-04-15 22:51:12 +02:00
Dag
73ebdbf67a Revert "[Arte7Bridge] Exclude trailers and sort by (#2660)" (#2662)
This reverts commit 924eaf2011.

That commit broke the bridge.
2022-04-15 22:02:41 +02:00
Eugene Molotov
ac766aa47f [RutubeBridge] Add bridge (#2661) 2022-04-16 00:37:38 +05:00
Nemo
2be613e015 [BookMyShowBridge] Add new bridge (#1349) 2022-04-15 19:55:32 +02:00
Loïc Fürhoff
924eaf2011 [Arte7Bridge] Exclude trailers and sort by (#2660) 2022-04-14 23:20:09 +02:00
Alex Balgavy
d082bfca4a [SeznamZpravyBridge] fix: broken bridge (#2658) 2022-04-14 14:38:16 +02:00
Dag
91283f3a62 fix: deprecation notice (#2656) 2022-04-13 21:35:54 +02:00
Dag
d6beb713b5 chore: upgrade dependencies and improve package.json (#2648)
* chore: update composer dependencies

$ composer update
Loading composer repositories with package information
Updating dependencies
Info from https://repo.packagist.org: #StandWithUkraine
Lock file operations: 0 installs, 8 updates, 0 removals
  - Upgrading doctrine/instantiator (1.4.0 => 1.4.1)
  - Upgrading myclabs/deep-copy (1.10.2 => 1.11.0)
  - Upgrading phpdocumentor/reflection-docblock (5.2.2 => 5.3.0)
  - Upgrading phpdocumentor/type-resolver (1.4.0 => 1.6.1)
  - Upgrading phpspec/prophecy (1.13.0 => v1.15.0)
  - Upgrading phpunit/php-file-iterator (2.0.4 => 2.0.5)
  - Upgrading sebastian/exporter (3.1.3 => 3.1.4)
  - Upgrading symfony/polyfill-ctype (v1.23.0 => v1.25.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 0 installs, 8 updates, 0 removals
  - Upgrading symfony/polyfill-ctype (v1.23.0 => v1.25.0): Extracting archive
  - Upgrading phpdocumentor/type-resolver (1.4.0 => 1.6.1): Extracting archive
  - Upgrading phpdocumentor/reflection-docblock (5.2.2 => 5.3.0): Extracting archive
  - Upgrading sebastian/exporter (3.1.3 => 3.1.4): Extracting archive
  - Upgrading phpunit/php-file-iterator (2.0.4 => 2.0.5): Extracting archive
  - Upgrading doctrine/instantiator (1.4.0 => 1.4.1): Extracting archive
  - Upgrading phpspec/prophecy (1.13.0 => v1.15.0): Extracting archive
  - Upgrading myclabs/deep-copy (1.10.2 => 1.11.0): Extracting archive
Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested.
Generating autoload files
16 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

* chore: add dev-dependency squizlabs/php_codesniffer (phpcs)

$ composer require --dev squizlabs/php_codesniffer
Info from https://repo.packagist.org: #StandWithUkraine
Using version ^3.6 for squizlabs/php_codesniffer
./composer.json has been updated
Running composer update squizlabs/php_codesniffer
Loading composer repositories with package information
Updating dependencies
Info from https://repo.packagist.org: #StandWithUkraine
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking squizlabs/php_codesniffer (3.6.2)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing squizlabs/php_codesniffer (3.6.2): Extracting archive
Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested.
Generating autoload files
16 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

* chore: add package type => "project" in composer.json
2022-04-13 21:04:27 +02:00
Dag
d62b977394 refactor: ./tests (#2649)
* refactor: ./tests

* test: consolidate testsuites

* refactor: move config setup into rssbridge.php

Makes it easier to unit test.

* lint
2022-04-13 21:04:10 +02:00
Austin Huang
183004f954 Update 06_Public_Hosts.md (#2654)
1. nixnet.xyz => nixnet.services
2. Add my instance

Supersedes #2653
2022-04-13 20:36:28 +02:00
Shikiryu
ff8ece213f [PicalaBridge] Add new bridge (#2646) 2022-04-13 13:25:32 +02:00
Nemo
3e5675c256 [GoodreadsBridge] Add new bridge (#1559) 2022-04-13 13:18:05 +02:00
Dag
5a7d305e07 [Nordbayern] fix: problem with absolute and relative link (#2637)
* [Nordbayern] fix: problem with absolute and relative link

Fixes:

cURL error: Could not resolve host: www.nordbayern.dehttps
2022-04-12 23:40:37 +02:00
Dag
7379e2b3d5 [Parler] feat: add new bridge (#2634) 2022-04-12 23:39:32 +02:00
Dag
57c8806954 [ParuVenduImmo] fix: try to repair broken css selectors (#2641)
* [ParuVenduImmo] fix: try to repair broken css selectors

Needs more work.
2022-04-12 23:37:54 +02:00
Dag
b6e8350596 fix: a bunch of small changes in multiple bridges (#2644)
Mostly refactors.
2022-04-12 23:37:30 +02:00
Dag
5b7dd45b20 [UsbekEtRica] fix: broken css selectors (#2643) 2022-04-12 23:37:17 +02:00
Dag
f9801a5c58 [RoadAndTrack] fix: broken css selectors (#2642) 2022-04-12 23:37:05 +02:00
Dag
563c099d80 [NFLRUS] fix: broken css selectors (#2640)
This bridge needs more work.
2022-04-12 23:35:04 +02:00
Dag
b6e8e3ea6e [N26] fix: broken css selectors (#2639) 2022-04-12 23:34:52 +02:00
Dag
9e2e32a19d [Amazon] fix: broken css selectors (#2638) 2022-04-12 23:34:40 +02:00
Dag
df5c259375 [LaCentrale] fix: broken css selectors (#2636) 2022-04-12 23:34:23 +02:00
quickwick
6021d2ffa6 [GelbooruBridge] Change tag examplevalue to one valid for all inheriting bridges (#2645) 2022-04-12 16:52:34 +02:00
Dag
908da78113 Update phpcs.xml 2022-04-11 02:28:41 +02:00
Dag
a28481aaa8 [XenForo] fix: sort items by date in descending order (#2633) 2022-04-11 00:42:53 +02:00
Dag
bb81af086f [Castorus] fix: htmlentities bug (#2632) 2022-04-10 22:47:02 +02:00
Dag
60f1c46779 docs: move screenshots from wiki to repo (#2631) 2022-04-10 22:23:06 +02:00
Corentin Garcia
ae760e40cc replace wiki links with documentation links (#2630)
* docs: update composer support links

* docs: update link from wiki to docs for github issue template

* docs: update link pointing to wiki to point to new documentation

* docs: replace wiki links by documentation links in README and
CONTRIBUTING files
2022-04-10 21:58:29 +02:00
dag
5a733b3d82 feat: add limit options to the slowest bridges 2022-04-10 18:56:24 +02:00
dag
0b40f51c01 [Picuki] fix: item parsing (#2619)
Fixes a problem with the entire content being a link.

Also truncate title.

They have referrer checks on their images. So clicking the
enclosure doesnt work. Will fix later.
2022-04-10 18:54:48 +02:00
dag
dbee47f1d6 fix: give better error message when feed can't be parsed (#2618) 2022-04-10 18:54:32 +02:00
dag
c3a106892d fix: require curl extension (#2617) 2022-04-10 18:54:18 +02:00
quickwick
db28bedb23 [TwitterV2Bridge] Changes to output HTML/CSS, larger display image by default (#2626) 2022-04-10 18:53:35 +02:00
User123698745
aacf5812ff [GiphyBridge] include search text in feed name (#2628)
Co-authored-by: User123698745 <usr123698745+git@gmail.com>
2022-04-10 18:06:33 +02:00
User123698745
bf2f9a06f9 [Docker] force unix line ending on bash files to prevent docker run failing on windows (#2629) 2022-04-10 18:05:43 +02:00
User123698745
7833d0e6c3 [GiphyBridge] include bundle parameter in api calls to reduce bandwidth (#2627) 2022-04-10 15:21:43 +02:00
Corentin Garcia
c498749c2b [TwitterEngineeringBridge] add bridge #2385 (#2623) 2022-04-10 11:47:42 +02:00
User123698745
722f9ff0ce [GiphyBridge] use not rate limited public api key (#2625)
Co-authored-by: User123698745 <usr123698745+git@gmail.com>
2022-04-10 11:37:02 +02:00
Vít Kabele
5c08984714 InstagramBridge.php: Display usernames and hashtags as links. (#1582) 2022-04-08 23:47:10 +02:00
dag
dc01891634 fix: enclosure link privacy (#2620) 2022-04-08 23:14:43 +02:00
dag
cce11964a4 feat: add a timeout option for http client (#2600) 2022-04-08 21:22:13 +02:00
Corentin Garcia
8c18c02c65 [GatesNotesBridge] Add feedaxpander bridge for Bill Gate's blog (fix issue #2386) (#2611) 2022-04-08 21:21:13 +02:00
Antoine Turmel
51d27300be [FeedMergeBridge] Add new bridge (#1385)
* [FeedMergeBridge] Add new bridge

Here is a bridge that merges two or more feeds into one.

Co-authored-by: Bocki <henning@bocklage.com>
Co-authored-by: Dag <me@dvikan.no>
2022-04-08 21:13:05 +02:00
586 changed files with 69835 additions and 55858 deletions

8
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM rssbridge/rss-bridge:latest
RUN apt-get update && \
apt-get install --yes --no-install-recommends \
git && \
pecl install xdebug && \
pear install PHP_CodeSniffer && \
docker-php-ext-enable xdebug

View File

@@ -0,0 +1,27 @@
{
"name": "rss-bridge dev",
"build": { "dockerfile": "Dockerfile" },
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"php.validate.executablePath": "/usr/local/bin/php",
"phpSniffer.executablesFolder": "/usr/local/bin/",
"phpcs.executablePath": "/usr/local/bin/phpcs",
"phpcs.lintOnType": false
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"xdebug.php-debug",
"bmewburn.vscode-intelephense-client",
"philfontaine.autolaunch",
"eamodio.gitlens",
"shevaua.phpcs"
]
}
},
"forwardPorts": [3100, 9000, 9003],
"postCreateCommand": "cp .devcontainer/nginx.conf /etc/nginx/conf.d/default.conf && cp .devcontainer/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && mkdir .vscode && cp .devcontainer/launch.json .vscode && echo '*' > whitelist.txt && chmod a+x \"$(pwd)\" && rm -rf /var/www/html && ln -s \"$(pwd)\" /var/www/html && nginx && php-fpm -D"
}

49
.devcontainer/launch.json Normal file
View File

@@ -0,0 +1,49 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"auto": true
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 0,
"runtimeArgs": [
"-dxdebug.start_with_request=yes"
],
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
},
{
"name": "Launch Built-in web server",
"type": "php",
"request": "launch",
"runtimeArgs": [
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-S",
"localhost:0"
],
"program": "",
"cwd": "${workspaceRoot}",
"port": 9003,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
}
}
]
}

17
.devcontainer/nginx.conf Normal file
View File

@@ -0,0 +1,17 @@
server {
listen 3100 default_server;
root /workspaces/rss-bridge;
access_log /var/log/nginx/rssbridge.access.log;
error_log /var/log/nginx/rssbridge.error.log;
index index.php;
location ~ /(\.|vendor|tests) {
deny all;
return 403; # Forbidden
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass 127.0.0.1:9000;
}
}

7
.devcontainer/xdebug.ini Normal file
View File

@@ -0,0 +1,7 @@
[xdebug]
xdebug.mode=develop,debug
xdebug.client_host=localhost
xdebug.client_port=9003
xdebug.start_with_request=yes
xdebug.discover_client_host=false
xdebug.log='/var/www/html/xdebug.log'

4
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,4 @@
# Reformat code base to PSR12
4f75591060d95208a301bc6bf460d875631b29cc
# Fix coding style missed by phpbcf
951092eef374db048b77bac85e75e3547bfac702

1
.gitattributes vendored
View File

@@ -1,5 +1,6 @@
# Auto detect text files and perform LF normalization
* text=auto
*.sh text eol=lf
# Custom for Visual Studio
*.cs diff=csharp

View File

@@ -1,49 +1,7 @@
### Pull request policy
* [Fix one issue per pull request](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#fix-one-issue-per-pull-request)
* [Respect the coding style policy](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#respect-the-coding-style-policy)
* [Properly name your commits](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#properly-name-your-commits)
* When fixing a bridge (located in the `bridges` directory), write `[BridgeName] Feature` <br>(i.e. `[YoutubeBridge] Fix typo in video titles`).
* When fixing other files, use `[FileName] Feature` <br>(i.e. `[index.php] Add multilingual support`).
* When fixing a general problem that applies to multiple files, write `category: feature` <br>(i.e. `bridges: Fix various typos`).
Note that all pull-requests must pass all tests before they can be merged.
See the [Pull request policy page on the documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/Pull_Request_policy.html) for more information on the pull request policy.
### Coding style
* [Whitespace](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace)
* [Add a new line at the end of a file](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
* [Do not add a whitespace before a semicolon](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
* [Do not add whitespace at start or end of a file or end of a line](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#do-not-add-whitespace-at-start-or-end-of-a-file-or-end-of-a-line)
* [Indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation)
* [Use tabs for indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation#use-tabs-for-indentation)
* [Maximum line length](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length)
* [The maximum line length should not exceed 80 characters](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length#the-maximum-line-length-should-not-exceed-80-characters)
* [Strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings)
* [Whenever possible use single quoted strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#whenever-possible-use-single-quote-strings)
* [Add spaces around the concatenation operator](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#add-spaces-around-the-concatenation-operator)
* [Use a single string instead of concatenating](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#use-a-single-string-instead-of-concatenating)
* [Constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants)
* [Use UPPERCASE for constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants#use-uppercase-for-constants)
* [Keywords](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords)
* [Use lowercase for `true`, `false` and `null`](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords#use-lowercase-for-true-false-and-null)
* [Operators](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators)
* [Operators must have a space around them](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators#operators-must-have-a-space-around-them)
* [Functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions)
* [Parameters with default values must appear last in functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#parameters-with-default-values-must-appear-last-in-functions)
* [Calling functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#calling-functions)
* [Do not add spaces after opening or before closing bracket](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#do-not-add-spaces-after-opening-or-before-closing-bracket)
* [Structures](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures)
* [Structures must always be formatted as multi-line blocks](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures#structures-must-always-be-formatted-as-multi-line-blocks)
* [If-Statement](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement)
* [Use `elseif` instead of `else if`](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#use-elseif-instead-of-else-if)
* [Do not write empty statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-empty-statements)
* [Do not write unconditional if-statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-unconditional-if-statements)
* [Classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes)
* [Use PascalCase for class names](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#use-pascalcase-for-class-names)
* [Do not use final statements inside final classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-use-final-statements-inside-final-classes)
* [Do not override methods to call their parent](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-override-methods-to-call-their-parent)
* [abstract and final declarations MUST precede the visibility declaration](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#abstract-and-final-declarations-must-precede-the-visibility-declaration)
* [static declaration MUST come after the visibility declaration](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#static-declaration-must-come-after-the-visibility-declaration)
* [Casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting)
* [Do not add spaces when casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting#do-not-add-spaces-when-casting)
See the [Coding style policy page on the documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/Coding_style_policy.html) for more information on the coding style of the project.

View File

@@ -60,5 +60,5 @@ Please describe what you expect from the bridge. Whenever possible provide sampl
Keep in mind that opening a request does not guarantee the bridge being implemented! That depends entirely on the interest and time of others to make the bridge for you.
You can also implement your own bridge (with support of the community if needed). Find more information in the [RSS-Bridge Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/For-developers) developer section.
You can also implement your own bridge (with support of the community if needed). Find more information in the [RSS-Bridge Documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/index.html) developer section.
-->

28
.github/prtester.py vendored
View File

@@ -1,4 +1,5 @@
import requests
import itertools
from bs4 import BeautifulSoup
from datetime import datetime
import os.path
@@ -15,6 +16,7 @@ def testBridges(bridges,status):
if bridge.get('data-ref'): # Some div entries are empty, this ignores those
bridgeid = bridge.get('id')
bridgeid = bridgeid.split('-')[1] # this extracts a readable bridge name from the bridge metadata
print(bridgeid + "\n")
bridgestring = '/?action=display&bridge=' + bridgeid + '&format=Html'
forms = bridge.find_all("form")
formid = 1
@@ -47,20 +49,30 @@ def testBridges(bridges,status):
if parameter.get('type') == 'checkbox':
if parameter.has_attr('checked'):
formstring = formstring + '&' + parameter.get('name') + '=on'
for list in lists:
for listing in lists:
selectionvalue = ''
for selectionentry in list.contents:
if 'selected' in selectionentry.attrs:
listname = listing.get('name')
cleanlist = []
for option in listing.contents:
if 'optgroup' in option.name:
cleanlist.extend(option)
else:
cleanlist.append(option)
firstselectionentry = 1
for selectionentry in cleanlist:
if firstselectionentry:
selectionvalue = selectionentry.get('value')
break
if selectionvalue == '':
selectionvalue = list.contents[0].get('value')
formstring = formstring + '&' + list.get('name') + '=' + selectionvalue
firstselectionentry = 0
else:
if 'selected' in selectionentry.attrs:
selectionvalue = selectionentry.get('value')
break
formstring = formstring + '&' + listname + '=' + selectionvalue
if not errormessages:
# if all example/default values are present, form the full request string, run the request, replace the static css
# file with the url of em's public instance and then upload it to termpad.com, a pastebin-like-site.
r = requests.get(URL + bridgestring + formstring)
pagetext = r.text.replace('static/HtmlFormat.css','https://feed.eugenemolotov.ru/static/HtmlFormat.css')
pagetext = r.text.replace('static/style.css','https://rss-bridge.org/bridge01/static/style.css')
pagetext = pagetext.encode("utf_8")
termpad = requests.post(url="https://termpad.com/", data=pagetext)
termpadurl = termpad.text

View File

@@ -17,11 +17,11 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2.3.4
uses: actions/checkout@v3
-
name: Docker meta
id: docker_meta
uses: docker/metadata-action@v3.5.0
uses: docker/metadata-action@v4
with:
images: |
${{ env.DOCKERHUB_SLUG }}
@@ -33,26 +33,26 @@ jobs:
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/20') }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
uses: docker/setup-buildx-action@v2
-
name: Login to DockerHub
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Login to GitHub Container Registry
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
uses: docker/bake-action@v1.6.0
uses: docker/bake-action@v2
with:
files: |
./docker-bake.hcl

View File

@@ -9,11 +9,11 @@ jobs:
documentation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@2.17.1
uses: shivammathur/setup-php@v2
with:
php-version: 8.0
- name: Install dependencies
@@ -21,7 +21,7 @@ jobs:
- name: Generate documentation
run: daux generate
- name: Deploy same repository 🚀
uses: JamesIves/github-pages-deploy-action@v4.2.5
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: "static"
branch: gh-pages
branch: gh-pages

View File

@@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-versions: ['7.1', '7.2', '7.3', '7.4']
php-versions: ['7.4']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
@@ -24,12 +24,13 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-versions: ['7.1', '7.4']
php-versions: ['7.4']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- run: composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
- 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
@@ -37,7 +38,7 @@ jobs:
executable_php_files_check:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- run: |
if find -name "*.php" -executable -type f -print -exec false {} +
then

View File

@@ -11,7 +11,7 @@ jobs:
# Needs additional permissions https://github.com/actions/first-interaction/issues/10#issuecomment-1041402989
steps:
- name: Check out self
uses: actions/checkout@v2.3.2
uses: actions/checkout@v3
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -31,7 +31,7 @@ jobs:
docker build -t prbuild .;
docker run -d -v $GITHUB_WORKSPACE/whitelist.txt:/app/whitelist.txt -v $GITHUB_WORKSPACE/DEBUG:/app/DEBUG -p 3001:80 prbuild
- name: Setup python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.7'
cache: 'pip'
@@ -48,8 +48,7 @@ jobs:
body="${body//'%'/'%25'}";
body="${body//$'\n'/'%0A'}";
body="${body//$'\r'/'%0D'}";
echo "::set-output name=bodylength::${#body}"
echo "::set-output name=body::$body"
echo "bodylength=${#body}" >> $GITHUB_OUTPUT
- name: Find Comment
if: ${{ steps.testrun.outputs.bodylength > 130 }}
uses: peter-evans/find-comment@v2
@@ -64,6 +63,5 @@ jobs:
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
${{ steps.testrun.outputs.body }}
edit-mode: replace
body-file: comment.txt
edit-mode: replace

View File

@@ -7,28 +7,15 @@ on:
branches: [ master ]
jobs:
phpunit7:
runs-on: ubuntu-20.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-20.04
strategy:
matrix:
php-versions: ['7.3', '7.4', '8.0', '8.1']
php-versions: ['7.4', '8.0', '8.1']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- 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/
- run: composer install
- run: composer test

1
.gitignore vendored
View File

@@ -229,6 +229,7 @@ pip-log.txt
/whitelist.txt
DEBUG
config.ini.php
config/*
######################
## VisualStudioCode ##

223
CONTRIBUTORS.md Normal file
View File

@@ -0,0 +1,223 @@
# Contributors
* [16mhz](https://github.com/16mhz)
* [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)
* [Alkarex](https://github.com/Alkarex)
* [AmauryCarrade](https://github.com/AmauryCarrade)
* [arnd-s](https://github.com/arnd-s)
* [ArthurHoaro](https://github.com/ArthurHoaro)
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
* [austinhuang0131](https://github.com/austinhuang0131)
* [AxorPL](https://github.com/AxorPL)
* [ayacoo](https://github.com/ayacoo)
* [az5he6ch](https://github.com/az5he6ch)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
* [Binnette](https://github.com/Binnette)
* [BoboTiG](https://github.com/BoboTiG)
* [Bockiii](https://github.com/Bockiii)
* [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)
* [da2x](https://github.com/da2x)
* [dabenzel](https://github.com/dabenzel)
* [Daiyousei](https://github.com/Daiyousei)
* [dawidsowa](https://github.com/dawidsowa)
* [DevonHess](https://github.com/DevonHess)
* [dhuschde](https://github.com/dhuschde)
* [disk0x](https://github.com/disk0x)
* [DJCrashdummy](https://github.com/DJCrashdummy)
* [Djuuu](https://github.com/Djuuu)
* [DnAp](https://github.com/DnAp)
* [dominik-th](https://github.com/dominik-th)
* [Draeli](https://github.com/Draeli)
* [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
* [drego85](https://github.com/drego85)
* [drklee3](https://github.com/drklee3)
* [DRogueRonin](https://github.com/DRogueRonin)
* [dvikan](https://github.com/dvikan)
* [eggwhalefrog](https://github.com/eggwhalefrog)
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [f0086](https://github.com/f0086)
* [fanch317](https://github.com/fanch317)
* [fatuuse](https://github.com/fatuuse)
* [fivefilters](https://github.com/fivefilters)
* [floviolleau](https://github.com/floviolleau)
* [fluffy-critter](https://github.com/fluffy-critter)
* [fmachen](https://github.com/fmachen)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
* [ggiessen](https://github.com/ggiessen)
* [gileri](https://github.com/gileri)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [girlpunk](https://github.com/girlpunk)
* [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)
* [imagoiq](https://github.com/imagoiq)
* [j0k3r](https://github.com/j0k3r)
* [JackNUMBER](https://github.com/JackNUMBER)
* [jacquesh](https://github.com/jacquesh)
* [jakubvalenta](https://github.com/jakubvalenta)
* [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)
* [jNullj](https://github.com/jNullj)
* [Jocker666z](https://github.com/Jocker666z)
* [johnnygroovy](https://github.com/johnnygroovy)
* [johnpc](https://github.com/johnpc)
* [joni1993](https://github.com/joni1993)
* [jtojnar](https://github.com/jtojnar)
* [KamaleiZestri](https://github.com/KamaleiZestri)
* [kkoyung](https://github.com/kkoyung)
* [klimplant](https://github.com/klimplant)
* [KN4CK3R](https://github.com/KN4CK3R)
* [kolarcz](https://github.com/kolarcz)
* [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc)
* [krisu5](https://github.com/krisu5)
* [l1n](https://github.com/l1n)
* [laBecasse](https://github.com/laBecasse)
* [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev)
* [langfingaz](https://github.com/langfingaz)
* [lassana](https://github.com/lassana)
* [ldidry](https://github.com/ldidry)
* [Leomaradan](https://github.com/Leomaradan)
* [leyrer](https://github.com/leyrer)
* [liamka](https://github.com/liamka)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos)
* [lukasklinger](https://github.com/lukasklinger)
* [m0zes](https://github.com/m0zes)
* [Mar-Koeh](https://github.com/Mar-Koeh)
* [marcus-at-localhost](https://github.com/marcus-at-localhost)
* [marius8510000-bot](https://github.com/marius8510000-bot)
* [matthewseal](https://github.com/matthewseal)
* [mcbyte-it](https://github.com/mcbyte-it)
* [mdemoss](https://github.com/mdemoss)
* [melangue](https://github.com/melangue)
* [metaMMA](https://github.com/metaMMA)
* [mibe](https://github.com/mibe)
* [mickaelBert](https://github.com/mickaelBert)
* [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)
* [muekoeff](https://github.com/muekoeff)
* [mw80](https://github.com/mw80)
* [mxmehl](https://github.com/mxmehl)
* [Mynacol](https://github.com/Mynacol)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [Niehztog](https://github.com/Niehztog)
* [NikNikYkt](https://github.com/NikNikYkt)
* [Nono-m0le](https://github.com/Nono-m0le)
* [obsiwitch](https://github.com/obsiwitch)
* [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)
* [Patricol](https://github.com/Patricol)
* [paulchen](https://github.com/paulchen)
* [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)
* [pirnz](https://github.com/pirnz)
* [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)
* [pubak42](https://github.com/pubak42)
* [Qluxzz](https://github.com/Qluxzz)
* [quentinus95](https://github.com/quentinus95)
* [quickwick](https://github.com/quickwick)
* [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)
* [s0lesurviv0r](https://github.com/s0lesurviv0r)
* [sal0max](https://github.com/sal0max)
* [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)
* [SpangleLabs](https://github.com/SpangleLabs)
* [SqrtMinusOne](https://github.com/SqrtMinusOne)
* [squeek502](https://github.com/squeek502)
* [StelFux](https://github.com/StelFux)
* [stjohnjohnson](https://github.com/stjohnjohnson)
* [Stopka](https://github.com/Stopka)
* [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)
* [TheRadialActive](https://github.com/TheRadialActive)
* [theScrabi](https://github.com/theScrabi)
* [thezeroalpha](https://github.com/thezeroalpha)
* [thibaultcouraud](https://github.com/thibaultcouraud)
* [timendum](https://github.com/timendum)
* [TitiTestScalingo](https://github.com/TitiTestScalingo)
* [tomaszkane](https://github.com/tomaszkane)
* [tomershvueli](https://github.com/tomershvueli)
* [TotalCaesar659](https://github.com/TotalCaesar659)
* [tpikonen](https://github.com/tpikonen)
* [TReKiE](https://github.com/TReKiE)
* [triatic](https://github.com/triatic)
* [User123698745](https://github.com/User123698745)
* [VerifiedJoseph](https://github.com/VerifiedJoseph)
* [vitkabele](https://github.com/vitkabele)
* [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)
* [yue-dongchen](https://github.com/yue-dongchen)
* [ZeNairolf](https://github.com/ZeNairolf)

View File

@@ -1,24 +1,36 @@
FROM php:7-apache-buster
FROM lwthiker/curl-impersonate:0.5-ff-slim-buster AS curlimpersonate
FROM php:8.0.27-fpm-buster AS rssbridge
LABEL description="RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one."
LABEL repository="https://github.com/RSS-Bridge/rss-bridge"
LABEL website="https://github.com/RSS-Bridge/rss-bridge"
ENV APACHE_DOCUMENT_ROOT=/app
RUN apt-get update && \
apt-get install --yes --no-install-recommends \
nginx \
zlib1g-dev \
libzip-dev \
libmemcached-dev \
nss-plugin-pem \
libicu-dev && \
docker-php-ext-install zip && \
docker-php-ext-install intl && \
pecl install memcached && \
docker-php-ext-enable memcached && \
docker-php-ext-enable opcache && \
mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
&& apt-get --yes update \
&& apt-get --yes --no-install-recommends install \
zlib1g-dev \
libmemcached-dev \
&& rm -rf /var/lib/apt/lists/* \
&& pecl install memcached \
&& docker-php-ext-enable memcached \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
&& sed -ri -e 's/(MinProtocol\s*=\s*)TLSv1\.2/\1None/' /etc/ssl/openssl.cnf \
&& sed -ri -e 's/(CipherString\s*=\s*DEFAULT)@SECLEVEL=2/\1/' /etc/ssl/openssl.cnf
COPY ./config/nginx.conf /etc/nginx/sites-enabled/default
COPY --chown=www-data:www-data ./ /app/
CMD ["/app/docker-entrypoint.sh"]
COPY --from=curlimpersonate /usr/local/lib/libcurl-impersonate-ff.so /usr/local/lib/curl-impersonate/
ENV LD_PRELOAD /usr/local/lib/curl-impersonate/libcurl-impersonate-ff.so
ENV CURL_IMPERSONATE ff91esr
EXPOSE 80
ENTRYPOINT ["/app/docker-entrypoint.sh"]

539
README.md
View File

@@ -1,42 +1,242 @@
# RSS-Bridge
![RSS-Bridge](static/logo_600px.png)
===
RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one.
[![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)
[![irc.libera.chat](https://img.shields.io/badge/irc.libera.chat-%23rssbridge-blue.svg)](https://web.libera.chat/#rssbridge)
[![Chat on Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#rssbridge:libera.chat)
[![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/)
[![Actions Status](https://img.shields.io/github/actions/workflow/status/RSS-Bridge/rss-bridge/tests.yml?branch=master&label=GitHub%20Actions&logo=github)](https://github.com/RSS-Bridge/rss-bridge/actions)
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.
|||
|:-:|:-:|
|![Screenshot #1](/static/screenshot-1.png?raw=true)|![Screenshot #2](/static/screenshot-2.png?raw=true)|
|![Screenshot #3](/static/screenshot-3.png?raw=true)|![Screenshot #4](/static/screenshot-4.png?raw=true)|
|![Screenshot #5](/static/screenshot-5.png?raw=true)|![Screenshot #6](/static/screenshot-6.png?raw=true)|
|![Screenshot #7](/static/twitter-form.png?raw=true)|![Screenshot #8](/static/twitter-rasmus.png?raw=true)|
**Important**: RSS-Bridge is __not__ a feed reader or feed aggregator, but a tool to generate feeds that are consumed by feed readers and feed aggregators. Find a list of feed aggregators on [Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators).
## A subset of bridges
Supported sites/pages (examples)
===
* `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
* `Cryptome` : Returns the most recent documents from [Cryptome.org](http://cryptome.org/)
* `DansTonChat`: Most recent quotes from [danstonchat.com](http://danstonchat.com/)
* `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
* `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/) (There is an [issue](https://github.com/RSS-Bridge/rss-bridge/issues/2047) for public instances)
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
* `GoogleSearch` : Most recent results from Google Search
* `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
* `Instagram`: Most recent photos from an Instagram user (It is recommended to [configure](https://rss-bridge.github.io/rss-bridge/Bridge_Specific/Instagram.html) this bridge to work)
* `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
* `Pinterest`: Most recent photos from user or search
* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://secouchermoinsbete.fr/)
* `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
* `Twitter` : Return keyword/hashtag search or user timeline
* `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
* `YouTube` : YouTube user channel, playlist or search
* `Twitter` : Return keyword/hashtag search or user timeline
* `Telegram` : Return the latest posts from a public group
* `Reddit` : Return the latest posts from a subreddit or user
* `Filter` : Filter an existing feed url
* `Vk` : Latest posts from a user or group
* `FeedMerge` : Merge two or more existing feeds into one
* `Twitch` : Fetch the latest videos from a channel
* `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
And [many more](bridges/), thanks to the community!
Output format
===
[Full documentation](https://rss-bridge.github.io/rss-bridge/index.html)
RSS-Bridge is capable of producing several output formats:
Check out RSS-Bridge right now on https://rss-bridge.org/bridge01 or find another
[public instance](https://rss-bridge.github.io/rss-bridge/General/Public_Hosts.html).
## Tutorial
RSS-Bridge requires php 7.4 (or higher).
### Install with composer or git
```shell
cd /var/www
composer create-project --no-dev rss-bridge/rss-bridge
```
```shell
cd /var/www
git clone https://github.com/RSS-Bridge/rss-bridge.git
```
Config:
```shell
# Give the http user write permission to the cache folder
chown www-data:www-data /var/www/rss-bridge/cache
# Optionally copy over the default config file
cp config.default.ini.php config.ini.php
```
Example config for nginx:
```nginx
# /etc/nginx/sites-enabled/rssbridge
server {
listen 80;
server_name example.com;
root /var/www/rss-bridge;
index index.php;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_read_timeout 60s;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}
```
### Install with Docker:
Install by using docker image from Docker Hub:
```bash
# Create container
docker create --name=rss-bridge --publish 3000:80 rssbridge/rss-bridge
# Start container
docker start rss-bridge
```
Browse http://localhost:3000/
Install by locally building the image:
```bash
# Build image from Dockerfile
docker build -t rss-bridge .
# Create container
docker create --name rss-bridge --publish 3000:80 rss-bridge
# Start the container
docker start rss-bridge
```
Browse http://localhost:3000/
#### Install with docker-compose
Create a `docker-compose.yml` file locally with with the following content:
```yml
version: '2'
services:
rss-bridge:
image: rssbridge/rss-bridge:latest
volumes:
- </local/custom/path>:/config
ports:
- 3000:80
restart: unless-stopped
```
Then launch with `docker-compose`:
```bash
docker-compose up
```
Browse http://localhost:3000/
### Alternative installation methods
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/sebsauvage/rss-bridge)
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
[![Deploy to Cloudron](https://cloudron.io/img/button.svg)](https://www.cloudron.io/store/com.rssbridgeapp.cloudronapp.html)
The Heroku quick deploy currently does not work. It might possibly work if you fork this repo and
modify the `repository` in `scalingo.json`. See https://github.com/RSS-Bridge/rss-bridge/issues/2688
Learn more in
[Installation](https://rss-bridge.github.io/rss-bridge/For_Hosts/Installation.html).
## How-to
### How to create a new bridge from scratch
Create the new bridge in e.g. `bridges/BearBlogBridge.php`:
```php
<?php
class BearBlogBridge extends BridgeAbstract
{
const NAME = 'BearBlog (bearblog.dev)';
public function collectData()
{
$dom = getSimpleHTMLDOM('https://herman.bearblog.dev/blog/');
foreach ($dom->find('.blog-posts li') as $li) {
$a = $li->find('a', 0);
$this->items[] = [
'title' => $a->plaintext,
'uri' => 'https://herman.bearblog.dev' . $a->href,
];
}
}
}
```
Learn more in [bridge api](https://rss-bridge.github.io/rss-bridge/Bridge_API/index.html).
### How to enable all bridges
enabled_bridges[] = *
### How to enable some bridges
```
enabled_bridges[] = TwitchBridge
enabled_bridges[] = GettrBridge
```
### How to enable debug mode
enable_debug_mode = true
Learn more in [debug mode](https://rss-bridge.github.io/rss-bridge/For_Developers/Debug_mode.html).
### How to create a new output format
[Create a new format](https://rss-bridge.github.io/rss-bridge/Format_API/index.html).
## Explanation
We are RSS-Bridge community, a group of developers continuing the project initiated by sebsauvage,
webmaster of
[sebsauvage.net](https://sebsauvage.net), author of
[Shaarli](https://sebsauvage.net/wiki/doku.php?id=php:shaarli) and
[ZeroBin](https://sebsauvage.net/wiki/doku.php?id=php:zerobin).
See [CONTRIBUTORS.md](CONTRIBUTORS.md)
RSS-Bridge uses caching to prevent services from banning your server for repeatedly updating feeds.
The specific cache duration can be different between bridges. Cached files are deleted automatically after 24 hours.
RSS-Bridge allows you to take full control over which bridges are displayed to the user.
That way you can host your own RSS-Bridge service with your favorite collection of bridges!
## Reference
### FeedItem properties
```php
$item = [
'uri' => 'https://example.com/blog/hello',
'title' => 'Hello world',
// Publication date in unix timestamp
'timestamp' => 1668706254,
'author' => 'Alice',
'content' => 'Here be item content',
'enclosures' => [
'https://example.com/foo.png',
'https://example.com/bar.png'
],
'categories' => [
'news',
'tech',
],
// Globally unique id
'uid' => 'e7147580c8747aad',
]
```
### Output formats:
* `Atom` : Atom feed, for use in feed readers
* `Html` : Simple HTML page
@@ -44,293 +244,18 @@ RSS-Bridge is capable of producing several output formats:
* `Mrss` : MRSS feed, for use in feed readers
* `Plaintext` : Raw text, for consumption by other applications
You can extend RSS-Bridge with your own format, using the [Format API](https://github.com/RSS-Bridge/rss-bridge/wiki/Format-API)!
Screenshot
===
Welcome screen:
![Screenshot](https://github.com/RSS-Bridge/rss-bridge/wiki/images/screenshot_rss-bridge_welcome.png)
***
RSS-Bridge hashtag (#rss-bridge) search on Twitter, in Atom format (as displayed by Firefox):
![Screenshot](https://github.com/RSS-Bridge/rss-bridge/wiki/images/screenshot_twitterbridge_atom.png)
Requirements
===
RSS-Bridge requires PHP 7.1 or higher with following extensions enabled:
- [`openssl`](https://secure.php.net/manual/en/book.openssl.php)
- [`libxml`](https://secure.php.net/manual/en/book.libxml.php)
- [`mbstring`](https://secure.php.net/manual/en/book.mbstring.php)
- [`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)
Enable / Disable bridges
===
RSS-Bridge allows you to take full control over which bridges are displayed to the user. That way you can host your own RSS-Bridge service with your favorite collection of bridges!
Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting)
**Notice**: By default, RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge!
Deploy
===
Thanks to the community, hosting your own instance of RSS-Bridge is as easy as clicking a button!
*Note: External providers' applications are packaged by 3rd parties. Use at your own discretion.*
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/sebsauvage/rss-bridge)
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
[![Deploy to Cloudron](https://cloudron.io/img/button.svg)](https://www.cloudron.io/store/com.rssbridgeapp.cloudronapp.html)
Getting involved
===
There are many ways for you to getting involved with RSS-Bridge. Here are a few things:
- Share RSS-Bridge with your friends (Twitter, Facebook, ..._you name it_...)
- Report broken bridges or bugs by opening [Issues](https://github.com/RSS-Bridge/rss-bridge/issues) on GitHub
- Request new features or suggest ideas (via [Issues](https://github.com/RSS-Bridge/rss-bridge/issues))
- Discuss bugs, features, ideas or [issues](https://github.com/RSS-Bridge/rss-bridge/issues)
- Add new bridges or improve the API
- Improve the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki)
- Host an instance of RSS-Bridge for your personal use or make it available to the community :sparkling_heart:
Authors
===
We are RSS-Bridge community, a group of developers continuing the project initiated by sebsauvage, webmaster of [sebsauvage.net](http://sebsauvage.net), author of [Shaarli](http://sebsauvage.net/wiki/doku.php?id=php:shaarli) and [ZeroBin](http://sebsauvage.net/wiki/doku.php?id=php:zerobin).
**Contributors** (sorted alphabetically):
<!--
Use this script to generate the list automatically (using the GitHub API):
./contrib/prepare_release/fetch_contributors.php
-->
* [16mhz](https://github.com/16mhz)
* [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)
* [b1nj](https://github.com/b1nj)
* [benasse](https://github.com/benasse)
* [Binnette](https://github.com/Binnette)
* [Bockiii](https://github.com/Bockiii)
* [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)
* [da2x](https://github.com/da2x)
* [dabenzel](https://github.com/dabenzel)
* [Daiyousei](https://github.com/Daiyousei)
* [dawidsowa](https://github.com/dawidsowa)
* [DevonHess](https://github.com/DevonHess)
* [dhuschde](https://github.com/dhuschde)
* [disk0x](https://github.com/disk0x)
* [DJCrashdummy](https://github.com/DJCrashdummy)
* [Djuuu](https://github.com/Djuuu)
* [DnAp](https://github.com/DnAp)
* [dominik-th](https://github.com/dominik-th)
* [Draeli](https://github.com/Draeli)
* [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
* [drego85](https://github.com/drego85)
* [drklee3](https://github.com/drklee3)
* [dvikan](https://github.com/dvikan)
* [em92](https://github.com/em92)
* [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
* [f0086](https://github.com/f0086)
* [fanch317](https://github.com/fanch317)
* [fatuuse](https://github.com/fatuuse)
* [fivefilters](https://github.com/fivefilters)
* [floviolleau](https://github.com/floviolleau)
* [fluffy-critter](https://github.com/fluffy-critter)
* [fmachen](https://github.com/fmachen)
* [Frenzie](https://github.com/Frenzie)
* [fulmeek](https://github.com/fulmeek)
* [ggiessen](https://github.com/ggiessen)
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
* [girlpunk](https://github.com/girlpunk)
* [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)
* [imagoiq](https://github.com/imagoiq)
* [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)
* [jNullj](https://github.com/jNullj)
* [Jocker666z](https://github.com/Jocker666z)
* [johnnygroovy](https://github.com/johnnygroovy)
* [johnpc](https://github.com/johnpc)
* [joni1993](https://github.com/joni1993)
* [KamaleiZestri](https://github.com/KamaleiZestri)
* [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)
* [laBecasse](https://github.com/laBecasse)
* [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev)
* [ldidry](https://github.com/ldidry)
* [Leomaradan](https://github.com/Leomaradan)
* [leyrer](https://github.com/leyrer)
* [liamka](https://github.com/liamka)
* [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
* [lorenzos](https://github.com/lorenzos)
* [lukasklinger](https://github.com/lukasklinger)
* [m0zes](https://github.com/m0zes)
* [Mar-Koeh](https://github.com/Mar-Koeh)
* [marcus-at-localhost](https://github.com/marcus-at-localhost)
* [marius8510000-bot](https://github.com/marius8510000-bot)
* [matthewseal](https://github.com/matthewseal)
* [mcbyte-it](https://github.com/mcbyte-it)
* [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)
* [Mynacol](https://github.com/Mynacol)
* [nel50n](https://github.com/nel50n)
* [niawag](https://github.com/niawag)
* [Niehztog](https://github.com/Niehztog)
* [Nono-m0le](https://github.com/Nono-m0le)
* [obsiwitch](https://github.com/obsiwitch)
* [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)
* [sal0max](https://github.com/sal0max)
* [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)
* [SpangleLabs](https://github.com/SpangleLabs)
* [squeek502](https://github.com/squeek502)
* [stjohnjohnson](https://github.com/stjohnjohnson)
* [Stopka](https://github.com/Stopka)
* [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)
* [timendum](https://github.com/timendum)
* [TitiTestScalingo](https://github.com/TitiTestScalingo)
* [tomaszkane](https://github.com/tomaszkane)
* [TReKiE](https://github.com/TReKiE)
* [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)
* [yue-dongchen](https://github.com/yue-dongchen)
* [ZeNairolf](https://github.com/ZeNairolf)
Licenses
===
### Licenses
The source code for RSS-Bridge is [Public Domain](UNLICENSE).
RSS-Bridge uses third party libraries with their own license:
* [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`PHP Simple HTML DOM Parser`](http://simplehtmldom.sourceforge.net/) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](https://opensource.org/licenses/MIT)
* [`PHP Simple HTML DOM Parser`](https://simplehtmldom.sourceforge.io/docs/1.9/index.html) licensed under the [MIT License](https://opensource.org/licenses/MIT)
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](https://opensource.org/licenses/MIT)
* [`Laravel framework`](https://github.com/laravel/framework/) licensed under the [MIT License](https://opensource.org/licenses/MIT)
Technical notes
===
* RSS-Bridge uses caching to prevent services from banning your server for repeatedly updating feeds. The specific cache duration can be different between bridges. Cached files are deleted automatically after 24 hours.
* You can implement your own bridge, [following these instructions](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API).
* You can enable debug mode to disable caching. Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Debug-mode)
Rant
===
## Rant
*Dear so-called "social" websites.*

View File

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

View File

@@ -1,4 +1,5 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
@@ -6,48 +7,47 @@
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class DetectAction extends ActionAbstract {
public function execute() {
$targetURL = $this->userData['url']
or returnClientError('You must specify a url!');
class DetectAction implements ActionInterface
{
public function execute(array $request)
{
$targetURL = $request['url'] ?? null;
$format = $request['format'] ?? null;
$format = $this->userData['format']
or returnClientError('You must specify a format!');
if (!$targetURL) {
throw new \Exception('You must specify a url!');
}
if (!$format) {
throw new \Exception('You must specify a format!');
}
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$bridgeFactory = new BridgeFactory();
foreach($bridgeFac->getBridgeNames() as $bridgeName) {
foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
if (!$bridgeFactory->isEnabled($bridgeClassName)) {
continue;
}
if(!$bridgeFac->isWhitelisted($bridgeName)) {
continue;
}
$bridge = $bridgeFactory->create($bridgeClassName);
$bridge = $bridgeFac->create($bridgeName);
$bridgeParams = $bridge->detectParameters($targetURL);
if($bridge === false) {
continue;
}
if (is_null($bridgeParams)) {
continue;
}
$bridgeParams = $bridge->detectParameters($targetURL);
$bridgeParams['bridge'] = $bridgeClassName;
$bridgeParams['format'] = $format;
if(is_null($bridgeParams)) {
continue;
}
$url = '?action=display&' . http_build_query($bridgeParams);
return new Response('', 301, ['Location' => $url]);
}
$bridgeParams['bridge'] = $bridgeName;
$bridgeParams['format'] = $format;
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
die();
}
returnClientError('No bridge found for given URL: ' . $targetURL);
}
throw new \Exception('No bridge found for given URL: ' . $targetURL);
}
}

View File

@@ -1,4 +1,5 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
@@ -6,255 +7,254 @@
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class DisplayAction extends ActionAbstract {
private function get_return_code($error) {
$returnCode = $error->getCode();
if ($returnCode === 301 || $returnCode === 302) {
# Don't pass redirect codes to the exterior
$returnCode = 508;
}
return $returnCode;
}
class DisplayAction implements ActionInterface
{
public function execute(array $request)
{
if (Configuration::getConfig('system', 'enable_maintenance_mode')) {
return new Response('503 Service Unavailable', 503);
}
public function execute() {
$bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
$bridgeFactory = new BridgeFactory();
$format = $this->userData['format']
or returnClientError('You must specify a format!');
$bridgeClassName = $bridgeFactory->createBridgeClassName($request['bridge'] ?? '');
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$format = $request['format'] ?? null;
if (!$format) {
throw new \Exception('You must specify a format!');
}
if (!$bridgeFactory->isEnabled($bridgeClassName)) {
throw new \Exception('This bridge is not whitelisted');
}
// whitelist control
if(!$bridgeFac->isWhitelisted($bridge)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
$formatFactory = new FormatFactory();
$format = $formatFactory->create($format);
// Data retrieval
$bridge = $bridgeFac->create($bridge);
$bridge->loadConfiguration();
$bridge = $bridgeFactory->create($bridgeClassName);
$bridge->loadConfiguration();
$noproxy = array_key_exists('_noproxy', $this->userData)
&& filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
$noproxy = $request['_noproxy'] ?? null;
if (
Configuration::getConfig('proxy', 'url')
&& Configuration::getConfig('proxy', 'by_bridge')
&& $noproxy
) {
// This const is only used once in getContents()
define('NOPROXY', true);
}
if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
define('NOPROXY', true);
}
$cacheTimeout = $request['_cache_timeout'] ?? null;
if (Configuration::getConfig('cache', 'custom_timeout') && $cacheTimeout) {
$cacheTimeout = (int) $cacheTimeout;
} else {
// At this point the query argument might still be in the url but it won't be used
$cacheTimeout = $bridge->getCacheTimeout();
}
// Cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $this->userData)) {
// Remove parameters that don't concern bridges
$bridge_params = array_diff_key(
$request,
array_fill_keys(
[
'action',
'bridge',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
],
''
)
);
if(!CUSTOM_CACHE_TIMEOUT) {
unset($this->userData['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($this->userData);
header('Location: ' . $uri, true, 301);
die();
}
// Remove parameters that don't concern caches
$cache_params = array_diff_key(
$request,
array_fill_keys(
[
'action',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
],
''
)
);
$cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT);
$cache = RssBridge::getCache();
$cache->setScope('');
$cache->setKey($cache_params);
// This cache purge will basically delete all cache items older than 24h, regardless of scope and key
$cache->purgeCache(86400);
} else {
$cache_timeout = $bridge->getCacheTimeout();
}
$items = [];
$infos = [];
$mtime = $cache->getTime();
// Remove parameters that don't concern bridges
$bridge_params = array_diff_key(
$this->userData,
array_fill_keys(
array(
'action',
'bridge',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
if (
$mtime
&& (time() - $cacheTimeout < $mtime)
&& !Debug::isEnabled()
) {
// At this point we found the feed in the cache and debug mode is disabled
// Remove parameters that don't concern caches
$cache_params = array_diff_key(
$this->userData,
array_fill_keys(
array(
'action',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
// The client wants to know if the feed has changed since its last check
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if ($mtime <= $stime) {
$lastModified2 = gmdate('D, d M Y H:i:s ', $mtime) . 'GMT';
return new Response('', 304, ['Last-Modified' => $lastModified2]);
}
}
// Initialize cache
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope('');
$cache->purgeCache(86400); // 24 hours
$cache->setKey($cache_params);
// Load the feed from cache and prepare it
$cached = $cache->loadData();
if (isset($cached['items']) && isset($cached['extraInfos'])) {
foreach ($cached['items'] as $item) {
$items[] = new FeedItem($item);
}
$infos = $cached['extraInfos'];
}
} else {
// At this point we did NOT find the feed in the cache or debug mode is enabled.
try {
$bridge->setDatas($bridge_params);
$bridge->collectData();
$items = array();
$infos = array();
$mtime = $cache->getTime();
$items = $bridge->getItems();
if($mtime !== false
&& (time() - $cache_timeout < $mtime)
&& !Debug::isEnabled()) { // Load cached data
if (isset($items[0]) && is_array($items[0])) {
$feedItems = [];
foreach ($items as $item) {
$feedItems[] = new FeedItem($item);
}
$items = $feedItems;
}
$infos = [
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'icon' => $bridge->getIcon()
];
} catch (\Throwable $e) {
if ($e instanceof HttpException) {
// Produce a smaller log record for http exceptions
Logger::warning(sprintf('Exception in %s: %s', $bridgeClassName, create_sane_exception_message($e)));
} else {
// Log the exception
Logger::error(sprintf('Exception in %s', $bridgeClassName), ['e' => $e]);
}
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
// Emit error only if we are passed the error report limit
$errorCount = self::logBridgeError($bridge->getName(), $e->getCode());
if ($errorCount >= Configuration::getConfig('error', 'report_limit')) {
if (Configuration::getConfig('error', 'output') === 'feed') {
// Emit the error as a feed item in a feed so that feed readers can pick it up
$items[] = $this->createFeedItemFromException($e, $bridge);
} elseif (Configuration::getConfig('error', 'output') === 'http') {
// Emit as a regular web response
throw $e;
}
}
}
if($mtime <= $stime) { // Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
die();
}
}
// Unfortunately need to set scope and key again because they might be modified
$cache->setScope('');
$cache->setKey($cache_params);
$cache->saveData([
'items' => array_map(function (FeedItem $item) {
return $item->toArray();
}, $items),
'extraInfos' => $infos
]);
}
$cached = $cache->loadData();
$format->setItems($items);
$format->setExtraInfos($infos);
$lastModified = $cache->getTime();
$format->setLastModified($lastModified);
$headers = [];
if ($lastModified) {
$headers['Last-Modified'] = gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT';
}
$headers['Content-Type'] = $format->getMimeType() . '; charset=' . $format->getCharset();
return new Response($format->stringify(), 200, $headers);
}
if(isset($cached['items']) && isset($cached['extraInfos'])) {
foreach($cached['items'] as $item) {
$items[] = new \FeedItem($item);
}
private function createFeedItemFromException($e, BridgeInterface $bridge): FeedItem
{
$item = new FeedItem();
$infos = $cached['extraInfos'];
}
// Create a unique identifier every 24 hours
$uniqueIdentifier = urlencode((int)(time() / 86400));
$itemTitle = sprintf('Bridge returned error %s! (%s)', $e->getCode(), $uniqueIdentifier);
$item->setTitle($itemTitle);
$item->setURI(get_current_url());
$item->setTimestamp(time());
} else { // Collect new data
// Create an item identifier for feed readers e.g. "staysafetv twitch videos_19389"
$item->setUid($bridge->getName() . '_' . $uniqueIdentifier);
try {
$bridge->setDatas($bridge_params);
$bridge->collectData();
$content = render_template(__DIR__ . '/../templates/bridge-error.html.php', [
'error' => render_template(__DIR__ . '/../templates/error.html.php', ['e' => $e]),
'searchUrl' => self::createGithubSearchUrl($bridge),
'issueUrl' => self::createGithubIssueUrl($bridge, $e, create_sane_exception_message($e)),
'maintainer' => $bridge->getMaintainer(),
]);
$item->setContent($content);
return $item;
}
$items = $bridge->getItems();
private static function logBridgeError($bridgeName, $code)
{
$cache = RssBridge::getCache();
$cache->setScope('error_reporting');
$cache->setkey([$bridgeName . '_' . $code]);
// Transform "legacy" items to FeedItems if necessary.
// Remove this code when support for "legacy" items ends!
if(isset($items[0]) && is_array($items[0])) {
$feedItems = array();
if ($report = $cache->loadData()) {
$report = Json::decode($report);
$report['time'] = time();
$report['count']++;
} else {
$report = [
'error' => $code,
'time' => time(),
'count' => 1,
];
}
$cache->saveData(Json::encode($report));
return $report['count'];
}
foreach($items as $item) {
$feedItems[] = new \FeedItem($item);
}
private static function createGithubIssueUrl($bridge, $e, string $message): string
{
return sprintf('https://github.com/RSS-Bridge/rss-bridge/issues/new?%s', http_build_query([
'title' => sprintf('%s failed with error %s', $bridge->getName(), $e->getCode()),
'body' => sprintf(
"```\n%s\n\n%s\n\nQuery string: %s\nVersion: %s\nOs: %s\nPHP version: %s\n```",
$message,
implode("\n", trace_to_call_points(trace_from_exception($e))),
$_SERVER['QUERY_STRING'] ?? '',
Configuration::getVersion(),
PHP_OS_FAMILY,
phpversion() ?: 'Unknown'
),
'labels' => 'Bridge-Broken',
'assignee' => $bridge->getMaintainer(),
]));
}
$items = $feedItems;
}
$infos = array(
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'icon' => $bridge->getIcon()
);
} catch(Error $e) {
error_log($e);
if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
if(Configuration::getConfig('error', 'output') === 'feed') {
$item = new \FeedItem();
// Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
// Error 0 is a special case (i.e. "trying to get property of non-object")
if($e->getCode() === 0) {
$item->setTitle(
'Bridge encountered an unexpected situation! ('
. $this->userData['_error_time']
. ')'
);
} else {
$item->setTitle(
'Bridge returned error '
. $e->getCode()
. '! ('
. $this->userData['_error_time']
. ')'
);
}
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($this->userData)
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} elseif(Configuration::getConfig('error', 'output') === 'http') {
header('Content-Type: text/html', true, $this->get_return_code($e));
die(buildTransformException($e, $bridge));
}
}
} catch(Exception $e) {
error_log($e);
if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
if(Configuration::getConfig('error', 'output') === 'feed') {
$item = new \FeedItem();
// Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($this->userData)
);
$item->setTitle(
'Bridge returned error '
. $e->getCode()
. '! ('
. $this->userData['_error_time']
. ')'
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} elseif(Configuration::getConfig('error', 'output') === 'http') {
header('Content-Type: text/html', true, $this->get_return_code($e));
die(buildTransformException($e, $bridge));
}
}
}
// Store data in cache
$cache->saveData(array(
'items' => array_map(function($i){ return $i->toArray(); }, $items),
'extraInfos' => $infos
));
}
// Data transformation
try {
$formatFac = new FormatFactory();
$formatFac->setWorkingDir(PATH_LIB_FORMATS);
$format = $formatFac->create($format);
$format->setItems($items);
$format->setExtraInfos($infos);
$format->setLastModified($cache->getTime());
$format->display();
} catch(Error $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
}
}
private static function createGithubSearchUrl($bridge): string
{
return sprintf(
'https://github.com/RSS-Bridge/rss-bridge/issues?q=%s',
urlencode('is:issue is:open ' . $bridge->getName())
);
}
}

View File

@@ -0,0 +1,36 @@
<?php
final class FrontpageAction implements ActionInterface
{
public function execute(array $request)
{
$showInactive = (bool) ($request['show_inactive'] ?? null);
$activeBridges = 0;
$bridgeFactory = new BridgeFactory();
$bridgeClassNames = $bridgeFactory->getBridgeClassNames();
$formatFactory = new FormatFactory();
$formats = $formatFactory->getFormatNames();
$body = '';
foreach ($bridgeClassNames as $bridgeClassName) {
if ($bridgeFactory->isEnabled($bridgeClassName)) {
$body .= BridgeCard::displayBridgeCard($bridgeClassName, $formats);
$activeBridges++;
} elseif ($showInactive) {
$body .= BridgeCard::displayBridgeCard($bridgeClassName, $formats, false) . PHP_EOL;
}
}
return render(__DIR__ . '/../templates/frontpage.html.php', [
'messages' => [],
'admin_email' => Configuration::getConfig('admin', 'email'),
'admin_telegram' => Configuration::getConfig('admin', 'telegram'),
'bridges' => $body,
'active_bridges' => $activeBridges,
'total_bridges' => count($bridgeClassNames),
'show_inactive' => $showInactive,
]);
}
}

15
actions/HealthAction.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
class HealthAction implements ActionInterface
{
public function execute(array $request)
{
$response = [
'code' => 200,
'message' => 'all is good',
];
return new Response(Json::encode($response), 200, ['content-type' => 'application/json']);
}
}

View File

@@ -1,4 +1,5 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
@@ -6,52 +7,36 @@
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class ListAction extends ActionAbstract {
public function execute() {
$list = new StdClass();
$list->bridges = array();
$list->total = 0;
class ListAction implements ActionInterface
{
public function execute(array $request)
{
$list = new \stdClass();
$list->bridges = [];
$list->total = 0;
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$bridgeFactory = new BridgeFactory();
foreach($bridgeFac->getBridgeNames() as $bridgeName) {
foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
$bridge = $bridgeFactory->create($bridgeClassName);
$bridge = $bridgeFac->create($bridgeName);
if($bridge === false) { // Broken bridge, show as inactive
$list->bridges[$bridgeName] = array(
'status' => 'inactive'
);
continue;
}
$status = $bridgeFac->isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'name' => $bridge->getName(),
'icon' => $bridge->getIcon(),
'parameters' => $bridge->getParameters(),
'maintainer' => $bridge->getMaintainer(),
'description' => $bridge->getDescription()
);
}
$list->total = count($list->bridges);
header('Content-Type: application/json');
echo json_encode($list, JSON_PRETTY_PRINT);
}
$list->bridges[$bridgeClassName] = [
'status' => $bridgeFactory->isEnabled($bridgeClassName) ? 'active' : 'inactive',
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'name' => $bridge->getName(),
'icon' => $bridge->getIcon(),
'parameters' => $bridge->getParameters(),
'maintainer' => $bridge->getMaintainer(),
'description' => $bridge->getDescription()
];
}
$list->total = count($list->bridges);
return new Response(Json::encode($list), 200, ['Content-Type' => 'application/json']);
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class SetBridgeCacheAction implements ActionInterface
{
public function execute(array $request)
{
$authenticationMiddleware = new ApiAuthenticationMiddleware();
$authenticationMiddleware($request);
$key = $request['key'] or returnClientError('You must specify key!');
$bridgeFactory = new BridgeFactory();
$bridgeClassName = $bridgeFactory->createBridgeClassName($request['bridge'] ?? '');
// whitelist control
if (!$bridgeFactory->isEnabled($bridgeClassName)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
$bridge = $bridgeFactory->create($bridgeClassName);
$bridge->loadConfiguration();
$value = $request['value'];
$cache = RssBridge::getCache();
$cache->setScope(get_class($bridge));
if (!is_array($key)) {
// not sure if $key is an array when it comes in from request
$key = [$key];
}
$cache->setKey($key);
$cache->saveData($value);
header('Content-Type: text/plain');
echo 'done';
}
}

View File

@@ -1,8 +1,8 @@
{
"service": "Heroku",
"name": "RSS-Bridge",
"name": "rss-bridge-heroku",
"description": "RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one.",
"repository": "https://github.com/RSS-Bridge/rss-bridge",
"repository": "https://github.com/RSS-Bridge/rss-bridge?1651005770",
"keywords": ["php", "rss-bridge", "rss"]
}

View File

@@ -1,45 +1,49 @@
<?php
class ABCNewsBridge extends BridgeAbstract {
const NAME = 'ABC News Bridge';
const URI = 'https://www.abc.net.au';
const DESCRIPTION = 'Topics of the Australian Broadcasting Corporation';
const MAINTAINER = 'yue-dongchen';
const PARAMETERS = array(
array(
'topic' => array(
'type' => 'list',
'name' => 'Region',
'title' => 'Choose state',
'values' => array(
'ACT' => 'act',
'NSW' => 'nsw',
'NT' => 'nt',
'QLD' => 'qld',
'SA' => 'sa',
'TAS' => 'tas',
'VIC' => 'vic',
'WA' => 'wa'
),
)
)
);
class ABCNewsBridge extends BridgeAbstract
{
const NAME = 'ABC News Bridge';
const URI = 'https://www.abc.net.au';
const DESCRIPTION = 'Topics of the Australian Broadcasting Corporation';
const MAINTAINER = 'yue-dongchen';
public function collectData() {
$url = 'https://www.abc.net.au/news/' . $this->getInput('topic');
$html = getSimpleHTMLDOM($url)->find('.YAJzu._2FvRw.ZWhbj._3BZxh', 0);
$html = defaultLinkTo($html, $this->getURI());
const PARAMETERS = [
[
'topic' => [
'type' => 'list',
'name' => 'Region',
'title' => 'Choose state',
'values' => [
'ACT' => 'act',
'NSW' => 'nsw',
'NT' => 'nt',
'QLD' => 'qld',
'SA' => 'sa',
'TAS' => 'tas',
'VIC' => 'vic',
'WA' => 'wa'
],
]
]
];
foreach($html->find('._2H7Su') as $article) {
$item = array();
$title = $article->find('._3T9Id.fmhNa.nsZdE._2c2Zy._1tOey._3EOTW', 0);
$item['title'] = $title->plaintext;
$item['uri'] = $title->href;
$item['content'] = $article->find('.rMkro._1cBaI._3PhF6._10YQT._1yL-m', 0)->plaintext;
$item['timestamp'] = strtotime($article->find('time', 0)->datetime);
$this->items[] = $item;
}
}
public function collectData()
{
$url = sprintf('https://www.abc.net.au/news/%s', $this->getInput('topic'));
$dom = getSimpleHTMLDOM($url);
$dom = $dom->find('div[data-component="CardList"]', 0);
if (!$dom) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
}
$dom = defaultLinkTo($dom, $this->getURI());
foreach ($dom->find('div[data-component="GenericCard"]') as $article) {
$a = $article->find('a', 0);
$this->items[] = [
'title' => $a->plaintext,
'uri' => $a->href,
'content' => $article->find('[data-component="CardDescription"]', 0)->plaintext,
'timestamp' => strtotime($article->find('time', 0)->datetime),
];
}
}
}

116
bridges/ABolaBridge.php Normal file
View File

@@ -0,0 +1,116 @@
<?php
class ABolaBridge extends BridgeAbstract
{
const NAME = 'A Bola';
const URI = 'https://abola.pt/';
const DESCRIPTION = 'Returns news from the Portuguese sports newspaper A BOLA.PT';
const MAINTAINER = 'rmscoelho';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = [
[
'feed' => [
'name' => 'News Feed',
'type' => 'list',
'title' => 'Feeds from the Portuguese sports newspaper A BOLA.PT',
'values' => [
'Últimas' => 'Nnh/Noticias',
'Seleção Nacional' => 'Selecao/Noticias',
'Futebol Nacional' => [
'Notícias' => 'Nacional/Noticias',
'Primeira Liga' => 'Nacional/Liga/Noticias',
'Liga 2' => 'Nacional/Liga2/Noticias',
'Liga 3' => 'Nacional/Liga3/Noticias',
'Liga Revelação' => 'Nacional/Liga-Revelacao/Noticias',
'Campeonato de Portugal' => 'Nacional/Campeonato-Portugal/Noticias',
'Distritais' => 'Nacional/Distritais/Noticias',
'Taça de Portugal' => 'Nacional/TPortugal/Noticias',
'Futebol Feminino' => 'Nacional/FFeminino/Noticias',
'Futsal' => 'Nacional/Futsal/Noticias',
],
'Futebol Internacional' => [
'Notícias' => 'Internacional/Noticias/Noticias',
'Liga dos Campeões' => 'Internacional/Liga-dos-campeoes/Noticias',
'Liga Europa' => 'Internacional/Liga-europa/Noticias',
'Liga Conferência' => 'Internacional/Liga-conferencia/Noticias',
'Liga das Nações' => 'Internacional/Liga-das-nacoes/Noticias',
'UEFA Youth League' => 'Internacional/Uefa-Youth-League/Noticias',
],
'Mercado' => 'Mercado',
'Modalidades' => 'Modalidades/Noticias',
'Motores' => 'Motores/Noticias',
]
]
]
];
public function getIcon()
{
return 'https://abola.pt/img/icons/favicon-96x96.png';
}
public function getName()
{
return !is_null($this->getKey('feed')) ? self::NAME . ' | ' . $this->getKey('feed') : self::NAME;
}
public function getURI()
{
return self::URI . $this->getInput('feed');
}
public function collectData()
{
$url = sprintf('https://abola.pt/%s', $this->getInput('feed'));
$dom = getSimpleHTMLDOM($url);
if ($this->getInput('feed') !== 'Mercado') {
$dom = $dom->find('div#body_Todas1_upNoticiasTodas', 0);
} else {
$dom = $dom->find('div#body_NoticiasMercado_upNoticiasTodas', 0);
}
if (!$dom) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
}
$dom = defaultLinkTo($dom, $this->getURI());
foreach ($dom->find('div.media') as $key => $article) {
//Get thumbnail
$image = $article->find('.media-img', 0)->style;
$image = preg_replace('/background-image: url\(/i', '', $image);
$image = substr_replace($image, '', -4);
$image = preg_replace('/https:\/\//i', '', $image);
$image = preg_replace('/www\./i', '', $image);
$image = preg_replace('/\/\//', '/', $image);
$image = preg_replace('/\/\/\//', '//', $image);
$image = substr($image, 7);
$image = 'https://' . $image;
$image = preg_replace('/ptimg/', 'pt/img', $image);
$image = preg_replace('/\/\/bola/', 'www.abola', $image);
//Timestamp
$date = date('Y/m/d');
if (!is_null($article->find("span#body_Todas1_rptNoticiasTodas_lblData_$key", 0))) {
$date = $article->find("span#body_Todas1_rptNoticiasTodas_lblData_$key", 0)->plaintext;
$date = preg_replace('/\./', '/', $date);
}
$time = $article->find("span#body_Todas1_rptNoticiasTodas_lblHora_$key", 0)->plaintext;
$date = explode('/', $date);
$time = explode(':', $time);
$year = $date[0];
$month = $date[1];
$day = $date[2];
$hour = $time[0];
$minute = $time[1];
$timestamp = mktime($hour, $minute, 0, $month, $day, $year);
//Content
$image = '<img src="' . $image . '" alt="' . $article->find('h4 span', 0)->plaintext . '" />';
$description = '<p>' . $article->find('.media-texto > span', 0)->plaintext . '</p>';
$content = $image . '</br>' . $description;
$a = $article->find('.media-body > a', 0);
$this->items[] = [
'title' => $a->find('h4 span', 0)->plaintext,
'uri' => $a->href,
'content' => $content,
'timestamp' => $timestamp,
];
}
}
}

View File

@@ -1,118 +1,134 @@
<?php
class AO3Bridge extends BridgeAbstract {
const NAME = 'AO3';
const URI = 'https://archiveofourown.org/';
const CACHE_TIMEOUT = 1800;
const DESCRIPTION = 'Returns works or chapters from Archive of Our Own';
const MAINTAINER = 'Obsidienne';
const PARAMETERS = array(
'List' => array(
'url' => array(
'name' => 'url',
'required' => true,
// Example: F/F tag, complete works only
'exampleValue' => 'https://archiveofourown.org/works?work_search[complete]=T&tag_id=F*s*F',
),
),
'Bookmarks' => array(
'user' => array(
'name' => 'user',
'required' => true,
// Example: Nyaaru's bookmarks
'exampleValue' => 'Nyaaru',
),
),
'Work' => array(
'id' => array(
'name' => 'id',
'required' => true,
// Example: latest chapters from A Better Past by LysSerris
'exampleValue' => '18181853',
),
)
);
class AO3Bridge extends BridgeAbstract
{
const NAME = 'AO3';
const URI = 'https://archiveofourown.org/';
const CACHE_TIMEOUT = 1800;
const DESCRIPTION = 'Returns works or chapters from Archive of Our Own';
const MAINTAINER = 'Obsidienne';
const PARAMETERS = [
'List' => [
'url' => [
'name' => 'url',
'required' => true,
// Example: F/F tag, complete works only
'exampleValue' => 'https://archiveofourown.org/works?work_search[complete]=T&tag_id=F*s*F',
],
],
'Bookmarks' => [
'user' => [
'name' => 'user',
'required' => true,
// Example: Nyaaru's bookmarks
'exampleValue' => 'Nyaaru',
],
],
'Work' => [
'id' => [
'name' => 'id',
'required' => true,
// Example: latest chapters from A Better Past by LysSerris
'exampleValue' => '18181853',
],
]
];
// Feed for lists of works (e.g. recent works, search results, filtered tags,
// bookmarks, series, collections).
private function collectList($url) {
$html = getSimpleHTMLDOM($url);
$html = defaultLinkTo($html, self::URI);
public function collectData()
{
switch ($this->queriedContext) {
case 'Bookmarks':
$user = $this->getInput('user');
$this->title = $user;
$url = self::URI
. '/users/' . $user
. '/bookmarks?bookmark_search[sort_column]=bookmarkable_date';
$this->collectList($url);
break;
case 'List':
$this->collectList($this->getInput('url'));
break;
case 'Work':
$this->collectWork($this->getInput('id'));
break;
}
}
foreach($html->find('.index.group > li') as $element) {
$item = array();
/**
* Feed for lists of works (e.g. recent works, search results, filtered tags,
* bookmarks, series, collections).
*/
private function collectList($url)
{
$html = getSimpleHTMLDOM($url);
$html = defaultLinkTo($html, self::URI);
$title = $element->find('div h4 a', 0);
if (!isset($title)) continue; // discard deleted works
$item['title'] = $title->plaintext;
$item['content'] = $element;
$item['uri'] = $title->href;
foreach ($html->find('.index.group > li') as $element) {
$item = [];
$strdate = $element->find('div p.datetime', 0)->plaintext;
$item['timestamp'] = strtotime($strdate);
$title = $element->find('div h4 a', 0);
if (!isset($title)) {
continue; // discard deleted works
}
$item['title'] = $title->plaintext;
$item['content'] = $element;
$item['uri'] = $title->href;
$chapters = $element->find('dl dd.chapters', 0);
// bookmarked series and external works do not have a chapters count
$chapters = (isset($chapters) ? $chapters->plaintext : 0);
$item['uid'] = $item['uri'] . "/$strdate/$chapters";
$strdate = $element->find('div p.datetime', 0)->plaintext;
$item['timestamp'] = strtotime($strdate);
$this->items[] = $item;
}
}
$chapters = $element->find('dl dd.chapters', 0);
// bookmarked series and external works do not have a chapters count
$chapters = (isset($chapters) ? $chapters->plaintext : 0);
$item['uid'] = $item['uri'] . "/$strdate/$chapters";
// Feed for recent chapters of a specific work.
private function collectWork($id) {
$url = self::URI . "/works/$id/navigate";
$html = getSimpleHTMLDOM($url);
$html = defaultLinkTo($html, self::URI);
$this->items[] = $item;
}
}
$this->title = $html->find('h2 a', 0)->plaintext;
/**
* Feed for recent chapters of a specific work.
*/
private function collectWork($id)
{
$url = self::URI . "/works/$id/navigate";
$response = _http_request($url, ['useragent' => 'rss-bridge bot (https://github.com/RSS-Bridge/rss-bridge)']);
$html = \str_get_html($response['body']);
$html = defaultLinkTo($html, self::URI);
foreach($html->find('ol.index.group > li') as $element) {
$item = array();
$this->title = $html->find('h2 a', 0)->plaintext;
$item['title'] = $element->find('a', 0)->plaintext;
$item['content'] = $element;
$item['uri'] = $element->find('a', 0)->href;
foreach ($html->find('ol.index.group > li') as $element) {
$item = [];
$strdate = $element->find('span.datetime', 0)->plaintext;
$strdate = str_replace('(', '', $strdate);
$strdate = str_replace(')', '', $strdate);
$item['timestamp'] = strtotime($strdate);
$item['title'] = $element->find('a', 0)->plaintext;
$item['content'] = $element;
$item['uri'] = $element->find('a', 0)->href;
$item['uid'] = $item['uri'] . "/$strdate";
$strdate = $element->find('span.datetime', 0)->plaintext;
$strdate = str_replace('(', '', $strdate);
$strdate = str_replace(')', '', $strdate);
$item['timestamp'] = strtotime($strdate);
$this->items[] = $item;
}
$item['uid'] = $item['uri'] . "/$strdate";
$this->items = array_reverse($this->items);
}
$this->items[] = $item;
}
public function collectData() {
switch($this->queriedContext) {
case 'Bookmarks':
$user = $this->getInput('user');
$this->title = $user;
$url = self::URI
. '/users/' . $user
. '/bookmarks?bookmark_search[sort_column]=bookmarkable_date';
return $this->collectList($url);
case 'List': return $this->collectList(
$this->getInput('url')
);
case 'Work': return $this->collectWork(
$this->getInput('id')
);
}
}
$this->items = array_reverse($this->items);
}
public function getName() {
$name = parent::getName() . " $this->queriedContext";
if (isset($this->title)) $name .= " - $this->title";
return $name;
}
public function getName()
{
$name = parent::getName() . " $this->queriedContext";
if (isset($this->title)) {
$name .= " - $this->title";
}
return $name;
}
public function getIcon() {
return self::URI . '/favicon.ico';
}
public function getIcon()
{
return self::URI . '/favicon.ico';
}
}

View File

@@ -0,0 +1,158 @@
<?php
class ARDAudiothekBridge extends BridgeAbstract
{
const NAME = 'ARD-Audiothek Bridge';
const URI = 'https://www.ardaudiothek.de';
const DESCRIPTION = 'Feed of any show in the ARD-Audiothek, specified by its path';
const MAINTAINER = 'Mar-Koeh';
/*
* The URL Prefix of the API
* @const APIENDPOINT https-URL of the used endpoint, ending in `/`
*/
const APIENDPOINT = 'https://api.ardaudiothek.de/';
/*
* The requested width of the preview image
* 448 and 128 have been observed on the wild
* @const IMAGEWIDTH width in px of the preview image
*/
const IMAGEWIDTH = 448;
/*
* Placeholder that will be replace by IMAGEWIDTH in the preview image URL
* @const IMAGEWIDTHPLACEHOLDER
*/
const IMAGEWIDTHPLACEHOLDER = '{width}';
/*
* File extension appended to image link in $this->icon
* @const IMAGEEXTENSION
*/
const IMAGEEXTENSION = '.jpg';
const PARAMETERS = [
[
'path' => [
'name' => 'Show Link or ID',
'required' => true,
'title' => 'Link to the show page or just its numeric suffix',
'defaultValue' => 'https://www.ardaudiothek.de/sendung/kalk-welk/10777871/'
],
'limit' => self::LIMIT,
]
];
/**
* Holds the title of the current show
*
* @var string
*/
private $title;
/**
* Holds the URI of the show
*
* @var string
*/
private $uri;
/**
* Holds the icon of the feed
*
*/
private $icon;
public function collectData()
{
$oldTz = date_default_timezone_get();
date_default_timezone_set('Europe/Berlin');
$pathComponents = explode('/', $this->getInput('path'));
if (empty($pathComponents)) {
returnClientError('Path may not be empty');
}
if (count($pathComponents) < 2) {
$showID = $pathComponents[0];
} else {
$lastKey = count($pathComponents) - 1;
$showID = $pathComponents[$lastKey];
if (strlen($showID) === 0) {
$showID = $pathComponents[$lastKey - 1];
}
}
$url = self::APIENDPOINT . 'programsets/' . $showID . '/';
$rawJSON = getContents($url);
$processedJSON = json_decode($rawJSON)->data->programSet;
$limit = $this->getInput('limit');
$answerLength = 1;
$offset = 0;
$numberOfElements = 1;
while ($answerLength != 0 && $offset < $numberOfElements && (is_null($limit) || $offset < $limit)) {
$rawJSON = getContents($url . '?offset=' . $offset);
$processedJSON = json_decode($rawJSON)->data->programSet;
$answerLength = count($processedJSON->items->nodes);
$offset = $offset + $answerLength;
$numberOfElements = $processedJSON->numberOfElements;
foreach ($processedJSON->items->nodes as $audio) {
$item = [];
$item['uri'] = $audio->sharingUrl;
$item['title'] = $audio->title;
$imageSquare = str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $audio->image->url1X1);
$image = str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $audio->image->url);
$item['enclosures'] = [
$audio->audios[0]->url,
$imageSquare
];
// synopsis in list is shortened, full synopsis is available using one request per item
$item['content'] = '<img src="' . $image . '" /><p>' . $audio->synopsis . '</p>';
$item['timestamp'] = $audio->publicationStartDateAndTime;
$item['uid'] = $audio->id;
$item['author'] = $audio->programSet->publicationService->title;
$item['categories'] = [ $audio->programSet->editorialCategories->title ];
$this->items[] = $item;
}
}
$this->title = $processedJSON->title;
$this->uri = $processedJSON->sharingUrl;
$this->icon = str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $processedJSON->image->url1X1);
// add image file extension to URL so icon is shown in generated RSS feeds, see
// https://github.com/RSS-Bridge/rss-bridge/blob/4aed05c7b678b5673386d61374bba13637d15487/formats/MrssFormat.php#L76
$this->icon = $this->icon . self::IMAGEEXTENSION;
$this->items = array_slice($this->items, 0, $limit);
date_default_timezone_set($oldTz);
}
/** {@inheritdoc} */
public function getURI()
{
if (!empty($this->uri)) {
return $this->uri;
}
return parent::getURI();
}
/** {@inheritdoc} */
public function getName()
{
if (!empty($this->title)) {
return $this->title;
}
return parent::getName();
}
/** {@inheritdoc} */
public function getIcon()
{
if (!empty($this->icon)) {
return $this->icon;
}
return parent::getIcon();
}
}

View File

@@ -1,95 +1,98 @@
<?php
class ARDMediathekBridge extends BridgeAbstract {
const NAME = 'ARD-Mediathek Bridge';
const URI = 'https://www.ardmediathek.de';
const DESCRIPTION = 'Feed of any series in the ARD-Mediathek, specified by its path';
const MAINTAINER = 'yue-dongchen';
/*
* Number of Items to be requested from ARDmediathek API
* 12 has been observed on the wild
* 29 is the highest successfully tested value
* More Items could be fetched via pagination
* The JSON-field pagination holds more information on that
* @const PAGESIZE number of requested items
*/
const PAGESIZE = 29;
/*
* The URL Prefix of the (Webapp-)API
* @const APIENDPOINT https-URL of the used endpoint
*/
const APIENDPOINT = 'https://api.ardmediathek.de/page-gateway/widgets/ard/asset/';
/*
* The URL prefix of the video link
* URLs from the webapp include a slug containing titles of show, episode, and tv station.
* It seems to work without that.
* @const VIDEOLINKPREFIX https-URL prefix of video links
*/
const VIDEOLINKPREFIX = 'https://www.ardmediathek.de/video/';
/*
* The requested width of the preview image
* 432 has been observed on the wild
* The webapp seems to also compute and add the height value
* It seems to works without that.
* @const IMAGEWIDTH width in px of the preview image
*/
const IMAGEWIDTH = 432;
/*
* Placeholder that will be replace by IMAGEWIDTH in the preview image URL
* @const IMAGEWIDTHPLACEHOLDER
*/
const IMAGEWIDTHPLACEHOLDER = '{width}';
const PARAMETERS = array(
array(
'path' => array(
'name' => 'Show Link or ID',
'required' => true,
'title' => 'Link to the show page or just its alphanumeric suffix',
'defaultValue' => 'https://www.ardmediathek.de/sendung/45-min/Y3JpZDovL25kci5kZS8xMzkx/'
)
)
);
class ARDMediathekBridge extends BridgeAbstract
{
const NAME = 'ARD-Mediathek Bridge';
const URI = 'https://www.ardmediathek.de';
const DESCRIPTION = 'Feed of any series in the ARD-Mediathek, specified by its path';
const MAINTAINER = 'yue-dongchen';
/*
* Number of Items to be requested from ARDmediathek API
* 12 has been observed on the wild
* 29 is the highest successfully tested value
* More Items could be fetched via pagination
* The JSON-field pagination holds more information on that
* @const PAGESIZE number of requested items
*/
const PAGESIZE = 29;
/*
* The URL Prefix of the (Webapp-)API
* @const APIENDPOINT https-URL of the used endpoint
*/
const APIENDPOINT = 'https://api.ardmediathek.de/page-gateway/widgets/ard/asset/';
/*
* The URL prefix of the video link
* URLs from the webapp include a slug containing titles of show, episode, and tv station.
* It seems to work without that.
* @const VIDEOLINKPREFIX https-URL prefix of video links
*/
const VIDEOLINKPREFIX = 'https://www.ardmediathek.de/video/';
/*
* The requested width of the preview image
* 432 has been observed on the wild
* The webapp seems to also compute and add the height value
* It seems to works without that.
* @const IMAGEWIDTH width in px of the preview image
*/
const IMAGEWIDTH = 432;
/*
* Placeholder that will be replace by IMAGEWIDTH in the preview image URL
* @const IMAGEWIDTHPLACEHOLDER
*/
const IMAGEWIDTHPLACEHOLDER = '{width}';
public function collectData() {
$oldTz = date_default_timezone_get();
const PARAMETERS = [
[
'path' => [
'name' => 'Show Link or ID',
'required' => true,
'title' => 'Link to the show page or just its alphanumeric suffix',
'defaultValue' => 'https://www.ardmediathek.de/sendung/45-min/Y3JpZDovL25kci5kZS8xMzkx/'
]
]
];
date_default_timezone_set('Europe/Berlin');
public function collectData()
{
$oldTz = date_default_timezone_get();
$pathComponents = explode('/', $this->getInput('path'));
if (empty($pathComponents)) {
returnClientError('Path may not be empty');
}
if (count($pathComponents) < 2) {
$showID = $pathComponents[0];
} else {
$lastKey = count($pathComponents) - 1;
$showID = $pathComponents[$lastKey];
if (strlen($showID) === 0) {
$showID = $pathComponents[$lastKey - 1];
}
}
date_default_timezone_set('Europe/Berlin');
$url = SELF::APIENDPOINT . $showID . '/?pageSize=' . SELF::PAGESIZE;
$rawJSON = getContents($url);
$processedJSON = json_decode($rawJSON);
$pathComponents = explode('/', $this->getInput('path'));
if (empty($pathComponents)) {
returnClientError('Path may not be empty');
}
if (count($pathComponents) < 2) {
$showID = $pathComponents[0];
} else {
$lastKey = count($pathComponents) - 1;
$showID = $pathComponents[$lastKey];
if (strlen($showID) === 0) {
$showID = $pathComponents[$lastKey - 1];
}
}
foreach($processedJSON->teasers as $video) {
$item = array();
// there is also ->links->self->id, ->links->self->urlId, ->links->target->id, ->links->target->urlId
$item['uri'] = SELF::VIDEOLINKPREFIX . $video->id . '/';
// there is also ->mediumTitle and ->shortTitle
$item['title'] = $video->longTitle;
// in the test, aspect16x9 was the only child of images, not sure whether that is always true
$item['enclosures'] = array(
str_replace(SELF::IMAGEWIDTHPLACEHOLDER, SELF::IMAGEWIDTH, $video->images->aspect16x9->src)
);
$item['content'] = '<img src="' . $item['enclosures'][0] . '" /><p>';
$item['timestamp'] = $video->broadcastedOn;
$item['uid'] = $video->id;
$item['author'] = $video->publicationService->name;
$this->items[] = $item;
}
$url = self::APIENDPOINT . $showID . '/?pageSize=' . self::PAGESIZE;
$rawJSON = getContents($url);
$processedJSON = json_decode($rawJSON);
date_default_timezone_set($oldTz);
}
foreach ($processedJSON->teasers as $video) {
$item = [];
// there is also ->links->self->id, ->links->self->urlId, ->links->target->id, ->links->target->urlId
$item['uri'] = self::VIDEOLINKPREFIX . $video->id . '/';
// there is also ->mediumTitle and ->shortTitle
$item['title'] = $video->longTitle;
// in the test, aspect16x9 was the only child of images, not sure whether that is always true
$item['enclosures'] = [
str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $video->images->aspect16x9->src)
];
$item['content'] = '<img src="' . $item['enclosures'][0] . '" /><p>';
$item['timestamp'] = $video->broadcastedOn;
$item['uid'] = $video->id;
$item['author'] = $video->publicationService->name;
$this->items[] = $item;
}
date_default_timezone_set($oldTz);
}
}

View File

@@ -1,55 +1,63 @@
<?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
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 = [];
public function collectData() {
const CACHE_TIMEOUT = 3600; // 1 hour
$html = getSimpleHTMLDOM(self::URI . '/news/index.asp');
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI . '/news/index.asp');
$html = defaultLinkTo($html, self::URI . '/news/');
$html = defaultLinkTo($html, self::URI . '/news/');
foreach($html->find('div.inner > a') as $index => $a) {
$item = array();
foreach ($html->find('div.inner > a') as $index => $a) {
$item = [];
$articlePath = $a->href;
$articlePath = $a->href;
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT);
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT);
$articlePageHtml = defaultLinkTo($articlePageHtml, self::URI);
$articlePageHtml = defaultLinkTo($articlePageHtml, self::URI);
$contents = $articlePageHtml->find('div.Contents', 0);
$contents = $articlePageHtml->find('div.Contents', 0);
$item['uri'] = $articlePath;
$item['title'] = $contents->find('h3', 0)->innertext;
$item['uri'] = $articlePath;
$item['title'] = $contents->find('h3', 0)->innertext;
$contents->find('h3', 0)->outertext = '';
$contents->find('h3', 0)->outertext = '';
$item['content'] = $contents->innertext;
$item['timestamp'] = $this->extractDate($a->plaintext);
$item['enclosures'][] = $a->find('img', 0)->src;
$this->items[] = $item;
$item['content'] = $contents->innertext;
$item['timestamp'] = $this->extractDate($a->plaintext);
if (count($this->items) >= 10) {
break;
}
}
}
$img = $a->find('img', 0);
if ($img) {
$item['enclosures'][] = $img->src;
}
private function extractDate($text) {
$dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
$this->items[] = $item;
$text = trim($text);
if (count($this->items) >= 10) {
break;
}
}
}
if (preg_match($dateRegex, $text, $matches)) {
return $matches[1];
}
private function extractDate($text)
{
$dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
return '';
}
$text = trim($text);
if (preg_match($dateRegex, $text, $matches)) {
return $matches[1];
}
return '';
}
}

View File

@@ -1,37 +1,40 @@
<?php
class AcrimedBridge extends FeedExpander {
const MAINTAINER = 'qwertygc';
const NAME = 'Acrimed Bridge';
const URI = 'https://www.acrimed.org/';
const CACHE_TIMEOUT = 4800; //2hours
const DESCRIPTION = 'Returns the newest articles';
class AcrimedBridge extends FeedExpander
{
const MAINTAINER = 'qwertygc';
const NAME = 'Acrimed Bridge';
const URI = 'https://www.acrimed.org/';
const CACHE_TIMEOUT = 4800; //2hours
const DESCRIPTION = 'Returns the newest articles';
const PARAMETERS = [
[
'limit' => [
'name' => 'limit',
'type' => 'number',
'defaultValue' => -1,
]
]
];
const PARAMETERS = [
[
'limit' => [
'name' => 'limit',
'type' => 'number',
'defaultValue' => -1,
]
]
];
public function collectData(){
$this->collectExpandableDatas(
static::URI . 'spip.php?page=backend',
$this->getInput('limit')
);
}
public function collectData()
{
$this->collectExpandableDatas(
static::URI . 'spip.php?page=backend',
$this->getInput('limit')
);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
protected function parseItem($newsItem)
{
$item = parent::parseItem($newsItem);
$articlePage = getSimpleHTMLDOM($newsItem->link);
$article = sanitize($articlePage->find('article.article1', 0)->innertext);
$article = defaultLinkTo($article, static::URI);
$item['content'] = $article;
$articlePage = getSimpleHTMLDOM($newsItem->link);
$article = sanitize($articlePage->find('article.article1', 0)->innertext);
$article = defaultLinkTo($article, static::URI);
$item['content'] = $article;
return $item;
}
return $item;
}
}

View File

@@ -1,54 +1,57 @@
<?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'
)
)
)
);
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 = [
'Publications' => [
'theme' => [
'name' => 'Thematique',
'type' => 'list',
'values' => [
'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 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.');
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 . '" />
foreach ($html->find('article') as $article) {
$item = [];
// 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;
}
}
. $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

@@ -1,73 +1,76 @@
<?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
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',
'required' => true,
'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'
),
));
const PARAMETERS = [ [
'postcount' => [
'name' => 'Limit',
'type' => 'number',
'required' => true,
'title' => 'Maximum number of items to return',
'defaultValue' => 5,
],
'language' => [
'name' => 'Language',
'type' => 'list',
'values' => [
'English' => 'en',
'Deutsch' => 'de',
'Polski' => 'pl',
'Français' => 'fr',
'Русский' => 'ru',
'Português' => 'pt',
'Español' => 'es',
],
'title' => 'Language of changelog posts',
'defaultValue' => 'en',
],
'full' => [
'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');
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);
$html = getSimpleHTMLDOM($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;
}
}
foreach ($html->find('li') as $data) {
$item = [];
$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);
$html = defaultLinkTo($html, self::URI);
return $html->find('div.small-12.columns', 1)->innertext;
}
private function getFullChangelog($url)
{
$html = getSimpleHTMLDOMCached($url);
$html = defaultLinkTo($html, self::URI);
return $html->find('div.small-12.columns', 1)->innertext;
}
}

View File

@@ -1,83 +1,87 @@
<?php
class AlfaBankByBridge extends BridgeAbstract {
const MAINTAINER = 'lassana';
const NAME = 'AlfaBank.by Новости';
const URI = 'https://www.alfabank.by';
const DESCRIPTION = 'Уведомления Alfa-Now — новости от Альфа-Банка';
const CACHE_TIMEOUT = 3600; // 1 hour
const PARAMETERS = array(
'News' => array(
'business' => array(
'name' => 'Альфа Бизнес',
'type' => 'list',
'title' => 'В зависимости от выбора, возращает уведомления для" .
class AlfaBankByBridge extends BridgeAbstract
{
const MAINTAINER = 'lassana';
const NAME = 'AlfaBank.by Новости';
const URI = 'https://www.alfabank.by';
const DESCRIPTION = 'Уведомления Alfa-Now — новости от Альфа-Банка';
const CACHE_TIMEOUT = 3600; // 1 hour
const PARAMETERS = [
'News' => [
'business' => [
'name' => 'Альфа Бизнес',
'type' => 'list',
'title' => 'В зависимости от выбора, возращает уведомления для" .
" клиентов физ. лиц либо для клиентов-юридических лиц и ИП',
'values' => array(
'Новости' => 'news',
'Новости бизнеса' => 'newsBusiness'
),
'defaultValue' => 'news'
),
'fullContent' => array(
'name' => 'Включать содержимое',
'type' => 'checkbox',
'title' => 'Если выбрано, содержимое уведомлений вставляется в поток (работает медленно)'
)
)
);
'values' => [
'Новости' => 'news',
'Новости бизнеса' => 'newsBusiness'
],
'defaultValue' => 'news'
],
'fullContent' => [
'name' => 'Включать содержимое',
'type' => 'checkbox',
'title' => 'Если выбрано, содержимое уведомлений вставляется в поток (работает медленно)'
]
]
];
public function collectData() {
$business = $this->getInput('business') == 'newsBusiness';
$fullContent = $this->getInput('fullContent') == 'on';
public function collectData()
{
$business = $this->getInput('business') == 'newsBusiness';
$fullContent = $this->getInput('fullContent') == 'on';
$mainPageUrl = self::URI . '/about/articles/uvedomleniya/';
if($business) {
$mainPageUrl .= '?business=true';
}
$html = getSimpleHTMLDOM($mainPageUrl);
$limit = 0;
$mainPageUrl = self::URI . '/about/articles/uvedomleniya/';
if ($business) {
$mainPageUrl .= '?business=true';
}
$html = getSimpleHTMLDOM($mainPageUrl);
$limit = 0;
foreach($html->find('a.notifications__item') as $element) {
if($limit < 10) {
$item = array();
$item['uid'] = 'urn:sha1:' . hash('sha1', $element->getAttribute('data-notification-id'));
$item['title'] = $element->find('div.item-title', 0)->innertext;
$item['timestamp'] = DateTime::createFromFormat(
'd M Y',
$this->ruMonthsToEn($element->find('div.item-date', 0)->innertext)
)->getTimestamp();
foreach ($html->find('a.notifications__item') as $element) {
if ($limit < 10) {
$item = [];
$item['uid'] = 'urn:sha1:' . hash('sha1', $element->getAttribute('data-notification-id'));
$item['title'] = $element->find('div.item-title', 0)->innertext;
$item['timestamp'] = DateTime::createFromFormat(
'd M Y',
$this->ruMonthsToEn($element->find('div.item-date', 0)->innertext)
)->getTimestamp();
$itemUrl = self::URI . $element->href;
if($business) {
$itemUrl = str_replace('?business=true', '', $itemUrl);
}
$item['uri'] = $itemUrl;
$itemUrl = self::URI . $element->href;
if ($business) {
$itemUrl = str_replace('?business=true', '', $itemUrl);
}
$item['uri'] = $itemUrl;
if($fullContent) {
$itemHtml = getSimpleHTMLDOM($itemUrl);
if($itemHtml) {
$item['content'] = $itemHtml->find('div.now-p__content-text', 0)->innertext;
}
}
if ($fullContent) {
$itemHtml = getSimpleHTMLDOM($itemUrl);
if ($itemHtml) {
$item['content'] = $itemHtml->find('div.now-p__content-text', 0)->innertext;
}
}
$this->items[] = $item;
$limit++;
}
}
}
$this->items[] = $item;
$limit++;
}
}
}
public function getIcon() {
return static::URI . '/local/images/favicon.ico';
}
public function getIcon()
{
return static::URI . '/local/images/favicon.ico';
}
private function ruMonthsToEn($date) {
$ruMonths = array(
'Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня',
'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря' );
$enMonths = array(
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December' );
return str_replace($ruMonths, $enMonths, $date);
}
private function ruMonthsToEn($date)
{
$ruMonths = [
'Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня',
'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря' ];
$enMonths = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December' ];
return str_replace($ruMonths, $enMonths, $date);
}
}

View File

@@ -0,0 +1,85 @@
<?php
class AllSidesBridge extends BridgeAbstract
{
const NAME = 'AllSides';
const URI = 'https://www.allsides.com';
const DESCRIPTION = 'Balanced news and media bias ratings.';
const MAINTAINER = 'Oliver Nutter';
const PARAMETERS = [
'global' => [
'limit' => [
'name' => 'Number of posts to return',
'type' => 'number',
'defaultValue' => 10,
'required' => false,
'title' => 'Zero or negative values return all posts (ignored if not fetching full article)',
],
'fetch' => [
'name' => 'Fetch full article content',
'type' => 'checkbox',
'defaultValue' => 'checked',
],
],
'Headline Roundups' => [],
];
private const ROUNDUPS_URI = self::URI . '/headline-roundups';
public function collectData()
{
switch ($this->queriedContext) {
case 'Headline Roundups':
$index = getSimpleHTMLDOM(self::ROUNDUPS_URI);
defaultLinkTo($index, self::ROUNDUPS_URI);
$entries = $index->find('table.views-table > tbody > tr');
$limit = (int) $this->getInput('limit');
$fetch = (bool) $this->getInput('fetch');
if ($limit > 0 && $fetch) {
$entries = array_slice($entries, 0, $limit);
}
foreach ($entries as $entry) {
$item = [
'title' => $entry->find('.views-field-name', 0)->text(),
'uri' => $entry->find('a', 0)->href,
'timestamp' => $entry->find('.date-display-single', 0)->content,
'author' => 'AllSides Staff',
];
if ($fetch) {
$article = getSimpleHTMLDOMCached($item['uri']);
defaultLinkTo($article, $item['uri']);
$item['content'] = $article->find('.story-id-page-description', 0);
foreach ($article->find('.page-tags a') as $tag) {
$item['categories'][] = $tag->text();
}
}
$this->items[] = $item;
}
break;
}
}
public function getName()
{
if ($this->queriedContext) {
return self::NAME . " - {$this->queriedContext}";
}
return self::NAME;
}
public function getURI()
{
switch ($this->queriedContext) {
case 'Headline Roundups':
return self::ROUNDUPS_URI;
}
return self::URI;
}
}

144
bridges/AllegroBridge.php Normal file
View File

@@ -0,0 +1,144 @@
<?php
class AllegroBridge extends BridgeAbstract
{
const NAME = 'Allegro';
const URI = 'https://www.allegro.pl';
const DESCRIPTION = 'Returns the search results from the Allegro.pl shopping and bidding portal';
const MAINTAINER = 'wrobelda';
const PARAMETERS = [[
'url' => [
'name' => 'Search URL',
'title' => 'Copy the URL from your browser\'s address bar after searching for your items and paste it here',
'exampleValue' => 'https://allegro.pl/kategoria/swieze-warzywa-cebula-318660',
'required' => true,
],
'sessioncookie' => [
'name' => 'The \'wdctx\' session cookie',
'title' => 'Paste the value of the \'wdctx\' cookie from your browser if you want to prevent Allegro imposing rate limits',
'pattern' => '^.{250,};?$',
// phpcs:ignore
'exampleValue' => 'v4.1-oCrmXTMqv2ppC21GTUCKLmUwRPP1ssQVALKuqwsZ1VXjcKgL2vO5TTRM5xMxS9GiyqxF1gAeyc-63dl0coUoBKXCXi_nAmr95yyqGpq2RAFoneZ4L399E8n6iYyemcuGARjAoSfjvLHJCEwvvHHynSgaxlFBu7hUnKfuy39zo9sSQdyTUjotJg3CAZ53q9v2raAnPCyGOAR4ytRILd9p24EJnxp7_oR0XbVPIo1hDa4WmjXFOxph8rHaO5tWd',
'required' => false,
],
'includeSponsoredOffers' => [
'type' => 'checkbox',
'name' => 'Include Sponsored Offers'
]
]];
public function getName()
{
parse_str(parse_url($this->getInput('url'), PHP_URL_QUERY), $fields);
if ($query = array_key_exists('string', $fields) ? urldecode($fields['string']) : false) {
return $query;
}
return parent::getName();
}
public function getURI()
{
return $this->getInput('url') ?? parent::getURI();
}
public function collectData()
{
# make sure we order by the most recently listed offers
$url = preg_replace('/([?&])order=[^&]+(&|$)/', '$1', $this->getInput('url'));
$url .= (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . 'order=n';
$opts = [];
// If a session cookie is provided
if ($sessioncookie = $this->getInput('sessioncookie')) {
$opts[CURLOPT_COOKIE] = 'wdctx=' . $sessioncookie;
}
$html = getSimpleHTMLDOM($url, [], $opts);
# if no results found
if ($html->find('.mzmg_6m.m9qz_yo._6a66d_-fJr5')) {
return;
}
$results = $html->find('._6a66d_V7Lel article');
if (!$this->getInput('includeSponsoredOffers')) {
$results = array_filter($results, function ($node) {
return $node->{'data-analytics-view-label'} != 'showSponsoredItems';
});
}
foreach ($results as $post) {
$item = [];
$item['uri'] = $post->find('._6a66d_LX75-', 0)->href;
//TODO: port this over, whatever it does, from https://github.com/MK-PL/AllegroRSS
// if (arrayLinks.includes('events/clicks?')) {
// let sponsoredLink = new URL(arrayLinks).searchParams.get('redirect')
// arrayLinks = sponsoredLink.slice(0, sponsoredLink.indexOf('?'))
// }
$item['title'] = $post->find('._6a66d_LX75-', 0)->innertext;
$item['uid'] = $post->{'data-analytics-view-value'};
$descriptionPatterns = ['/<\s*dt[^>]*>\b/', '/<\/dt>/', '/<\s*dd[^>]*>\b/', '/<\/dd>/'];
$descriptionReplacements = ['<span>', ':</span> ', '<strong>', '&emsp;</strong> '];
$description = $post->find('.m7er_k4.mpof_5r.mpof_z0_s', 0)->innertext;
$descriptionPretty = preg_replace($descriptionPatterns, $descriptionReplacements, $description);
$buyNowAuction = $post->find('.mqu1_g3.mvrt_0.mgn2_12', 0)->innertext ?? '';
$buyNowAuction = str_replace('</span><span', '</span> <span', $buyNowAuction);
$auctionTimeLeft = $post->find('._6a66d_ImOzU', 0)->innertext ?? '';
$price = $post->find('._6a66d_6R3iN', 0)->plaintext;
$price = empty($auctionTimeLeft) ? $price : $price . '- kwota licytacji';
$image = $post->find('._6a66d_44ioA img', 0)->{'data-src'} ?: $post->find('._6a66d_44ioA img', 0)->src ?? false;
if ($image) {
$item['enclosures'] = [$image . '#.image'];
}
$offerExtraInfo = array_filter($post->find('.mqu1_g3.mgn2_12'), function ($node) {
return empty($node->find('.mvrt_0'));
});
$offerExtraInfo = $offerExtraInfo[0]->plaintext ?? '';
$isSmart = $post->find('._6a66d_TC2Zk', 0)->innertext ?? '';
if (str_contains($isSmart, 'z kurierem')) {
$offerExtraInfo .= ', Smart z kurierem';
} else {
$offerExtraInfo .= ', Smart';
}
$item['categories'] = [];
$parameters = $post->find('dd');
foreach ($parameters as $parameter) {
if (in_array(strtolower($parameter->innertext), ['brak', 'nie'])) {
continue;
}
$item['categories'][] = $parameter->innertext;
}
$item['content'] = $descriptionPretty
. '<div><strong>'
. $price
. '</strong></div><div>'
. $auctionTimeLeft
. '</div><div>'
. $buyNowAuction
. '</div><dl>'
. $offerExtraInfo
. '</dl><hr>';
$this->items[] = $item;
}
}
}

View File

@@ -1,106 +1,107 @@
<?php
class AllocineFRBridge extends BridgeAbstract {
const MAINTAINER = 'superbaillot.net';
const NAME = 'Allo Cine Bridge';
const CACHE_TIMEOUT = 25200; // 7h
const URI = 'https://www.allocine.fr/';
const DESCRIPTION = 'Bridge for allocine.fr';
const PARAMETERS = array( array(
'category' => array(
'name' => 'Emission',
'type' => 'list',
'title' => 'Sélectionner l\'emission',
'values' => array(
'Faux Raccord' => 'faux-raccord',
'Fanzone' => 'fanzone',
'Game In Ciné' => 'game-in-cine',
'Pour la faire courte' => 'pour-la-faire-courte',
'Home Cinéma' => 'home-cinema',
'PILS - Par Ici Les Sorties' => 'pils-par-ici-les-sorties',
'AlloCiné : l\'émission, sur LeStream' => 'allocine-lemission-sur-lestream',
'Give Me Five' => 'give-me-five',
'Aviez-vous remarqué ?' => 'aviez-vous-remarque',
'Et paf, il est mort' => 'et-paf-il-est-mort',
'The Big Fan Theory' => 'the-big-fan-theory',
'Clichés' => 'cliches',
'Complètement...' => 'completement',
'#Fun Facts' => 'fun-facts',
'Origin Story' => 'origin-story',
)
)
));
class AllocineFRBridge extends BridgeAbstract
{
const MAINTAINER = 'superbaillot.net';
const NAME = 'Allo Cine Bridge';
const CACHE_TIMEOUT = 25200; // 7h
const URI = 'https://www.allocine.fr';
const DESCRIPTION = 'Bridge for allocine.fr';
const PARAMETERS = [ [
'category' => [
'name' => 'Emission',
'type' => 'list',
'title' => 'Sélectionner l\'emission',
'values' => [
'Faux Raccord' => 'faux-raccord',
'Fanzone' => 'fanzone',
'Game In Ciné' => 'game-in-cine',
'Pour la faire courte' => 'pour-la-faire-courte',
'Home Cinéma' => 'home-cinema',
'PILS - Par Ici Les Sorties' => 'pils-par-ici-les-sorties',
'AlloCiné : l\'émission, sur LeStream' => 'allocine-lemission-sur-lestream',
'Give Me Five' => 'give-me-five',
'Aviez-vous remarqué ?' => 'aviez-vous-remarque',
'Et paf, il est mort' => 'et-paf-il-est-mort',
'The Big Fan Theory' => 'the-big-fan-theory',
'Clichés' => 'cliches',
'Complètement...' => 'completement',
'#Fun Facts' => 'fun-facts',
'Origin Story' => 'origin-story',
]
]
]];
public function getURI(){
if(!is_null($this->getInput('category'))) {
public function getURI()
{
if (!is_null($this->getInput('category'))) {
$categories = [
'faux-raccord' => '/video/programme-12284/',
'fanzone' => '/video/programme-12298/',
'game-in-cine' => '/video/programme-12288/',
'pour-la-faire-courte' => '/video/programme-20960/',
'home-cinema' => '/video/programme-12287/',
'pils-par-ici-les-sorties' => '/video/programme-25789/',
'allocine-lemission-sur-lestream' => '/video/programme-25123/',
'give-me-five' => '/video/programme-21919/saison-34518/',
'aviez-vous-remarque' => '/video/programme-19518/',
'et-paf-il-est-mort' => '/video/programme-25113/',
'the-big-fan-theory' => '/video/programme-20403/',
'cliches' => '/video/programme-24834/',
'completement' => '/video/programme-23859/',
'fun-facts' => '/video/programme-23040/',
'origin-story' => '/video/programme-25667/'
];
$categories = array(
'faux-raccord' => 'video/programme-12284/saison-37054/',
'fanzone' => 'video/programme-12298/saison-37059/',
'game-in-cine' => 'video/programme-12288/saison-22971/',
'pour-la-faire-courte' => 'video/programme-20960/saison-29678/',
'home-cinema' => 'video/programme-12287/saison-34703/',
'pils-par-ici-les-sorties' => 'video/programme-25789/saison-37253/',
'allocine-lemission-sur-lestream' => 'video/programme-25123/saison-36067/',
'give-me-five' => 'video/programme-21919/saison-34518/',
'aviez-vous-remarque' => 'video/programme-19518/saison-37084/',
'et-paf-il-est-mort' => 'video/programme-25113/saison-36657/',
'the-big-fan-theory' => 'video/programme-20403/saison-37419/',
'cliches' => 'video/programme-24834/saison-35591/',
'completement' => 'video/programme-23859/saison-34102/',
'fun-facts' => 'video/programme-23040/saison-32686/',
'origin-story' => 'video/programme-25667/saison-37041/'
);
$category = $this->getInput('category');
if (array_key_exists($category, $categories)) {
return static::URI . $this->getLastSeasonURI($categories[$category]);
} else {
returnClientError('Emission inconnue');
}
}
$category = $this->getInput('category');
if(array_key_exists($category, $categories)) {
return static::URI . $categories[$category];
} else {
returnClientError('Emission inconnue');
}
}
return parent::getURI();
}
return parent::getURI();
}
private function getLastSeasonURI($category)
{
$html = getSimpleHTMLDOMCached(static::URI . $category, 86400);
$seasonLink = $html->find('section[class=section-wrap section]', 0)->find('div[class=cf]', 0)->find('a', 0);
$URI = $seasonLink->href;
return $URI;
}
public function getName(){
if(!is_null($this->getInput('category'))) {
return self::NAME . ' : '
. array_search(
$this->getInput('category'),
self::PARAMETERS[$this->queriedContext]['category']['values']
);
}
public function getName()
{
if (!is_null($this->getInput('category'))) {
return self::NAME . ' : ' . $this->getKey('category');
}
return parent::getName();
}
return parent::getName();
}
public function collectData(){
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
$html = getSimpleHTMLDOM($this->getURI());
foreach ($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
$item = [];
$category = array_search(
$this->getInput('category'),
self::PARAMETERS[$this->queriedContext]['category']['values']
);
$title = $element->find('a[class*=meta-title-link]', 0);
$content = trim(defaultLinkTo($element->outertext, static::URI));
foreach($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
$item = array();
// Replace image 'src' with the one in 'data-src'
$content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content);
$content = preg_replace('@data-src=@', 'src=', $content);
$title = $element->find('a[class*=meta-title-link]', 0);
$content = trim($element->outertext);
// Remove date in the content to prevent content update while the video is getting older
$content = preg_replace('@<div class="meta-sub light">.*<span>[^<]*</span>[^<]*</div>@', '', $content);
// Replace image 'src' with the one in 'data-src'
$content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content);
$content = preg_replace('@data-src=@', 'src=', $content);
// Remove date in the content to prevent content update while the video is getting older
$content = preg_replace('@<div class="meta-sub light">.*<span>[^<]*</span>[^<]*</div>@', '', $content);
$item['content'] = $content;
$item['title'] = trim($title->innertext);
$item['uri'] = static::URI . substr($title->href, 1);
$this->items[] = $item;
}
}
$item['content'] = $content;
$item['title'] = trim($title->innertext);
$item['uri'] = static::URI . '/' . substr($title->href, 1);
$this->items[] = $item;
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
class AllocineFRSortiesBridge extends BridgeAbstract
{
const MAINTAINER = 'Simounet';
const NAME = 'AlloCiné Sorties Bridge';
const CACHE_TIMEOUT = 25200; // 7h
const BASE_URI = 'https://www.allocine.fr';
const URI = self::BASE_URI . '/film/sorties-semaine/';
const DESCRIPTION = 'Bridge for AlloCiné - Sorties cinéma cette semaine';
public function getName()
{
return self::NAME;
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
foreach ($html->find('section.section.section-wrap', 0)->find('li.mdl') as $element) {
$item = [];
$thumb = $element->find('figure.thumbnail', 0);
$meta = $element->find('div.meta-body', 0);
$synopsis = $element->find('div.synopsis', 0);
$title = $element->find('a[class*=meta-title-link]', 0);
$content = trim(defaultLinkTo($thumb->outertext . $meta->outertext . $synopsis->outertext, static::URI));
// Replace image 'src' with the one in 'data-src'
$content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9=+\/]*"@', '', $content);
$content = preg_replace('@data-src=@', 'src=', $content);
$item['content'] = $content;
$item['title'] = trim($title->innertext);
$item['uri'] = static::BASE_URI . '/' . substr($title->href, 1);
$this->items[] = $item;
}
}
}

View File

@@ -1,95 +1,105 @@
<?php
class AmazonBridge extends BridgeAbstract {
class AmazonBridge extends BridgeAbstract
{
const MAINTAINER = 'Alexis CHEMEL';
const NAME = 'Amazon';
const URI = 'https://www.amazon.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns products from Amazon search';
const MAINTAINER = 'Alexis CHEMEL';
const NAME = 'Amazon';
const URI = 'https://www.amazon.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns products from Amazon search';
const PARAMETERS = [[
'q' => [
'name' => 'Keyword',
'required' => true,
'exampleValue' => 'watch',
],
'sort' => [
'name' => 'Sort by',
'type' => 'list',
'values' => [
'Relevance' => 'relevanceblender',
'Price: Low to High' => 'price-asc-rank',
'Price: High to Low' => 'price-desc-rank',
'Average Customer Review' => 'review-rank',
'Newest Arrivals' => 'date-desc-rank',
],
'defaultValue' => 'relevanceblender',
],
'tld' => [
'name' => 'Country',
'type' => 'list',
'values' => [
'Australia' => 'com.au',
'Brazil' => 'com.br',
'Canada' => 'ca',
'China' => 'cn',
'France' => 'fr',
'Germany' => 'de',
'India' => 'in',
'Italy' => 'it',
'Japan' => 'co.jp',
'Mexico' => 'com.mx',
'Netherlands' => 'nl',
'Poland' => 'pl',
'Spain' => 'es',
'Sweden' => 'se',
'Turkey' => 'com.tr',
'United Kingdom' => 'co.uk',
'United States' => 'com',
],
'defaultValue' => 'com',
],
]];
const PARAMETERS = array(array(
'q' => array(
'name' => 'Keyword',
'required' => true,
'exampleValue' => 'watch',
),
'sort' => array(
'name' => 'Sort by',
'type' => 'list',
'values' => array(
'Relevance' => 'relevanceblender',
'Price: Low to High' => 'price-asc-rank',
'Price: High to Low' => 'price-desc-rank',
'Average Customer Review' => 'review-rank',
'Newest Arrivals' => 'date-desc-rank',
),
'defaultValue' => 'relevanceblender',
),
'tld' => array(
'name' => 'Country',
'type' => 'list',
'values' => array(
'Australia' => 'com.au',
'Brazil' => 'com.br',
'Canada' => 'ca',
'China' => 'cn',
'France' => 'fr',
'Germany' => 'de',
'India' => 'in',
'Italy' => 'it',
'Japan' => 'co.jp',
'Mexico' => 'com.mx',
'Netherlands' => 'nl',
'Spain' => 'es',
'United Kingdom' => 'co.uk',
'United States' => 'com',
),
'defaultValue' => 'com',
),
));
public function collectData()
{
$baseUrl = sprintf('https://www.amazon.%s', $this->getInput('tld'));
public function getName(){
if(!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q');
}
$url = sprintf(
'%s/s/?field-keywords=%s&sort=%s',
$baseUrl,
urlencode($this->getInput('q')),
$this->getInput('sort')
);
return parent::getName();
}
$dom = getSimpleHTMLDOM($url);
public function collectData() {
$elements = $dom->find('div.s-result-item');
$uri = 'https://www.amazon.' . $this->getInput('tld') . '/';
$uri .= 's/?field-keywords=' . urlencode($this->getInput('q')) . '&sort=' . $this->getInput('sort');
foreach ($elements as $element) {
$item = [];
$html = getSimpleHTMLDOM($uri);
$title = $element->find('h2', 0);
if (!$title) {
continue;
}
foreach($html->find('li.s-result-item') as $element) {
$item['title'] = $title->innertext;
$item = array();
$itemUrl = $element->find('a', 0)->href;
$item['uri'] = urljoin($baseUrl, $itemUrl);
// Title
$title = $element->find('h2', 0);
if (is_null($title)) {
continue;
}
$image = $element->find('img', 0);
if ($image) {
$item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />';
}
$item['title'] = html_entity_decode($title->innertext, ENT_QUOTES);
$price = $element->find('span.a-price > .a-offscreen', 0);
if ($price) {
$item['content'] .= $price->innertext;
}
// Url
$uri = $title->parent()->getAttribute('href');
$uri = substr($uri, 0, strrpos($uri, '/'));
$this->items[] = $item;
}
}
$item['uri'] = substr($uri, 0, strrpos($uri, '/'));
public function getName()
{
if (!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q');
}
// Content
$image = $element->find('img', 0);
$price = $element->find('span.s-price', 0);
$price = ($price) ? $price->innertext : '';
$item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />' . $price;
$this->items[] = $item;
}
}
return parent::getName();
}
}

View File

@@ -1,242 +1,254 @@
<?php
class AmazonPriceTrackerBridge extends BridgeAbstract {
const MAINTAINER = 'captn3m0, sal0max';
const NAME = 'Amazon Price Tracker';
const URI = 'https://www.amazon.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Tracks price for a single product on Amazon';
class AmazonPriceTrackerBridge extends BridgeAbstract
{
const MAINTAINER = 'captn3m0, sal0max';
const NAME = 'Amazon Price Tracker';
const URI = 'https://www.amazon.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Tracks price for a single product on Amazon';
const PARAMETERS = array(
array(
'asin' => array(
'name' => 'ASIN',
'required' => true,
'exampleValue' => 'B071GB1VMQ',
// https://stackoverflow.com/a/12827734
'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)',
),
'tld' => array(
'name' => 'Country',
'type' => 'list',
'values' => array(
'Australia' => 'com.au',
'Brazil' => 'com.br',
'Canada' => 'ca',
'China' => 'cn',
'France' => 'fr',
'Germany' => 'de',
'India' => 'in',
'Italy' => 'it',
'Japan' => 'co.jp',
'Mexico' => 'com.mx',
'Netherlands' => 'nl',
'Spain' => 'es',
'Sweden' => 'se',
'United Kingdom' => 'co.uk',
'United States' => 'com',
),
'defaultValue' => 'com',
),
));
const PARAMETERS = [
[
'asin' => [
'name' => 'ASIN',
'required' => true,
'exampleValue' => 'B071GB1VMQ',
// https://stackoverflow.com/a/12827734
'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)',
],
'tld' => [
'name' => 'Country',
'type' => 'list',
'values' => [
'Australia' => 'com.au',
'Brazil' => 'com.br',
'Canada' => 'ca',
'China' => 'cn',
'France' => 'fr',
'Germany' => 'de',
'India' => 'in',
'Italy' => 'it',
'Japan' => 'co.jp',
'Mexico' => 'com.mx',
'Netherlands' => 'nl',
'Poland' => 'pl',
'Spain' => 'es',
'Sweden' => 'se',
'Turkey' => 'com.tr',
'United Kingdom' => 'co.uk',
'United States' => 'com',
],
'defaultValue' => 'com',
],
]];
const PRICE_SELECTORS = array(
'#priceblock_ourprice',
'.priceBlockBuyingPriceString',
'#newBuyBoxPrice',
'#tp_price_block_total_price_ww',
'span.offer-price',
'.a-color-price',
);
const PRICE_SELECTORS = [
'#priceblock_ourprice',
'.priceBlockBuyingPriceString',
'#newBuyBoxPrice',
'#tp_price_block_total_price_ww',
'span.offer-price',
'.a-color-price',
];
const WHITESPACE = " \t\n\r\0\x0B\xC2\xA0";
const WHITESPACE = " \t\n\r\0\x0B\xC2\xA0";
protected $title;
protected $title;
/**
* Generates domain name given a amazon TLD
*/
private function getDomainName() {
return 'https://www.amazon.' . $this->getInput('tld');
}
/**
* Generates domain name given a amazon TLD
*/
private function getDomainName()
{
return 'https://www.amazon.' . $this->getInput('tld');
}
/**
* Generates URI for a Amazon product page
*/
public function getURI() {
if (!is_null($this->getInput('asin'))) {
return $this->getDomainName() . '/dp/' . $this->getInput('asin');
}
return parent::getURI();
}
/**
* Generates URI for a Amazon product page
*/
public function getURI()
{
if (!is_null($this->getInput('asin'))) {
return $this->getDomainName() . '/dp/' . $this->getInput('asin');
}
return parent::getURI();
}
/**
* Scrapes the product title from the html page
* returns the default title if scraping fails
*/
private function getTitle($html) {
$titleTag = $html->find('#productTitle', 0);
/**
* Scrapes the product title from the html page
* returns the default title if scraping fails
*/
private function getTitle($html)
{
$titleTag = $html->find('#productTitle', 0);
if (!$titleTag) {
return $this->getDefaultTitle();
} else {
return trim(html_entity_decode($titleTag->innertext, ENT_QUOTES));
}
}
if (!$titleTag) {
return $this->getDefaultTitle();
} else {
return trim(html_entity_decode($titleTag->innertext, ENT_QUOTES));
}
}
/**
* Title used by the feed if none could be found
*/
private function getDefaultTitle() {
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin');
}
/**
* Title used by the feed if none could be found
*/
private function getDefaultTitle()
{
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin');
}
/**
* Returns name for the feed
* Uses title (already scraped) if it has one
*/
public function getName() {
if (isset($this->title)) {
return $this->title;
} else {
return parent::getName();
}
}
/**
* Returns name for the feed
* Uses title (already scraped) if it has one
*/
public function getName()
{
if (isset($this->title)) {
return $this->title;
} else {
return parent::getName();
}
}
private function parseDynamicImage($attribute) {
$json = json_decode(html_entity_decode($attribute), true);
private function parseDynamicImage($attribute)
{
$json = json_decode(html_entity_decode($attribute), true);
if ($json and count($json) > 0) {
return array_keys($json)[0];
}
}
if ($json and count($json) > 0) {
return array_keys($json)[0];
}
}
/**
* Returns a generated image tag for the product
*/
private function getImage($html) {
$imageSrc = $html->find('#main-image-container img', 0);
/**
* Returns a generated image tag for the product
*/
private function getImage($html)
{
$imageSrc = $html->find('#main-image-container img', 0);
if ($imageSrc) {
$hiresImage = $imageSrc->getAttribute('data-old-hires');
$dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image');
$image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute);
}
$image = $image ?: 'https://placekitten.com/200/300';
if ($imageSrc) {
$hiresImage = $imageSrc->getAttribute('data-old-hires');
$dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image');
$image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute);
}
$image = $image ?: 'https://placekitten.com/200/300';
return <<<EOT
return <<<EOT
<img width="300" style="max-width:300;max-height:300" src="$image" alt="{$this->title}" />
EOT;
}
}
/**
* Return \simple_html_dom object
* for the entire html of the product page
*/
private function getHtml() {
$uri = $this->getURI();
/**
* Return \simple_html_dom object
* for the entire html of the product page
*/
private function getHtml()
{
$uri = $this->getURI();
return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.');
}
return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.');
}
private function scrapePriceFromMetrics($html) {
$asinData = $html->find('#cerberus-data-metrics', 0);
private function scrapePriceFromMetrics($html)
{
$asinData = $html->find('#cerberus-data-metrics', 0);
// <div id="cerberus-data-metrics" style="display: none;"
// data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
// data-asin-currency-code="USD" data-substitute-count="-1" ... />
if ($asinData) {
return array(
'price' => $asinData->getAttribute('data-asin-price'),
'currency' => $asinData->getAttribute('data-asin-currency-code'),
'shipping' => $asinData->getAttribute('data-asin-shipping')
);
}
// <div id="cerberus-data-metrics" style="display: none;"
// data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
// data-asin-currency-code="USD" data-substitute-count="-1" ... />
if ($asinData) {
return [
'price' => $asinData->getAttribute('data-asin-price'),
'currency' => $asinData->getAttribute('data-asin-currency-code'),
'shipping' => $asinData->getAttribute('data-asin-shipping')
];
}
return false;
}
return false;
}
private function scrapePriceTwister($html) {
$str = $html->find('.twister-plus-buying-options-price-data', 0);
private function scrapePriceTwister($html)
{
$str = $html->find('.twister-plus-buying-options-price-data', 0);
$data = json_decode($str->innertext, true);
if(count($data) === 1) {
$data = $data[0];
return array(
'displayPrice' => $data['displayPrice'],
'currency' => $data['currency'],
'shipping' => '0',
);
}
$data = json_decode($str->innertext, true);
if (count($data) === 1) {
$data = $data[0];
return [
'displayPrice' => $data['displayPrice'],
'currency' => $data['currency'],
'shipping' => '0',
];
}
return false;
}
return false;
}
private function scrapePriceGeneric($html) {
$priceDiv = null;
private function scrapePriceGeneric($html)
{
$default = [
'price' => null,
'displayPrice' => null,
'currency' => null,
'shipping' => null,
];
$priceDiv = null;
foreach(self::PRICE_SELECTORS as $sel) {
$priceDiv = $html->find($sel, 0);
if ($priceDiv) {
break;
}
}
foreach (self::PRICE_SELECTORS as $sel) {
$priceDiv = $html->find($sel, 0);
if ($priceDiv) {
break;
}
}
if (!$priceDiv) {
return false;
}
if (!$priceDiv) {
return $default;
}
$priceString = str_replace(str_split(self::WHITESPACE), '', $priceDiv->plaintext);
preg_match('/(\d+\.\d{0,2})/', $priceString, $matches);
$priceString = str_replace(str_split(self::WHITESPACE), '', $priceDiv->plaintext);
preg_match('/(\d+\.\d{0,2})/', $priceString, $matches);
$price = $matches[0];
$currency = str_replace($price, '', $priceString);
$price = $matches[0] ?? null;
$currency = str_replace($price, '', $priceString);
if ($price != null && $currency != null) {
return array(
'price' => $price,
'currency' => $currency,
'shipping' => '0'
);
}
if ($price != null && $currency != null) {
return [
'price' => $price,
'displayPrice' => null,
'currency' => $currency,
'shipping' => '0'
];
}
return $default;
}
return false;
}
public function collectData()
{
$html = $this->getHtml();
$this->title = $this->getTitle($html);
$image = $this->getImage($html);
$data = $this->scrapePriceGeneric($html);
private function renderContent($image, $data) {
$price = $data['displayPrice'];
if (!$price) {
$price = "{$data['price']} {$data['currency']}";
}
// render
$content = '';
$price = $data['displayPrice'];
if (!$price) {
$price = sprintf('%s %s', $data['price'], $data['currency']);
}
$content .= sprintf('%s<br>Price: %s', $image, $price);
if ($data['shipping'] !== '0') {
$content .= sprintf('<br>Shipping: %s %s</br>', $data['shipping'], $data['currency']);
}
$html = "$image<br>Price: $price";
$item = [
'title' => $this->title,
'uri' => $this->getURI(),
'content' => $content,
// This is to ensure that feed readers notice the price change
'uid' => md5($data['price'])
];
if ($data['shipping'] !== '0') {
$html .= "<br>Shipping: {$data['shipping']} {$data['currency']}</br>";
}
return $html;
}
/**
* Scrape method for Amazon product page
* @return [type] [description]
*/
public function collectData() {
$html = $this->getHtml();
$this->title = $this->getTitle($html);
$imageTag = $this->getImage($html);
$data = $this->scrapePriceGeneric($html);
$item = array(
'title' => $this->title,
'uri' => $this->getURI(),
'content' => $this->renderContent($imageTag, $data),
// This is to ensure that feed readers notice the price change
'uid' => md5($data['price'])
);
$this->items[] = $item;
}
$this->items[] = $item;
}
}

View File

@@ -1,217 +1,218 @@
<?php
class AnidexBridge extends BridgeAbstract {
const MAINTAINER = 'ORelio';
const NAME = 'Anidex';
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(
'id' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All categories' => '0',
'Anime' => '1,2,3',
'Anime - Sub' => '1',
'Anime - Raw' => '2',
'Anime - Dub' => '3',
'Live Action' => '4,5',
'Live Action - Sub' => '4',
'Live Action - Raw' => '5',
'Light Novel' => '6',
'Manga' => '7,8',
'Manga - Translated' => '7',
'Manga - Raw' => '8',
'Music' => '9,10,11',
'Music - Lossy' => '9',
'Music - Lossless' => '10',
'Music - Video' => '11',
'Games' => '12',
'Applications' => '13',
'Pictures' => '14',
'Adult Video' => '15',
'Other' => '16'
)
),
'lang_id' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'All languages' => '0',
'English' => '1',
'Japanese' => '2',
'Polish' => '3',
'Serbo-Croatian' => '4',
'Dutch' => '5',
'Italian' => '6',
'Russian' => '7',
'German' => '8',
'Hungarian' => '9',
'French' => '10',
'Finnish' => '11',
'Vietnamese' => '12',
'Greek' => '13',
'Bulgarian' => '14',
'Spanish (Spain)' => '15',
'Portuguese (Brazil)' => '16',
'Portuguese (Portugal)' => '17',
'Swedish' => '18',
'Arabic' => '19',
'Danish' => '20',
'Chinese (Simplified)' => '21',
'Bengali' => '22',
'Romanian' => '23',
'Czech' => '24',
'Mongolian' => '25',
'Turkish' => '26',
'Indonesian' => '27',
'Korean' => '28',
'Spanish (LATAM)' => '29',
'Persian' => '30',
'Malaysian' => '31'
)
),
'group_id' => array(
'name' => 'Group ID',
'type' => 'number'
),
'r' => array(
'name' => 'Hide Remakes',
'type' => 'checkbox'
),
'b' => array(
'name' => 'Only Batches',
'type' => 'checkbox'
),
'a' => array(
'name' => 'Only Authorized',
'type' => 'checkbox'
),
'q' => array(
'name' => 'Keyword',
'description' => 'Keyword(s)',
'type' => 'text'
),
'h' => array(
'name' => 'Adult content',
'type' => 'list',
'values' => array(
'No filter' => '0',
'Hide +18' => '1',
'Only +18' => '2'
)
)
)
);
class AnidexBridge extends BridgeAbstract
{
const MAINTAINER = 'ORelio';
const NAME = 'Anidex';
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 = [
[
'id' => [
'name' => 'Category',
'type' => 'list',
'values' => [
'All categories' => '0',
'Anime' => '1,2,3',
'Anime - Sub' => '1',
'Anime - Raw' => '2',
'Anime - Dub' => '3',
'Live Action' => '4,5',
'Live Action - Sub' => '4',
'Live Action - Raw' => '5',
'Light Novel' => '6',
'Manga' => '7,8',
'Manga - Translated' => '7',
'Manga - Raw' => '8',
'Music' => '9,10,11',
'Music - Lossy' => '9',
'Music - Lossless' => '10',
'Music - Video' => '11',
'Games' => '12',
'Applications' => '13',
'Pictures' => '14',
'Adult Video' => '15',
'Other' => '16'
]
],
'lang_id' => [
'name' => 'Language',
'type' => 'list',
'values' => [
'All languages' => '0',
'English' => '1',
'Japanese' => '2',
'Polish' => '3',
'Serbo-Croatian' => '4',
'Dutch' => '5',
'Italian' => '6',
'Russian' => '7',
'German' => '8',
'Hungarian' => '9',
'French' => '10',
'Finnish' => '11',
'Vietnamese' => '12',
'Greek' => '13',
'Bulgarian' => '14',
'Spanish (Spain)' => '15',
'Portuguese (Brazil)' => '16',
'Portuguese (Portugal)' => '17',
'Swedish' => '18',
'Arabic' => '19',
'Danish' => '20',
'Chinese (Simplified)' => '21',
'Bengali' => '22',
'Romanian' => '23',
'Czech' => '24',
'Mongolian' => '25',
'Turkish' => '26',
'Indonesian' => '27',
'Korean' => '28',
'Spanish (LATAM)' => '29',
'Persian' => '30',
'Malaysian' => '31'
]
],
'group_id' => [
'name' => 'Group ID',
'type' => 'number'
],
'r' => [
'name' => 'Hide Remakes',
'type' => 'checkbox'
],
'b' => [
'name' => 'Only Batches',
'type' => 'checkbox'
],
'a' => [
'name' => 'Only Authorized',
'type' => 'checkbox'
],
'q' => [
'name' => 'Keyword',
'description' => 'Keyword(s)',
'type' => 'text'
],
'h' => [
'name' => 'Adult content',
'type' => 'list',
'values' => [
'No filter' => '0',
'Hide +18' => '1',
'Only +18' => '2'
]
]
]
];
public function collectData() {
public function collectData()
{
// Build Search URL from user-provided parameters
$search_url = self::ALTERNATE_URI . '?s=upload_timestamp&o=desc';
foreach (['id', 'lang_id', 'group_id'] as $param_name) {
$param = $this->getInput($param_name);
if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) {
$search_url .= '&' . $param_name . '=' . $param;
}
}
foreach (['r', 'b', 'a'] as $param_name) {
$param = $this->getInput($param_name);
if (!empty($param) && boolval($param)) {
$search_url .= '&' . $param_name . '=1';
}
}
$query = $this->getInput('q');
if (!empty($query)) {
$search_url .= '&q=' . urlencode($query);
}
$opt = [];
$h = $this->getInput('h');
if (!empty($h) && intval($h) != 0 && ctype_digit($h)) {
$opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h;
}
// Build Search URL from user-provided parameters
$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))) {
$search_url .= '&' . $param_name . '=' . $param;
}
}
foreach (array('r', 'b', 'a') as $param_name) {
$param = $this->getInput($param_name);
if (!empty($param) && boolval($param)) {
$search_url .= '&' . $param_name . '=1';
}
}
$query = $this->getInput('q');
if (!empty($query)) {
$search_url .= '&q=' . urlencode($query);
}
$opt = array();
$h = $this->getInput('h');
if (!empty($h) && intval($h) != 0 && ctype_digit($h)) {
$opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h;
}
// We need to use a different Host HTTP header to reach the correct page on ALTERNATE_URI
$headers = ['Host: ' . self::ALTERNATE_HOST];
// 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;
// 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, $headers, $opt);
$links = $html->find('a');
$results = [];
foreach ($links as $link) {
if (strpos($link->href, '/torrent/') === 0 && !in_array($link->href, $results)) {
$results[] = $link->href;
}
}
if (empty($results) && empty($this->getInput('q'))) {
returnServerError('No results from Anidex: ' . $search_url);
}
// Retrieve torrent listing from search results, which does not contain torrent description
$html = getSimpleHTMLDOM($search_url, $headers, $opt);
$links = $html->find('a');
$results = array();
foreach ($links as $link)
if (strpos($link->href, '/torrent/') === 0 && !in_array($link->href, $results))
$results[] = $link->href;
if (empty($results) && empty($this->getInput('q')))
returnServerError('No results from Anidex: ' . $search_url);
//Process each item individually
foreach ($results as $element) {
//Limit total amount of requests
if (count($this->items) >= 20) {
break;
}
//Process each item individually
foreach ($results as $element) {
$torrent_id = str_replace('/torrent/', '', $element);
//Limit total amount of requests
if(count($this->items) >= 20) {
break;
}
//Ignore entries without valid torrent ID
if ($torrent_id != 0 && ctype_digit($torrent_id)) {
//Retrieve data for this torrent ID
$item_browse_uri = self::URI . 'torrent/' . $torrent_id;
$item_fetch_uri = self::ALTERNATE_URI . 'torrent/' . $torrent_id;
$torrent_id = str_replace('/torrent/', '', $element);
//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);
$item_desc = $item_html->find('div.panel-body', 0);
$item_author = trim($item_html->find('span.fa-user', 0)->parent()->plaintext);
$item_date = strtotime(trim($item_html->find('span.fa-clock', 0)->parent()->plaintext));
$item_image = $this->getURI() . 'images/user_logos/default.png';
//Ignore entries without valid torrent ID
if ($torrent_id != 0 && ctype_digit($torrent_id)) {
//Check for description-less torrent andn optionally extract image
$desc_title_found = false;
foreach ($item_html->find('h3.panel-title') as $h3) {
if (strpos($h3, 'Description') !== false) {
$desc_title_found = true;
break;
}
}
if ($desc_title_found) {
//Retrieve image for thumbnail or generic logo fallback
foreach ($item_desc->find('img') as $img) {
if (strpos($img->src, 'prez') === false) {
$item_image = $img->src;
break;
}
}
$item_desc = trim($item_desc->innertext);
} else {
$item_desc = '<em>No description.</em>';
}
//Retrieve data for this 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 (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);
$item_desc = $item_html->find('div.panel-body', 0);
$item_author = trim($item_html->find('span.fa-user', 0)->parent()->plaintext);
$item_date = strtotime(trim($item_html->find('span.fa-clock', 0)->parent()->plaintext));
$item_image = $this->getURI() . 'images/user_logos/default.png';
//Check for description-less torrent andn optionally extract image
$desc_title_found = false;
foreach ($item_html->find('h3.panel-title') as $h3) {
if (strpos($h3, 'Description') !== false) {
$desc_title_found = true;
break;
}
}
if ($desc_title_found) {
//Retrieve image for thumbnail or generic logo fallback
foreach ($item_desc->find('img') as $img) {
if (strpos($img->src, 'prez') === false) {
$item_image = $img->src;
break;
}
}
$item_desc = trim($item_desc->innertext);
} else {
$item_desc = '<em>No description.</em>';
}
//Build and add final item
$item = array();
$item['uri'] = $item_browse_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;
}
}
$element = null;
}
$results = null;
}
//Build and add final item
$item = [];
$item['uri'] = $item_browse_uri;
$item['title'] = $item_title;
$item['author'] = $item_author;
$item['timestamp'] = $item_date;
$item['enclosures'] = [$item_image];
$item['content'] = $item_desc;
$this->items[] = $item;
}
}
$element = null;
}
$results = null;
}
}

View File

@@ -1,141 +1,133 @@
<?php
class AnimeUltimeBridge extends BridgeAbstract {
const MAINTAINER = 'ORelio';
const NAME = 'Anime-Ultime';
const URI = 'http://www.anime-ultime.net/';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the newest releases posted on Anime-Ultime.';
const PARAMETERS = array( array(
'type' => array(
'name' => 'Type',
'type' => 'list',
'values' => array(
'Everything' => '',
'Anime' => 'A',
'Drama' => 'D',
'Tokusatsu' => 'T'
)
)
));
class AnimeUltimeBridge extends BridgeAbstract
{
const MAINTAINER = 'ORelio';
const NAME = 'Anime-Ultime';
const URI = 'http://www.anime-ultime.net/';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the newest releases posted on Anime-Ultime.';
const PARAMETERS = [ [
'type' => [
'name' => 'Type',
'type' => 'list',
'values' => [
'Everything' => '',
'Anime' => 'A',
'Drama' => 'D',
'Tokusatsu' => 'T'
]
]
]];
private $filter = 'Releases';
private $filter = 'Releases';
public function collectData(){
public function collectData()
{
//Add type filter if provided
$typeFilter = $this->getKey('type');
//Add type filter if provided
$typeFilter = array_search(
$this->getInput('type'),
self::PARAMETERS[$this->queriedContext]['type']['values']
);
//Build date and filters for making requests
$thismonth = date('mY') . $typeFilter;
$lastmonth = date('mY', mktime(0, 0, 0, date('n') - 1, 1, date('Y'))) . $typeFilter;
//Build date and filters for making requests
$thismonth = date('mY') . $typeFilter;
$lastmonth = date('mY', mktime(0, 0, 0, date('n') - 1, 1, date('Y'))) . $typeFilter;
//Process each HTML page until having 10 releases
$processedOK = 0;
foreach ([$thismonth, $lastmonth] as $requestFilter) {
$url = self::URI . 'history-0-1/' . $requestFilter;
$html = getContents($url);
// Convert html from iso-8859-1 => utf8
$html = utf8_encode($html);
$html = str_get_html($html);
//Process each HTML page until having 10 releases
$processedOK = 0;
foreach (array($thismonth, $lastmonth) as $requestFilter) {
//Relases are sorted by day : process each day individually
foreach ($html->find('div.history', 0)->find('h3') as $daySection) {
//Retrieve day and build date information
$dateString = $daySection->plaintext;
$day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2));
$item_date = strtotime(str_pad($day, 2, '0', STR_PAD_LEFT)
. '-'
. substr($requestFilter, 0, 2)
. '-'
. substr($requestFilter, 2, 4));
$url = self::URI . 'history-0-1/' . $requestFilter;
$html = getContents($url);
// Convert html from iso-8859-1 => utf8
$html = utf8_encode($html);
$html = str_get_html($html);
//<h3>day</h3><br /><table><tr> <-- useful data in table rows
$release = $daySection->next_sibling()->next_sibling()->first_child();
//Relases are sorted by day : process each day individually
foreach($html->find('div.history', 0)->find('h3') as $daySection) {
//Process each release of that day, ignoring first table row: contains table headers
while (!is_null($release = $release->next_sibling())) {
if (count($release->find('td')) > 0) {
//Retrieve metadata from table columns
$item_link_element = $release->find('td', 0)->find('a', 0);
$item_uri = self::URI . $item_link_element->href;
$item_name = html_entity_decode($item_link_element->plaintext);
//Retrieve day and build date information
$dateString = $daySection->plaintext;
$day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2));
$item_date = strtotime(str_pad($day, 2, '0', STR_PAD_LEFT)
. '-'
. substr($requestFilter, 0, 2)
. '-'
. substr($requestFilter, 2, 4));
$item_image = self::URI . substr(
$item_link_element->onmouseover,
37,
strpos($item_link_element->onmouseover, ' ', 37) - 37
);
//<h3>day</h3><br /><table><tr> <-- useful data in table rows
$release = $daySection->next_sibling()->next_sibling()->first_child();
$item_episode = html_entity_decode(
str_pad(
$release->find('td', 1)->plaintext,
2,
'0',
STR_PAD_LEFT
)
);
//Process each release of that day, ignoring first table row: contains table headers
while(!is_null($release = $release->next_sibling())) {
if(count($release->find('td')) > 0) {
$item_fansub = $release->find('td', 2)->plaintext;
$item_type = $release->find('td', 4)->plaintext;
//Retrieve metadata from table columns
$item_link_element = $release->find('td', 0)->find('a', 0);
$item_uri = self::URI . $item_link_element->href;
$item_name = html_entity_decode($item_link_element->plaintext);
if (!empty($item_uri)) {
// Retrieve description from description page
$html_item = getContents($item_uri);
// Convert html from iso-8859-1 => utf8
$html_item = utf8_encode($html_item);
$item_description = substr(
$html_item,
strpos($html_item, 'class="principal_contain" align="center">') + 41
);
$item_description = substr(
$item_description,
0,
strpos($item_description, '<div id="table">')
);
$item_image = self::URI . substr(
$item_link_element->onmouseover,
37,
strpos($item_link_element->onmouseover, ' ', 37) - 37
);
// Convert relative image src into absolute image src, remove line breaks
$item_description = defaultLinkTo($item_description, self::URI);
$item_description = str_replace("\r", '', $item_description);
$item_description = str_replace("\n", '', $item_description);
$item_episode = html_entity_decode(
str_pad(
$release->find('td', 1)->plaintext,
2,
'0',
STR_PAD_LEFT
)
);
//Build and add final item
$item = [];
$item['uri'] = $item_uri;
$item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode;
$item['author'] = $item_fansub;
$item['timestamp'] = $item_date;
$item['enclosures'] = [$item_image];
$item['content'] = $item_description;
$this->items[] = $item;
$processedOK++;
$item_fansub = $release->find('td', 2)->plaintext;
$item_type = $release->find('td', 4)->plaintext;
//Stop processing once limit is reached
if ($processedOK >= 10) {
return;
}
}
}
}
}
}
}
if(!empty($item_uri)) {
public function getName()
{
if (!is_null($this->getInput('type'))) {
return 'Latest ' . $this->getKey('type') . ' - Anime-Ultime Bridge';
}
// Retrieve description from description page
$html_item = getContents($item_uri);
// Convert html from iso-8859-1 => utf8
$html_item = utf8_encode($html_item);
$item_description = substr(
$html_item,
strpos($html_item, 'class="principal_contain" align="center">') + 41
);
$item_description = substr($item_description,
0,
strpos($item_description, '<div id="table">')
);
// Convert relative image src into absolute image src, remove line breaks
$item_description = defaultLinkTo($item_description, self::URI);
$item_description = str_replace("\r", '', $item_description);
$item_description = str_replace("\n", '', $item_description);
//Build and add final item
$item = array();
$item['uri'] = $item_uri;
$item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode;
$item['author'] = $item_fansub;
$item['timestamp'] = $item_date;
$item['enclosures'] = array($item_image);
$item['content'] = $item_description;
$this->items[] = $item;
$processedOK++;
//Stop processing once limit is reached
if ($processedOK >= 10)
return;
}
}
}
}
}
}
public function getName() {
if(!is_null($this->getInput('type'))) {
$typeFilter = array_search(
$this->getInput('type'),
self::PARAMETERS[$this->queriedContext]['type']['values']
);
return 'Latest ' . $typeFilter . ' - Anime-Ultime Bridge';
}
return parent::getName();
}
return parent::getName();
}
}

View File

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

View File

@@ -1,55 +1,57 @@
<?php
class AppleMusicBridge extends BridgeAbstract {
const NAME = 'Apple Music';
const URI = 'https://www.apple.com';
const DESCRIPTION = 'Fetches the latest releases from an artist';
const MAINTAINER = 'bockiii';
const PARAMETERS = array(array(
'artist' => array(
'name' => 'Artist ID',
'exampleValue' => '909253',
'required' => true,
),
'limit' => array(
'name' => 'Latest X Releases (max 50)',
'defaultValue' => '10',
'required' => true,
),
));
const CACHE_TIMEOUT = 21600; // 6 hours
class AppleMusicBridge extends BridgeAbstract
{
const NAME = 'Apple Music';
const URI = 'https://www.apple.com';
const DESCRIPTION = 'Fetches the latest releases from an artist';
const MAINTAINER = 'bockiii';
const PARAMETERS = [[
'artist' => [
'name' => 'Artist ID',
'exampleValue' => '909253',
'required' => true,
],
'limit' => [
'name' => 'Latest X Releases (max 50)',
'defaultValue' => '10',
'required' => true,
],
]];
const CACHE_TIMEOUT = 21600; // 6 hours
public function collectData() {
# Limit the amount of releases to 50
if ($this->getInput('limit') > 50) {
$limit = 50;
} else {
$limit = $this->getInput('limit');
}
public function collectData()
{
# Limit the amount of releases to 50
if ($this->getInput('limit') > 50) {
$limit = 50;
} else {
$limit = $this->getInput('limit');
}
$url = 'https://itunes.apple.com/lookup?id='
. $this->getInput('artist')
. '&entity=album&limit='
. $limit .
'&sort=recent';
$html = getSimpleHTMLDOM($url);
$url = 'https://itunes.apple.com/lookup?id='
. $this->getInput('artist')
. '&entity=album&limit='
. $limit .
'&sort=recent';
$html = getSimpleHTMLDOM($url);
$json = json_decode($html);
$json = json_decode($html);
foreach ($json->results as $obj) {
if ($obj->wrapperType === 'collection') {
$this->items[] = array(
'title' => $obj->artistName . ' - ' . $obj->collectionName,
'uri' => $obj->collectionViewUrl,
'timestamp' => $obj->releaseDate,
'enclosures' => $obj->artworkUrl100,
'content' => '<a href=' . $obj->collectionViewUrl
. '><img src="' . $obj->artworkUrl100 . '" /></a><br><br>'
. $obj->artistName . ' - ' . $obj->collectionName
. '<br>'
. $obj->copyright,
);
}
}
}
foreach ($json->results as $obj) {
if ($obj->wrapperType === 'collection') {
$this->items[] = [
'title' => $obj->artistName . ' - ' . $obj->collectionName,
'uri' => $obj->collectionViewUrl,
'timestamp' => $obj->releaseDate,
'enclosures' => $obj->artworkUrl100,
'content' => '<a href=' . $obj->collectionViewUrl
. '><img src="' . $obj->artworkUrl100 . '" /></a><br><br>'
. $obj->artistName . ' - ' . $obj->collectionName
. '<br>'
. $obj->copyright,
];
}
}
}
}

View File

@@ -1,92 +1,101 @@
<?php
class ArtStationBridge extends BridgeAbstract {
const NAME = 'ArtStation';
const URI = 'https://www.artstation.com';
const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
const PARAMETERS = array(
'Search Query' => array(
'q' => array(
'name' => 'Search term',
'required' => true,
'exampleValue' => 'bird'
)
)
);
class ArtStationBridge extends BridgeAbstract
{
const NAME = 'ArtStation';
const URI = 'https://www.artstation.com';
const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
public function getIcon() {
return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico';
}
const PARAMETERS = [
'Search Query' => [
'q' => [
'name' => 'Search term',
'required' => true,
'exampleValue' => 'bird'
]
]
];
public function getName() {
return self::NAME . ': ' . $this->getInput('q');
}
public function getIcon()
{
return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico';
}
private function fetchSearch($searchQuery) {
$data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",';
$data .= '"pro_first":"1","filters":[],"additional_fields":[]}';
public function getName()
{
return self::NAME . ': ' . $this->getInput('q');
}
$header = array(
'Content-Type: application/json',
'Accept: application/json'
);
private function fetchSearch($searchQuery)
{
$data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",';
$data .= '"pro_first":"1","filters":[],"additional_fields":[]}';
$opts = array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true
);
$header = [
'Content-Type: application/json',
'Accept: application/json'
];
$jsonSearchURL = self::URI . '/api/v2/search/projects.json';
$jsonSearchStr = getContents($jsonSearchURL, $header, $opts);
return json_decode($jsonSearchStr);
}
$opts = [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true
];
private function fetchProject($hashID) {
$jsonProjectURL = self::URI . '/projects/' . $hashID . '.json';
$jsonProjectStr = getContents($jsonProjectURL);
return json_decode($jsonProjectStr);
}
$jsonSearchURL = self::URI . '/api/v2/search/projects.json';
$jsonSearchStr = getContents($jsonSearchURL, $header, $opts);
return json_decode($jsonSearchStr);
}
public function collectData() {
$searchTerm = $this->getInput('q');
$jsonQuery = $this->fetchSearch($searchTerm);
private function fetchProject($hashID)
{
$jsonProjectURL = self::URI . '/projects/' . $hashID . '.json';
$jsonProjectStr = getContents($jsonProjectURL);
return json_decode($jsonProjectStr);
}
foreach($jsonQuery->data as $media) {
// get detailed info about media item
$jsonProject = $this->fetchProject($media->hash_id);
public function collectData()
{
$searchTerm = $this->getInput('q');
$jsonQuery = $this->fetchSearch($searchTerm);
// create item
$item = array();
$item['title'] = $media->title;
$item['uri'] = $media->url;
$item['timestamp'] = strtotime($jsonProject->published_at);
$item['author'] = $media->user->full_name;
$item['categories'] = implode(',', $jsonProject->tags);
foreach ($jsonQuery->data as $media) {
// get detailed info about media item
$jsonProject = $this->fetchProject($media->hash_id);
$item['content'] = '<a href="'
. $media->url
. '"><img style="max-width: 100%" src="'
. $jsonProject->cover_url
. '"></a><p>'
. $jsonProject->description
. '</p>';
// create item
$item = [];
$item['title'] = $media->title;
$item['uri'] = $media->url;
$item['timestamp'] = strtotime($jsonProject->published_at);
$item['author'] = $media->user->full_name;
$item['categories'] = implode(',', $jsonProject->tags);
$numAssets = count($jsonProject->assets);
$item['content'] = '<a href="'
. $media->url
. '"><img style="max-width: 100%" src="'
. $jsonProject->cover_url
. '"></a><p>'
. $jsonProject->description
. '</p>';
if ($numAssets > 1)
$item['content'] .= '<p><a href="'
. $media->url
. '">Project contains '
. ($numAssets - 1)
. ' more item(s).</a></p>';
$numAssets = count($jsonProject->assets);
$this->items[] = $item;
if ($numAssets > 1) {
$item['content'] .= '<p><a href="'
. $media->url
. '">Project contains '
. ($numAssets - 1)
. ' more item(s).</a></p>';
}
if (count($this->items) >= 10)
break;
}
}
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
}
}

View File

@@ -1,130 +1,162 @@
<?php
class Arte7Bridge extends BridgeAbstract {
// const MAINTAINER = 'mitsukarenai';
const NAME = 'Arte +7';
const URI = 'https://www.arte.tv/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns newest videos from ARTE +7';
class Arte7Bridge extends BridgeAbstract
{
const NAME = 'Arte +7';
const URI = 'https://www.arte.tv/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns newest videos from ARTE +7';
const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
const PARAMETERS = array(
'global' => [
'video_duration_filter' => [
'name' => 'Exclude short videos',
'type' => 'checkbox',
'title' => 'Exclude videos that are shorter than 3 minutes',
'defaultValue' => false,
],
],
'Category' => array(
'lang' => array(
'type' => 'list',
'name' => 'Language',
'values' => array(
'Français' => 'fr',
'Deutsch' => 'de',
'English' => 'en',
'Español' => 'es',
'Polski' => 'pl',
'Italiano' => 'it'
),
'title' => 'ex. RC-014095 pour https://www.arte.tv/fr/videos/RC-014095/blow-up/',
'exampleValue' => 'RC-014095'
),
'cat' => array(
'type' => 'list',
'name' => 'Category',
'values' => array(
'All videos' => null,
'News & society' => 'ACT',
'Series & fiction' => 'SER',
'Cinema' => 'CIN',
'Culture' => 'ARS',
'Culture pop' => 'CPO',
'Discovery' => 'DEC',
'History' => 'HIST',
'Science' => 'SCI',
'Other' => 'AUT'
)
),
),
'Collection' => array(
'lang' => array(
'type' => 'list',
'name' => 'Language',
'values' => array(
'Français' => 'fr',
'Deutsch' => 'de',
'English' => 'en',
'Español' => 'es',
'Polski' => 'pl',
'Italiano' => 'it'
)
),
'col' => array(
'name' => 'Collection id',
'required' => true,
'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/',
'exampleValue' => 'RC-014095'
)
)
);
const PARAMETERS = [
'global' => [
'sort_by' => [
'type' => 'list',
'name' => 'Sort by',
'required' => false,
'defaultValue' => null,
'values' => [
'Default' => null,
'Video rights start date' => 'videoRightsBegin',
'Video rights end date' => 'videoRightsEnd',
'Brodcast date' => 'broadcastBegin',
'Creation date' => 'creationDate',
'Last modified' => 'lastModified',
'Number of views' => 'views',
'Number of views per period' => 'viewsPeriod',
'Available screens' => 'availableScreens',
'Episode' => 'episode'
],
],
'sort_direction' => [
'type' => 'list',
'name' => 'Sort direction',
'required' => false,
'defaultValue' => 'DESC',
'values' => [
'Ascending' => 'ASC',
'Descending' => 'DESC'
],
],
'exclude_trailers' => [
'name' => 'Exclude trailers',
'type' => 'checkbox',
'required' => false,
'defaultValue' => false
],
],
'Category' => [
'lang' => [
'type' => 'list',
'name' => 'Language',
'values' => [
'Français' => 'fr',
'Deutsch' => 'de',
'English' => 'en',
'Español' => 'es',
'Polski' => 'pl',
'Italiano' => 'it'
],
],
'cat' => [
'type' => 'list',
'name' => 'Category',
'values' => [
'All videos' => null,
'News & society' => 'ACT',
'Series & fiction' => 'SER',
'Cinema' => 'CIN',
'Culture' => 'ARS',
'Culture pop' => 'CPO',
'Discovery' => 'DEC',
'History' => 'HIST',
'Science' => 'SCI',
'Other' => 'AUT'
]
],
],
'Collection' => [
'lang' => [
'type' => 'list',
'name' => 'Language',
'values' => [
'Français' => 'fr',
'Deutsch' => 'de',
'English' => 'en',
'Español' => 'es',
'Polski' => 'pl',
'Italiano' => 'it'
]
],
'col' => [
'name' => 'Collection id',
'required' => true,
'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/',
'exampleValue' => 'RC-014095'
]
]
];
public function collectData(){
$lang = $this->getInput('lang');
switch($this->queriedContext) {
case 'Category':
$category = $this->getInput('cat');
$collectionId = null;
break;
case 'Collection':
$collectionId = $this->getInput('col');
$category = null;
break;
}
public function collectData()
{
switch ($this->queriedContext) {
case 'Category':
$category = $this->getInput('cat');
$collectionId = null;
break;
case 'Collection':
$collectionId = $this->getInput('col');
$category = null;
break;
}
$url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=15&language='
. $lang
. ($category != null ? '&category.code=' . $category : '')
. ($collectionId != null ? '&collections.collectionId=' . $collectionId : '');
$lang = $this->getInput('lang');
$sort_by = $this->getInput('sort_by');
$sort_direction = $this->getInput('sort_direction') == 'ASC' ? '' : '-';
$header = array(
'Authorization: Bearer ' . self::API_TOKEN
);
$url = 'https://api.arte.tv/api/opa/v3/videos?limit=15&language='
. $lang
. ($sort_by != null ? '&sort=' . $sort_direction . $sort_by : '')
. ($category != null ? '&category.code=' . $category : '')
. ($collectionId != null ? '&collections.collectionId=' . $collectionId : '');
$input = getContents($url, $header);
$input_json = json_decode($input, true);
$header = [
'Authorization: Bearer ' . self::API_TOKEN
];
foreach($input_json['videos'] as $element) {
$durationSeconds = $element['durationSeconds'];
$input = getContents($url, $header);
$input_json = json_decode($input, true);
if ($this->getInput('video_duration_filter') && $durationSeconds < 60 * 3) {
continue;
}
foreach ($input_json['videos'] as $element) {
if ($this->getInput('exclude_trailers') && $element['platform'] == 'EXTRAIT') {
continue;
}
$item = array();
$item['uri'] = $element['url'];
$item['id'] = $element['id'];
$durationSeconds = $element['durationSeconds'];
$item['timestamp'] = strtotime($element['videoRightsBegin']);
$item['title'] = $element['title'];
$item = [];
$item['uri'] = $element['url'];
$item['id'] = $element['id'];
if(!empty($element['subtitle']))
$item['title'] = $element['title'] . ' | ' . $element['subtitle'];
$item['timestamp'] = strtotime($element['videoRightsBegin']);
$item['title'] = $element['title'];
$durationMinutes = round((int)$durationSeconds / 60);
$item['content'] = $element['teaserText']
. '<br><br>'
. $durationMinutes
. 'min<br><a href="'
. $item['uri']
. '"><img src="'
. $element['mainImage']['url']
. '" /></a>';
if (!empty($element['subtitle'])) {
$item['title'] = $element['title'] . ' | ' . $element['subtitle'];
}
$this->items[] = $item;
}
}
$durationMinutes = round((int)$durationSeconds / 60);
$item['content'] = $element['teaserText']
. '<br><br>'
. $durationMinutes
. 'min<br><a href="'
. $item['uri']
. '"><img src="'
. $element['mainImage']['url']
. '" /></a>';
$this->items[] = $item;
}
}
}

View File

@@ -1,71 +1,81 @@
<?php
class AsahiShimbunAJWBridge extends BridgeAbstract {
const NAME = 'Asahi Shimbun AJW';
const BASE_URI = 'http://www.asahi.com';
const URI = self::BASE_URI . '/ajw/';
const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch';
const MAINTAINER = 'somini';
const PARAMETERS = array(
array(
'section' => array(
'type' => 'list',
'name' => 'Section',
'values' => array(
'Japan » Social Affairs' => 'japan/social',
'Japan » People' => 'japan/people',
'Japan » 3/11 Disaster' => 'japan/0311disaster',
'Japan » Sci & Tech' => 'japan/sci_tech',
'Politics' => 'politics',
'Business' => 'business',
'Culture » Style' => 'culture/style',
'Culture » Movies' => 'culture/movies',
'Culture » Manga & Anime' => 'culture/manga_anime',
'Asia » China' => 'asia/china',
'Asia » Korean Peninsula' => 'asia/korean_peninsula',
'Asia » Around Asia' => 'asia/around_asia',
'Opinion » Editorial' => 'opinion/editorial',
'Opinion » Vox Populi' => 'opinion/vox',
),
'defaultValue' => 'politics',
)
)
);
private function getSectionURI($section) {
return self::getURI() . $section . '/';
}
class AsahiShimbunAJWBridge extends BridgeAbstract
{
const NAME = 'Asahi Shimbun AJW';
const BASE_URI = 'http://www.asahi.com';
const URI = self::BASE_URI . '/ajw/';
const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch';
const MAINTAINER = 'somini';
const PARAMETERS = [
[
'section' => [
'type' => 'list',
'name' => 'Section',
'values' => [
'Japan » Social Affairs' => 'japan/social',
'Japan » People' => 'japan/people',
'Japan » 3/11 Disaster' => 'japan/0311disaster',
'Japan » Sci & Tech' => 'japan/sci_tech',
'Politics' => 'politics',
'Business' => 'business',
'Culture » Style' => 'culture/style',
'Culture » Movies' => 'culture/movies',
'Culture » Manga & Anime' => 'culture/manga_anime',
'Asia » China' => 'asia_world/china',
'Asia » Korean Peninsula' => 'asia_world/korean_peninsula',
'Asia » Around Asia' => 'asia_world/around_asia',
'Asia » World' => 'asia_world/world',
'Opinion » Editorial' => 'opinion/editorial',
'Opinion » Vox Populi' => 'opinion/vox',
],
'defaultValue' => 'politics',
]
]
];
public function collectData() {
$html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section')));
private function getSectionURI($section)
{
return $this->getURI() . $section . '/';
}
foreach($html->find('#MainInner li a') as $element) {
if ($element->parent()->class == 'HeadlineTopImage-S') {
Debug::log('Skip Headline, it is repeated below');
continue;
}
$item = array();
public function collectData()
{
$html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section')));
$item['uri'] = self::BASE_URI . $element->href;
$e_lead = $element->find('span.Lead', 0);
if ($e_lead) {
$item['content'] = $e_lead->innertext;
$e_lead->outertext = '';
} else {
$item['content'] = $element->innertext;
}
$e_date = $element->find('span.EnDate', 0);
if ($e_date) {
$item['timestamp'] = strtotime($e_date->innertext);
$e_date->outertext = '';
}
$e_video = $element->find('span.EnVideo', 0);
if ($e_video) {
$e_video->outertext = '';
$element->innertext = "VIDEO: $element->innertext";
}
$item['title'] = $element->innertext;
foreach ($html->find('#MainInner li a') as $element) {
if ($element->parent()->class == 'HeadlineTopImage-S') {
Debug::log('Skip Headline, it is repeated below');
continue;
}
$item = [];
$this->items[] = $item;
}
}
$item['uri'] = self::BASE_URI . $element->href;
$e_lead = $element->find('span.Lead', 0);
if ($e_lead) {
$item['content'] = $e_lead->innertext;
$e_lead->outertext = '';
} else {
$item['content'] = $element->innertext;
}
$e_date = $element->find('span.EnDate', 0);
if ($e_date) {
$item['timestamp'] = strtotime($e_date->innertext);
$e_date->outertext = '';
}
$e_video = $element->find('span.EnVideo', 0);
if ($e_video) {
$e_video->outertext = '';
$element->innertext = "VIDEO: $element->innertext";
}
$e_title = $element->find('.title', 0);
if ($e_title) {
$item['title'] = $e_title->innertext;
} else {
$item['title'] = $element->innertext;
}
$this->items[] = $item;
}
}
}

View File

@@ -1,74 +1,79 @@
<?php
class AskfmBridge extends BridgeAbstract {
const MAINTAINER = 'az5he6ch, logmanoriginal';
const NAME = 'Ask.fm Answers';
const URI = 'https://ask.fm/';
const CACHE_TIMEOUT = 300; //5 min
const DESCRIPTION = 'Returns answers from an Ask.fm user';
const PARAMETERS = array(
'Ask.fm username' => array(
'u' => array(
'name' => 'Username',
'required' => true,
'exampleValue' => 'ApprovedAndReal'
)
)
);
class AskfmBridge extends BridgeAbstract
{
const MAINTAINER = 'az5he6ch, logmanoriginal';
const NAME = 'Ask.fm Answers';
const URI = 'https://ask.fm/';
const CACHE_TIMEOUT = 300; //5 min
const DESCRIPTION = 'Returns answers from an Ask.fm user';
const PARAMETERS = [
'Ask.fm username' => [
'u' => [
'name' => 'Username',
'required' => true,
'exampleValue' => 'ApprovedAndReal'
]
]
];
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI());
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
$html = defaultLinkTo($html, self::URI);
$html = defaultLinkTo($html, self::URI);
foreach($html->find('article.streamItem-answer') as $element) {
$item = array();
$item['uri'] = $element->find('a.streamItem_meta', 0)->href;
$question = trim($element->find('header.streamItem_header', 0)->innertext);
foreach ($html->find('article.streamItem-answer') as $element) {
$item = [];
$item['uri'] = $element->find('a.streamItem_meta', 0)->href;
$question = trim($element->find('header.streamItem_header', 0)->innertext);
$item['title'] = trim(
htmlspecialchars_decode($element->find('header.streamItem_header', 0)->plaintext,
ENT_QUOTES
)
);
$item['title'] = trim(
htmlspecialchars_decode(
$element->find('header.streamItem_header', 0)->plaintext,
ENT_QUOTES
)
);
$item['timestamp'] = strtotime($element->find('time', 0)->datetime);
$item['timestamp'] = strtotime($element->find('time', 0)->datetime);
$answer = trim($element->find('div.streamItem_content', 0)->innertext);
$answer = trim($element->find('div.streamItem_content', 0)->innertext);
// This probably should be cleaned up, especially for YouTube embeds
if($visual = $element->find('div.streamItem_visual', 0)) {
$visual = $visual->innertext;
}
// This probably should be cleaned up, especially for YouTube embeds
if ($visual = $element->find('div.streamItem_visual', 0)) {
$visual = $visual->innertext;
}
// Fix tracking links, also doesn't work
foreach($element->find('a') as $link) {
if(strpos($link->href, 'l.ask.fm') !== false) {
$link->href = $link->plaintext;
}
}
// Fix tracking links, also doesn't work
foreach ($element->find('a') as $link) {
if (strpos($link->href, 'l.ask.fm') !== false) {
$link->href = $link->plaintext;
}
}
$item['content'] = '<p>' . $question
. '</p><p>' . $answer
. '</p><p>' . $visual . '</p>';
$item['content'] = '<p>' . $question
. '</p><p>' . $answer
. '</p><p>' . $visual . '</p>';
$this->items[] = $item;
}
}
$this->items[] = $item;
}
}
public function getName(){
if(!is_null($this->getInput('u'))) {
return self::NAME . ' : ' . $this->getInput('u');
}
public function getName()
{
if (!is_null($this->getInput('u'))) {
return self::NAME . ' : ' . $this->getInput('u');
}
return parent::getName();
}
return parent::getName();
}
public function getURI(){
if(!is_null($this->getInput('u'))) {
return self::URI . urlencode($this->getInput('u'));
}
public function getURI()
{
if (!is_null($this->getInput('u'))) {
return self::URI . urlencode($this->getInput('u'));
}
return parent::getURI();
}
return parent::getURI();
}
}

View File

@@ -1,270 +1,281 @@
<?php
class AssociatedPressNewsBridge extends BridgeAbstract {
const NAME = 'Associated Press News Bridge';
const URI = 'https://apnews.com/';
const DESCRIPTION = 'Returns newest articles by topic';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(
'Standard Topics' => array(
'topic' => array(
'name' => 'Topic',
'type' => 'list',
'values' => array(
'AP Top News' => 'apf-topnews',
'Sports' => 'apf-sports',
'Entertainment' => 'apf-entertainment',
'Oddities' => 'apf-oddities',
'Travel' => 'apf-Travel',
'Technology' => 'apf-technology',
'Lifestyle' => 'apf-lifestyle',
'Business' => 'apf-business',
'U.S. News' => 'apf-usnews',
'Health' => 'apf-Health',
'Science' => 'apf-science',
'World News' => 'apf-WorldNews',
'Politics' => 'apf-politics',
'Religion' => 'apf-religion',
'Photo Galleries' => 'PhotoGalleries',
'Fact Checks' => 'APFactCheck',
'Videos' => 'apf-videos',
),
'defaultValue' => 'apf-topnews',
),
),
'Custom Topic' => array(
'topic' => array(
'name' => 'Topic',
'type' => 'text',
'required' => true,
'exampleValue' => 'europe'
),
)
);
const CACHE_TIMEOUT = 900; // 15 mins
class AssociatedPressNewsBridge extends BridgeAbstract
{
const NAME = 'Associated Press News Bridge';
const URI = 'https://apnews.com/';
const DESCRIPTION = 'Returns newest articles by topic';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = [
'Standard Topics' => [
'topic' => [
'name' => 'Topic',
'type' => 'list',
'values' => [
'AP Top News' => 'apf-topnews',
'Sports' => 'apf-sports',
'Entertainment' => 'apf-entertainment',
'Oddities' => 'apf-oddities',
'Travel' => 'apf-Travel',
'Technology' => 'apf-technology',
'Lifestyle' => 'apf-lifestyle',
'Business' => 'apf-business',
'U.S. News' => 'apf-usnews',
'Health' => 'apf-Health',
'Science' => 'apf-science',
'World News' => 'apf-WorldNews',
'Politics' => 'apf-politics',
'Religion' => 'apf-religion',
'Photo Galleries' => 'PhotoGalleries',
'Fact Checks' => 'APFactCheck',
'Videos' => 'apf-videos',
],
'defaultValue' => 'apf-topnews',
],
],
'Custom Topic' => [
'topic' => [
'name' => 'Topic',
'type' => 'text',
'required' => true,
'exampleValue' => 'europe'
],
]
];
private $detectParamRegex = '/^https?:\/\/(?:www\.)?apnews\.com\/(?:[tag|hub]+\/)?([\w-]+)$/';
private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags=';
private $feedName = '';
const CACHE_TIMEOUT = 900; // 15 mins
public function detectParameters($url) {
$params = array();
private $detectParamRegex = '/^https?:\/\/(?:www\.)?apnews\.com\/(?:[tag|hub]+\/)?([\w-]+)$/';
private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags=';
private $feedName = '';
if(preg_match($this->detectParamRegex, $url, $matches) > 0) {
$params['topic'] = $matches[1];
$params['context'] = 'Custom Topic';
return $params;
}
public function detectParameters($url)
{
$params = [];
return null;
}
if (preg_match($this->detectParamRegex, $url, $matches) > 0) {
$params['topic'] = $matches[1];
$params['context'] = 'Custom Topic';
return $params;
}
public function collectData() {
switch($this->getInput('topic')) {
case 'Podcasts':
returnClientError('Podcasts topic feed is not supported');
break;
case 'PressReleases':
returnClientError('PressReleases topic feed is not supported');
break;
default:
$this->collectCardData();
}
}
return null;
}
public function getURI() {
if (!is_null($this->getInput('topic'))) {
return self::URI . $this->getInput('topic');
}
public function collectData()
{
switch ($this->getInput('topic')) {
case 'Podcasts':
returnClientError('Podcasts topic feed is not supported');
break;
case 'PressReleases':
returnClientError('PressReleases topic feed is not supported');
break;
default:
$this->collectCardData();
}
}
return parent::getURI();
}
public function getURI()
{
if (!is_null($this->getInput('topic'))) {
return self::URI . $this->getInput('topic');
}
public function getName() {
if (!empty($this->feedName)) {
return $this->feedName . ' - Associated Press';
}
return parent::getURI();
}
return parent::getName();
}
public function getName()
{
if (!empty($this->feedName)) {
return $this->feedName . ' - Associated Press';
}
private function getTagURI() {
if (!is_null($this->getInput('topic'))) {
return $this->tagEndpoint . $this->getInput('topic');
}
return parent::getName();
}
return parent::getURI();
}
private function getTagURI()
{
if (!is_null($this->getInput('topic'))) {
return $this->tagEndpoint . $this->getInput('topic');
}
private function collectCardData() {
$json = getContents($this->getTagURI())
or returnServerError('Could not request: ' . $this->getTagURI());
return parent::getURI();
}
$tagContents = json_decode($json, true);
private function collectCardData()
{
$json = getContents($this->getTagURI())
or returnServerError('Could not request: ' . $this->getTagURI());
if (empty($tagContents['tagObjs'])) {
returnClientError('Topic not found: ' . $this->getInput('topic'));
}
$tagContents = json_decode($json, true);
$this->feedName = $tagContents['tagObjs'][0]['name'];
if (empty($tagContents['tagObjs'])) {
returnClientError('Topic not found: ' . $this->getInput('topic'));
}
foreach ($tagContents['cards'] as $card) {
$item = array();
$this->feedName = $tagContents['tagObjs'][0]['name'];
// skip hub peeks & Notifications
if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') {
continue;
}
foreach ($tagContents['cards'] as $card) {
$item = [];
$storyContent = $card['contents'][0];
// skip hub peeks & Notifications
if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') {
continue;
}
switch($storyContent['contentType']) {
case 'web': // Skip link only content
continue 2;
$storyContent = $card['contents'][0];
case 'video':
$html = $this->processVideo($storyContent);
switch ($storyContent['contentType']) {
case 'web': // Skip link only content
continue 2;
$item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/'
. $storyContent['media'][0]['id'] . '/800.jpeg';
break;
default:
if (empty($storyContent['storyHTML'])) { // Skip if no storyHTML
continue 2;
}
case 'video':
$html = $this->processVideo($storyContent);
$html = defaultLinkTo($storyContent['storyHTML'], self::URI);
$html = str_get_html($html);
$item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/'
. $storyContent['media'][0]['id'] . '/800.jpeg';
break;
default:
if (empty($storyContent['storyHTML'])) { // Skip if no storyHTML
continue 2;
}
$this->processMediaPlaceholders($html, $storyContent['id']);
$this->processHubLinks($html, $storyContent);
$this->processIframes($html);
$html = defaultLinkTo($storyContent['storyHTML'], self::URI);
$html = str_get_html($html);
if (!is_null($storyContent['leadPhotoId'])) {
$item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/'
. $storyContent['leadPhotoId'] . '/800.jpeg';
}
}
$this->processMediaPlaceholders($html, $storyContent['id']);
$this->processHubLinks($html, $storyContent);
$this->processIframes($html);
$item['title'] = $card['contents'][0]['headline'];
$item['uri'] = self::URI . $card['shortId'];
if (!is_null($storyContent['leadPhotoId'])) {
$leadPhotoUrl = sprintf('https://storage.googleapis.com/afs-prod/media/%s/800.jpeg', $storyContent['leadPhotoId']);
$leadPhotoImageTag = sprintf('<img src="%s">', $leadPhotoUrl);
// Move the image to the beginning of the content
$html = $leadPhotoImageTag . $html;
// Explicitly not adding it to the item's enclosures!
}
}
if ($card['contents'][0]['localLinkUrl']) {
$item['uri'] = $card['contents'][0]['localLinkUrl'];
}
$item['title'] = $card['contents'][0]['headline'];
$item['uri'] = self::URI . $card['shortId'];
$item['timestamp'] = $storyContent['published'];
if ($card['contents'][0]['localLinkUrl']) {
$item['uri'] = $card['contents'][0]['localLinkUrl'];
}
if (is_null($storyContent['bylines']) === false) {
// Remove 'By' from the bylines
if (substr($storyContent['bylines'], 0, 2) == 'By') {
$item['author'] = ltrim($storyContent['bylines'], 'By ');
} else {
$item['author'] = $storyContent['bylines'];
}
}
$item['timestamp'] = $storyContent['published'];
$item['content'] = $html;
if (is_null($storyContent['bylines']) === false) {
// Remove 'By' from the bylines
if (substr($storyContent['bylines'], 0, 2) == 'By') {
$item['author'] = ltrim($storyContent['bylines'], 'By ');
} else {
$item['author'] = $storyContent['bylines'];
}
}
foreach ($storyContent['tagObjs'] as $tag) {
$item['categories'][] = $tag['name'];
}
$item['content'] = $html;
$this->items[] = $item;
foreach ($storyContent['tagObjs'] as $tag) {
$item['categories'][] = $tag['name'];
}
if (count($this->items) >= 15) {
break;
}
}
}
$this->items[] = $item;
private function processMediaPlaceholders($html, $id) {
if (count($this->items) >= 15) {
break;
}
}
}
if ($html->find('div.media-placeholder', 0)) {
// Fetch page content
$json = getContents('https://afs-prod.appspot.com/api/v2/content/' . $id);
$storyContent = json_decode($json, true);
private function processMediaPlaceholders($html, $id)
{
if ($html->find('div.media-placeholder', 0)) {
// Fetch page content
$json = getContents('https://afs-prod.appspot.com/api/v2/content/' . $id);
$storyContent = json_decode($json, true);
foreach ($html->find('div.media-placeholder') as $div) {
$key = array_search($div->id, $storyContent['mediumIds']);
foreach ($html->find('div.media-placeholder') as $div) {
$key = array_search($div->id, $storyContent['mediumIds']);
if (!isset($storyContent['media'][$key])) {
continue;
}
if (!isset($storyContent['media'][$key])) {
continue;
}
$media = $storyContent['media'][$key];
$media = $storyContent['media'][$key];
if ($media['type'] === 'Photo') {
$mediaUrl = $media['gcsBaseUrl'] . $media['imageRenderedSizes'][0] . $media['imageFileExtension'];
$mediaCaption = $media['caption'];
if ($media['type'] === 'Photo') {
$mediaUrl = $media['gcsBaseUrl'] . $media['imageRenderedSizes'][0] . $media['imageFileExtension'];
$mediaCaption = $media['caption'];
$div->outertext = <<<EOD
$div->outertext = <<<EOD
<figure><img loading="lazy" src="{$mediaUrl}"/><figcaption>{$mediaCaption}</figcaption></figure>
EOD;
}
}
if ($media['type'] === 'YouTube') {
$div->outertext = <<<EOD
if ($media['type'] === 'YouTube') {
$div->outertext = <<<EOD
<iframe src="https://www.youtube.com/embed/{$media['externalId']}" width="560" height="315">
</iframe>
EOD;
}
}
}
}
}
}
}
}
/*
Create full coverage links (HubLinks)
*/
private function processHubLinks($html, $storyContent) {
/*
Create full coverage links (HubLinks)
*/
private function processHubLinks($html, $storyContent)
{
if (!empty($storyContent['richEmbeds'])) {
foreach ($storyContent['richEmbeds'] as $embed) {
if ($embed['type'] === 'Hub Link') {
$url = self::URI . $embed['tag']['id'];
$div = $html->find('div[id=' . $embed['id'] . ']', 0);
if (!empty($storyContent['richEmbeds'])) {
foreach ($storyContent['richEmbeds'] as $embed) {
if ($embed['type'] === 'Hub Link') {
$url = self::URI . $embed['tag']['id'];
$div = $html->find('div[id=' . $embed['id'] . ']', 0);
if ($div) {
$div->outertext = <<<EOD
if ($div) {
$div->outertext = <<<EOD
<p><a href="{$url}">{$embed['calloutText']} {$embed['displayName']}</a></p>
EOD;
}
}
}
}
}
}
}
}
}
}
private function processVideo($storyContent) {
$video = $storyContent['media'][0];
private function processVideo($storyContent)
{
$video = $storyContent['media'][0];
if ($video['type'] === 'YouTube') {
$url = 'https://www.youtube.com/embed/' . $video['externalId'];
$html = <<<EOD
if ($video['type'] === 'YouTube') {
$url = 'https://www.youtube.com/embed/' . $video['externalId'];
$html = <<<EOD
<iframe width="560" height="315" src="{$url}" frameborder="0" allowfullscreen></iframe>
EOD;
} else {
$html = <<<EOD
} else {
$html = <<<EOD
<video controls poster="https://storage.googleapis.com/afs-prod/media/{$video['id']}/800.jpeg" preload="none">
<source src="{$video['gcsBaseUrl']} {$video['videoRenderedSizes'][0]} {$video['videoFileExtension']}" type="video/mp4">
</video>
EOD;
}
}
return $html;
}
return $html;
}
// Remove datawrapper.dwcdn.net iframes and related javaScript
private function processIframes($html) {
// Remove datawrapper.dwcdn.net iframes and related javaScript
private function processIframes($html)
{
foreach ($html->find('iframe') as $index => $iframe) {
if (preg_match('/datawrapper\.dwcdn\.net/', $iframe->src)) {
$iframe->outertext = '';
foreach ($html->find('iframe') as $index => $iframe) {
if (preg_match('/datawrapper\.dwcdn\.net/', $iframe->src)) {
$iframe->outertext = '';
if ($html->find('script', $index)) {
$html->find('script', $index)->outertext = '';
}
}
}
}
if ($html->find('script', $index)) {
$html->find('script', $index)->outertext = '';
}
}
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
class AstrophysicsDataSystemBridge extends BridgeAbstract
{
const NAME = 'SAO/NASA Astrophysics Data System';
const DESCRIPTION = 'Returns the latest publications from a query';
const URI = 'https://ui.adsabs.harvard.edu';
const PARAMETERS = [
'Publications' => [
'query' => [
'name' => 'query',
'title' => 'Same format as the search bar on the website',
'exampleValue' => 'author:"huchra, john"',
'required' => true
]
]];
private $feedTitle;
public function getName()
{
if ($this->queriedContext === 'Publications') {
return $this->feedTitle;
}
return parent::getName();
}
public function getURI()
{
if ($this->queriedContext === 'Publications') {
return self::URI . '/search/?q=' . urlencode($this->getInput('query'));
}
return parent::getURI();
}
public function collectData()
{
$headers = [
'Cookie: core=always;'
];
$html = str_get_html(defaultLinkTo(getContents($this->getURI(), $headers), self::URI));
$this->feedTitle = html_entity_decode($html->find('title', 0)->plaintext);
foreach ($html->find('div.row > ul > li') as $pub) {
$item = [];
$item['title'] = $pub->find('h3.s-results-title', 0)->plaintext;
$item['content'] = $pub->find('div.s-results-links', 0);
$item['uri'] = $pub->find('a.abs-redirect-link', 0)->href;
$item['author'] = rtrim($pub->find('li.article-author', 0)->plaintext, ' ;');
$item['timestamp'] = $pub->find('div[aria-label="date published"]', 0)->plaintext;
$this->items[] = $item;
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,135 +1,135 @@
<?php
class AutoJMBridge extends BridgeAbstract {
class AutoJMBridge extends BridgeAbstract
{
const NAME = 'AutoJM';
const URI = 'https://www.autojm.fr/';
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = [
'Afficher les offres de véhicules disponible sur la recheche AutoJM' => [
'url' => [
'name' => 'URL de la page de recherche',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
'exampleValue' => 'recherche?brands[]=peugeot&ranges[]=peugeot-nouvelle-308-2021-5p'
],
]
];
const CACHE_TIMEOUT = 3600;
const NAME = 'AutoJM';
const URI = 'https://www.autojm.fr/';
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Afficher les offres de véhicules disponible sur la recheche AutoJM' => array(
'url' => array(
'name' => 'URL de la page de recherche',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
'exampleValue' => 'recherche?brands[]=peugeot&ranges[]=peugeot-nouvelle-308-2021-5p'
),
)
);
const CACHE_TIMEOUT = 3600;
public function getIcon()
{
return self::URI . 'favicon.ico';
}
public function getIcon() {
return self::URI . 'favicon.ico';
}
public function getName()
{
switch ($this->queriedContext) {
case 'Afficher les offres de véhicules disponible sur la recheche AutoJM':
return 'AutoJM | Recherche de véhicules';
break;
default:
return parent::getName();
}
}
public function getName() {
switch($this->queriedContext) {
case 'Afficher les offres de véhicules disponible sur la recheche AutoJM':
return 'AutoJM | Recherche de véhicules';
break;
default:
return parent::getName();
}
public function collectData()
{
// Get the number of result for this search
$search_url = self::URI . $this->getInput('url') . '&open=energy&onlyFilters=false';
}
// Set the header 'X-Requested-With' like the website does it
$header = [
'X-Requested-With: XMLHttpRequest'
];
public function collectData() {
// Get the JSON content of the form
$json = getContents($search_url, $header);
// Get the number of result for this search
$search_url = self::URI . $this->getInput('url') . '&open=energy&onlyFilters=false';
// Extract the HTML content from the JSON result
$data = json_decode($json);
// Set the header 'X-Requested-With' like the website does it
$header = array(
'X-Requested-With: XMLHttpRequest'
);
$nb_results = $data->nbResults;
$total_pages = ceil($nb_results / 15);
// Get the JSON content of the form
$json = getContents($search_url, $header);
// Limit the number of page to analyse to 10
for ($page = 1; $page <= $total_pages && $page <= 10; $page++) {
// Get the result the next page
$html = $this->getResults($page);
// Extract the HTML content from the JSON result
$data = json_decode($json);
// Go through every car of the search
$list = $html->find('div[class*=card-car card-car--listing]');
foreach ($list as $car) {
// Get the info about the car offer
$image = $car->find('div[class=card-car__header__img]', 0)->find('img', 0)->src;
// Decode HTML attribute JSON data
$car_data = json_decode(html_entity_decode($car->{'data-layer'}));
$car_model = $car->{'data-title'} . ' ' . $car->{'data-suptitle'};
$availability = $car->find('div[class=card-car__modalites]', 0)->find('div[class=col]', 0)->plaintext;
$warranty = $car->find('div[data-type=WarrantyCard]', 0)->plaintext;
$discount_html = $car->find('div[class=subtext vehicle_reference_element]', 0);
// Check if there is any discount info displayed
if ($discount_html != null) {
$reference_price_value = $discount_html->find('span[data-cfg=vehicle__reference_price]', 0)->plaintext;
$discount_percent_value = $discount_html->find('span[data-cfg=vehicle__discount_percent]', 0)->plaintext;
$reference_price = '<li>Prix de référence : <s>' . $reference_price_value . '</s></li>';
$discount_percent = '<li>Réduction : ' . $discount_percent_value . ' %</li>';
} else {
$reference_price = '';
$discount_percent = '';
}
$price = $car_data->price;
$kilometer = $car->find('span[data-cfg=vehicle__kilometer]', 0)->plaintext;
$energy = $car->find('span[data-cfg=vehicle__energy__label]', 0)->plaintext;
$power = $car->find('span[data-cfg=vehicle__tax_horse_power]', 0)->plaintext;
$seats = $car->find('span[data-cfg=vehicle__seats]', 0)->plaintext;
$doors = $car->find('span[data-cfg=vehicle__door__label]', 0)->plaintext;
$transmission = $car->find('span[data-cfg=vehicle__transmission]', 0)->plaintext;
$loa_html = $car->find('span[data-cfg=vehicle__loa]', 0);
// Check if any LOA price is displayed
if ($loa_html != null) {
$loa_value = $car->find('span[data-cfg=vehicle__loa]', 0)->plaintext;
$loa = '<li>LOA : à partir de ' . $loa_value . ' / mois </li>';
} else {
$loa = '';
}
$nb_results = $data->nbResults;
$total_pages = ceil($nb_results / 15);
// Construct the new item
$item = [];
$item['title'] = $car_model;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
. $car_model . '</p>';
$item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>';
$item['content'] .= '<li>Prix : ' . $price . ' €</li>';
$item['content'] .= $reference_price;
$item['content'] .= $loa;
$item['content'] .= $discount_percent;
$item['content'] .= '<li>Garantie : ' . $warranty . '</li>';
$item['content'] .= '<li>Kilométrage : ' . $kilometer . ' km</li>';
$item['content'] .= '<li>Energie : ' . $energy . '</li>';
$item['content'] .= '<li>Puissance: ' . $power . ' CV Fiscaux</li>';
$item['content'] .= '<li>Nombre de Places : ' . $seats . ' place(s)</li>';
$item['content'] .= '<li>Nombre de portes : ' . $doors . '</li>';
$item['content'] .= '<li>Boite de vitesse : ' . $transmission . '</li></ul>';
$item['uri'] = $car_data->{'uri'};
$item['uid'] = hash('md5', $item['content']);
$this->items[] = $item;
}
}
}
// Limit the number of page to analyse to 10
for($page = 1; $page <= $total_pages && $page <= 10; $page++) {
// Get the result the next page
$html = $this->getResults($page);
private function getResults(int $page)
{
$user_input = $this->getInput('url');
$search_data = preg_replace('#(recherche|recherche/[0-9]{1,10})\?#', 'recherche/' . $page . '?', $user_input);
// Go through every car of the search
$list = $html->find('div[class*=card-car card-car--listing]');
foreach ($list as $car) {
$search_url = self::URI . $search_data . '&open=energy&onlyFilters=false';
// Get the info about the car offer
$image = $car->find('div[class=card-car__header__img]', 0)->find('img', 0)->src;
// Decode HTML attribute JSON data
$car_data = json_decode(html_entity_decode($car->{'data-layer'}));
$car_model = $car->{'data-title'} . ' ' . $car->{'data-suptitle'};
$availability = $car->find('div[class=card-car__modalites]', 0)->find('div[class=col]', 0)->plaintext;
$warranty = $car->find('div[data-type=WarrantyCard]', 0)->plaintext;
$discount_html = $car->find('div[class=subtext vehicle_reference_element]', 0);
// Check if there is any discount info displayed
if ($discount_html != null) {
$reference_price_value = $discount_html->find('span[data-cfg=vehicle__reference_price]', 0)->plaintext;
$discount_percent_value = $discount_html->find('span[data-cfg=vehicle__discount_percent]', 0)->plaintext;
$reference_price = '<li>Prix de référence : <s>' . $reference_price_value . '</s></li>';
$discount_percent = '<li>Réduction : ' . $discount_percent_value . ' %</li>';
} else {
$reference_price = '';
$discount_percent = '';
}
$price = $car_data->price;
$kilometer = $car->find('span[data-cfg=vehicle__kilometer]', 0)->plaintext;
$energy = $car->find('span[data-cfg=vehicle__energy__label]', 0)->plaintext;
$power = $car->find('span[data-cfg=vehicle__tax_horse_power]', 0)->plaintext;
$seats = $car->find('span[data-cfg=vehicle__seats]', 0)->plaintext;
$doors = $car->find('span[data-cfg=vehicle__door__label]', 0)->plaintext;
$transmission = $car->find('span[data-cfg=vehicle__transmission]', 0)->plaintext;
$loa_html = $car->find('span[data-cfg=vehicle__loa]', 0);
// Check if any LOA price is displayed
if($loa_html != null) {
$loa_value = $car->find('span[data-cfg=vehicle__loa]', 0)->plaintext;
$loa = '<li>LOA : à partir de ' . $loa_value . ' / mois </li>';
} else {
$loa = '';
}
// Get the HTML content of the page
$html = getSimpleHTMLDOMCached($search_url);
// Construct the new item
$item = array();
$item['title'] = $car_model;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
. $car_model . '</p>';
$item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>';
$item['content'] .= '<li>Prix : ' . $price . ' €</li>';
$item['content'] .= $reference_price;
$item['content'] .= $loa;
$item['content'] .= $discount_percent;
$item['content'] .= '<li>Garantie : ' . $warranty . '</li>';
$item['content'] .= '<li>Kilométrage : ' . $kilometer . ' km</li>';
$item['content'] .= '<li>Energie : ' . $energy . '</li>';
$item['content'] .= '<li>Puissance: ' . $power . ' CV Fiscaux</li>';
$item['content'] .= '<li>Nombre de Places : ' . $seats . ' place(s)</li>';
$item['content'] .= '<li>Nombre de portes : ' . $doors . '</li>';
$item['content'] .= '<li>Boite de vitesse : ' . $transmission . '</li></ul>';
$item['uri'] = $car_data->{'uri'};
$item['uid'] = hash('md5', $item['content']);
$this->items[] = $item;
}
}
}
private function getResults(int $page)
{
$user_input = $this->getInput('url');
$search_data = preg_replace('#(recherche|recherche/[0-9]{1,10})\?#', 'recherche/' . $page . '?', $user_input);
$search_url = self::URI . $search_data . '&open=energy&onlyFilters=false';
// Get the HTML content of the page
$html = getSimpleHTMLDOMCached($search_url);
return $html;
}
return $html;
}
}

View File

@@ -1,54 +1,59 @@
<?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/';
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;
private $sites = array();
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/';
public function getIcon() {
return 'https://www.awwwards.com/favicon.ico';
}
private $sites = [];
private function fetchSites() {
Debug::log('Fetching all sites');
$sites = getSimpleHTMLDOM(self::SITESURI);
public function getIcon()
{
return 'https://www.awwwards.com/favicon.ico';
}
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;
}
}
private function fetchSites()
{
Debug::log('Fetching all sites');
$sites = getSimpleHTMLDOM(self::SITESURI);
public function collectData() {
$this->fetchSites();
Debug::log('Parsing all JSON data');
foreach ($sites->find('.grid-sites li') as $site) {
$decode = html_entity_decode($site->attr['data-collectable-model-value'], ENT_QUOTES, 'utf-8');
$decode = json_decode($decode, true);
$this->sites[] = $decode;
}
}
Debug::log('Building RSS feed');
foreach($this->sites as $site) {
$item = array();
$item['title'] = $site['title'];
$item['timestamp'] = $site['createdAt'];
$item['categories'] = $site['tags'];
public function collectData()
{
$this->fetchSites();
$item['content'] = '<img src="'
. self::ASSETSURI
. $site['images']['thumbnail']
. '">';
$item['uri'] = self::SITEURI . $site['slug'];
Debug::log('Building RSS feed');
foreach ($this->sites as $site) {
$item = [];
$item['title'] = $site['title'];
$item['timestamp'] = $site['createdAt'];
$item['categories'] = $site['tags'];
$this->items[] = $item;
$item['content'] = '<img src="'
. self::ASSETSURI
. $site['images']['thumbnail']
. '">';
$item['uri'] = self::SITEURI . $site['slug'];
if(count($this->items) >= 10)
break;
}
}
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
}
}

View File

@@ -1,263 +1,269 @@
<?php
class BAEBridge extends BridgeAbstract {
const MAINTAINER = 'couraudt';
const NAME = 'Bourse Aux Equipiers Bridge';
const URI = 'https://www.bourse-aux-equipiers.com';
const DESCRIPTION = 'Returns the newest sailing offers.';
const PARAMETERS = array(
array(
'keyword' => array(
'name' => 'Filtrer par mots clés',
'title' => 'Entrez le mot clé à filtrer ici'
),
'type' => array(
'name' => 'Type de recherche',
'title' => 'Afficher seuleument un certain type d\'annonce',
'type' => 'list',
'values' => array(
'Toutes les annonces' => false,
'Les embarquements' => 'boat',
'Les skippers' => 'skipper',
'Les équipiers' => 'crew'
)
)
)
);
public function collectData() {
$url = $this->getURI();
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
class BAEBridge extends BridgeAbstract
{
const MAINTAINER = 'couraudt';
const NAME = 'Bourse Aux Equipiers Bridge';
const URI = 'https://www.bourse-aux-equipiers.com';
const DESCRIPTION = 'Returns the newest sailing offers.';
const PARAMETERS = [
[
'keyword' => [
'name' => 'Filtrer par mots clés',
'title' => 'Entrez le mot clé à filtrer ici'
],
'type' => [
'name' => 'Type de recherche',
'title' => 'Afficher seuleument un certain type d\'annonce',
'type' => 'list',
'values' => [
'Toutes les annonces' => false,
'Les embarquements' => 'boat',
'Les skippers' => 'skipper',
'Les équipiers' => 'crew'
]
]
]
];
$annonces = $html->find('main article');
foreach ($annonces as $annonce) {
$detail = $annonce->find('footer a', 0);
public function collectData()
{
$url = $this->getURI();
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
$htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href);
if (!$htmlDetail)
continue;
$annonces = $html->find('main article');
foreach ($annonces as $annonce) {
$detail = $annonce->find('footer a', 0);
$item = array();
$htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href);
if (!$htmlDetail) {
continue;
}
$item['title'] = $annonce->find('header h2', 0)->plaintext;
$item['uri'] = parent::getURI() . $detail->href;
$item = [];
$content = $htmlDetail->find('article p', 0)->innertext;
if (!empty($this->getInput('keyword'))) {
$keyword = $this->remove_accents(strtolower($this->getInput('keyword')));
$cleanTitle = $this->remove_accents(strtolower($item['title']));
if (strpos($cleanTitle, $keyword) === false) {
$cleanContent = $this->remove_accents(strtolower($content));
if (strpos($cleanContent, $keyword) === false) {
continue;
}
}
}
$item['title'] = $annonce->find('header h2', 0)->plaintext;
$item['uri'] = parent::getURI() . $detail->href;
$content .= '<hr>';
$content .= $htmlDetail->find('section', 0)->innertext;
$item['content'] = defaultLinkTo($content, parent::getURI());
$image = $htmlDetail->find('#zoom', 0);
if ($image) {
$item['enclosures'] = array(parent::getURI() . $image->getAttribute('src'));
}
$this->items[] = $item;
}
}
$content = $htmlDetail->find('article p', 0)->innertext;
if (!empty($this->getInput('keyword'))) {
$keyword = $this->removeAccents(strtolower($this->getInput('keyword')));
$cleanTitle = $this->removeAccents(strtolower($item['title']));
if (strpos($cleanTitle, $keyword) === false) {
$cleanContent = $this->removeAccents(strtolower($content));
if (strpos($cleanContent, $keyword) === false) {
continue;
}
}
}
public function getURI() {
$uri = parent::getURI();
if (!empty($this->getInput('type'))) {
if ($this->getInput('type') == 'boat') {
$uri .= '/embarquements.html';
} elseif ($this->getInput('type') == 'skipper') {
$uri .= '/skippers.html';
} else {
$uri .= '/equipiers.html';
}
}
$content .= '<hr>';
$content .= $htmlDetail->find('section', 0)->innertext;
$item['content'] = defaultLinkTo($content, parent::getURI());
$image = $htmlDetail->find('#zoom', 0);
if ($image) {
$item['enclosures'] = [parent::getURI() . $image->getAttribute('src')];
}
$this->items[] = $item;
}
}
return $uri;
}
public function getURI()
{
$uri = parent::getURI();
if (!empty($this->getInput('type'))) {
if ($this->getInput('type') == 'boat') {
$uri .= '/embarquements.html';
} elseif ($this->getInput('type') == 'skipper') {
$uri .= '/skippers.html';
} else {
$uri .= '/equipiers.html';
}
}
private function remove_accents($string) {
$chars = array(
// Decompositions for Latin-1 Supplement
'ª' => 'a', 'º' => 'o',
'À' => 'A', 'Á' => 'A',
'Â' => 'A', 'Ã' => 'A',
'Ä' => 'A', 'Å' => 'A',
'Æ' => 'AE', 'Ç' => 'C',
'È' => 'E', 'É' => 'E',
'Ê' => 'E', 'Ë' => 'E',
'Ì' => 'I', 'Í' => 'I',
'Î' => 'I', 'Ï' => 'I',
'Ð' => 'D', 'Ñ' => 'N',
'Ò' => 'O', 'Ó' => 'O',
'Ô' => 'O', 'Õ' => 'O',
'Ö' => 'O', 'Ù' => 'U',
'Ú' => 'U', 'Û' => 'U',
'Ü' => 'U', 'Ý' => 'Y',
'Þ' => 'TH', 'ß' => 's',
'à' => 'a', 'á' => 'a',
'â' => 'a', 'ã' => 'a',
'ä' => 'a', 'å' => 'a',
'æ' => 'ae', 'ç' => 'c',
'è' => 'e', 'é' => 'e',
'ê' => 'e', 'ë' => 'e',
'ì' => 'i', 'í' => 'i',
'î' => 'i', 'ï' => 'i',
'ð' => 'd', 'ñ' => 'n',
'ò' => 'o', 'ó' => 'o',
'ô' => 'o', 'õ' => 'o',
'ö' => 'o', 'ø' => 'o',
'ù' => 'u', 'ú' => 'u',
'û' => 'u', 'ü' => 'u',
'ý' => 'y', 'þ' => 'th',
'ÿ' => 'y', 'Ø' => 'O',
// Decompositions for Latin Extended-A
'Ā' => 'A', 'ā' => 'a',
'Ă' => 'A', 'ă' => 'a',
'Ą' => 'A', 'ą' => 'a',
'Ć' => 'C', 'ć' => 'c',
'Ĉ' => 'C', 'ĉ' => 'c',
'Ċ' => 'C', 'ċ' => 'c',
'Č' => 'C', 'č' => 'c',
'Ď' => 'D', 'ď' => 'd',
'Đ' => 'D', 'đ' => 'd',
'Ē' => 'E', 'ē' => 'e',
'Ĕ' => 'E', 'ĕ' => 'e',
'Ė' => 'E', 'ė' => 'e',
'Ę' => 'E', 'ę' => 'e',
'Ě' => 'E', 'ě' => 'e',
'Ĝ' => 'G', 'ĝ' => 'g',
'Ğ' => 'G', 'ğ' => 'g',
'Ġ' => 'G', 'ġ' => 'g',
'Ģ' => 'G', 'ģ' => 'g',
'Ĥ' => 'H', 'ĥ' => 'h',
'Ħ' => 'H', 'ħ' => 'h',
'Ĩ' => 'I', 'ĩ' => 'i',
'Ī' => 'I', 'ī' => 'i',
'Ĭ' => 'I', 'ĭ' => 'i',
'Į' => 'I', 'į' => 'i',
'İ' => 'I', 'ı' => 'i',
'IJ' => 'IJ', 'ij' => 'ij',
'Ĵ' => 'J', 'ĵ' => 'j',
'Ķ' => 'K', 'ķ' => 'k',
'ĸ' => 'k', 'Ĺ' => 'L',
'ĺ' => 'l', 'Ļ' => 'L',
'ļ' => 'l', 'Ľ' => 'L',
'ľ' => 'l', 'Ŀ' => 'L',
'ŀ' => 'l', 'Ł' => 'L',
'ł' => 'l', 'Ń' => 'N',
'ń' => 'n', 'Ņ' => 'N',
'ņ' => 'n', 'Ň' => 'N',
'ň' => 'n', 'ʼn' => 'n',
'Ŋ' => 'N', 'ŋ' => 'n',
'Ō' => 'O', 'ō' => 'o',
'Ŏ' => 'O', 'ŏ' => 'o',
'Ő' => 'O', 'ő' => 'o',
'Œ' => 'OE', 'œ' => 'oe',
'Ŕ' => 'R', 'ŕ' => 'r',
'Ŗ' => 'R', 'ŗ' => 'r',
'Ř' => 'R', 'ř' => 'r',
'Ś' => 'S', 'ś' => 's',
'Ŝ' => 'S', 'ŝ' => 's',
'Ş' => 'S', 'ş' => 's',
'Š' => 'S', 'š' => 's',
'Ţ' => 'T', 'ţ' => 't',
'Ť' => 'T', 'ť' => 't',
'Ŧ' => 'T', 'ŧ' => 't',
'Ũ' => 'U', 'ũ' => 'u',
'Ū' => 'U', 'ū' => 'u',
'Ŭ' => 'U', 'ŭ' => 'u',
'Ů' => 'U', 'ů' => 'u',
'Ű' => 'U', 'ű' => 'u',
'Ų' => 'U', 'ų' => 'u',
'Ŵ' => 'W', 'ŵ' => 'w',
'Ŷ' => 'Y', 'ŷ' => 'y',
'Ÿ' => 'Y', 'Ź' => 'Z',
'ź' => 'z', 'Ż' => 'Z',
'ż' => 'z', 'Ž' => 'Z',
'ž' => 'z', 'ſ' => 's',
// Decompositions for Latin Extended-B
'Ș' => 'S', 'ș' => 's',
'Ț' => 'T', 'ț' => 't',
// Euro Sign
'€' => 'E',
// GBP (Pound) Sign
'£' => '',
// Vowels with diacritic (Vietnamese)
// unmarked
'Ơ' => 'O', 'ơ' => 'o',
'Ư' => 'U', 'ư' => 'u',
// grave accent
'Ầ' => 'A', 'ầ' => 'a',
'Ằ' => 'A', 'ằ' => 'a',
'Ề' => 'E', 'ề' => 'e',
'Ồ' => 'O', 'ồ' => 'o',
'Ờ' => 'O', 'ờ' => 'o',
'Ừ' => 'U', 'ừ' => 'u',
'Ỳ' => 'Y', 'ỳ' => 'y',
// hook
'Ả' => 'A', 'ả' => 'a',
'Ẩ' => 'A', 'ẩ' => 'a',
'Ẳ' => 'A', 'ẳ' => 'a',
'Ẻ' => 'E', 'ẻ' => 'e',
'Ể' => 'E', 'ể' => 'e',
'Ỉ' => 'I', 'ỉ' => 'i',
'Ỏ' => 'O', 'ỏ' => 'o',
'Ổ' => 'O', 'ổ' => 'o',
'Ở' => 'O', 'ở' => 'o',
'Ủ' => 'U', 'ủ' => 'u',
'Ử' => 'U', 'ử' => 'u',
'Ỷ' => 'Y', 'ỷ' => 'y',
// tilde
'Ẫ' => 'A', 'ẫ' => 'a',
'Ẵ' => 'A', 'ẵ' => 'a',
'Ẽ' => 'E', 'ẽ' => 'e',
'Ễ' => 'E', 'ễ' => 'e',
'Ỗ' => 'O', 'ỗ' => 'o',
'Ỡ' => 'O', 'ỡ' => 'o',
'Ữ' => 'U', 'ữ' => 'u',
'Ỹ' => 'Y', 'ỹ' => 'y',
// acute accent
'Ấ' => 'A', 'ấ' => 'a',
'Ắ' => 'A', 'ắ' => 'a',
'Ế' => 'E', 'ế' => 'e',
'Ố' => 'O', 'ố' => 'o',
'Ớ' => 'O', 'ớ' => 'o',
'Ứ' => 'U', 'ứ' => 'u',
// dot below
'Ạ' => 'A', 'ạ' => 'a',
'Ậ' => 'A', 'ậ' => 'a',
'Ặ' => 'A', 'ặ' => 'a',
'Ẹ' => 'E', 'ẹ' => 'e',
'Ệ' => 'E', 'ệ' => 'e',
'Ị' => 'I', 'ị' => 'i',
'Ọ' => 'O', 'ọ' => 'o',
'Ộ' => 'O', 'ộ' => 'o',
'Ợ' => 'O', 'ợ' => 'o',
'Ụ' => 'U', 'ụ' => 'u',
'Ự' => 'U', 'ự' => 'u',
'Ỵ' => 'Y', 'ỵ' => 'y',
// Vowels with diacritic (Chinese, Hanyu Pinyin)
'ɑ' => 'a',
// macron
'Ǖ' => 'U', 'ǖ' => 'u',
// acute accent
'Ǘ' => 'U', 'ǘ' => 'u',
// caron
'Ǎ' => 'A', 'ǎ' => 'a',
'Ǐ' => 'I', 'ǐ' => 'i',
'Ǒ' => 'O', 'ǒ' => 'o',
'Ǔ' => 'U', 'ǔ' => 'u',
'Ǚ' => 'U', 'ǚ' => 'u',
// grave accent
'Ǜ' => 'U', 'ǜ' => 'u',
);
return $uri;
}
$string = strtr($string, $chars);
private function removeAccents($string)
{
$chars = [
// Decompositions for Latin-1 Supplement
'ª' => 'a', 'º' => 'o',
'À' => 'A', 'Á' => 'A',
'Â' => 'A', 'Ã' => 'A',
'Ä' => 'A', 'Å' => 'A',
'Æ' => 'AE', 'Ç' => 'C',
'È' => 'E', 'É' => 'E',
'Ê' => 'E', 'Ë' => 'E',
'Ì' => 'I', 'Í' => 'I',
'Î' => 'I', 'Ï' => 'I',
'Ð' => 'D', 'Ñ' => 'N',
'Ò' => 'O', 'Ó' => 'O',
'Ô' => 'O', 'Õ' => 'O',
'Ö' => 'O', 'Ù' => 'U',
'Ú' => 'U', 'Û' => 'U',
'Ü' => 'U', 'Ý' => 'Y',
'Þ' => 'TH', 'ß' => 's',
'à' => 'a', 'á' => 'a',
'â' => 'a', 'ã' => 'a',
'ä' => 'a', 'å' => 'a',
'æ' => 'ae', 'ç' => 'c',
'è' => 'e', 'é' => 'e',
'ê' => 'e', 'ë' => 'e',
'ì' => 'i', 'í' => 'i',
'î' => 'i', 'ï' => 'i',
'ð' => 'd', 'ñ' => 'n',
'ò' => 'o', 'ó' => 'o',
'ô' => 'o', 'õ' => 'o',
'ö' => 'o', 'ø' => 'o',
'ù' => 'u', 'ú' => 'u',
'û' => 'u', 'ü' => 'u',
'ý' => 'y', 'þ' => 'th',
'ÿ' => 'y', 'Ø' => 'O',
// Decompositions for Latin Extended-A
'Ā' => 'A', 'ā' => 'a',
'Ă' => 'A', 'ă' => 'a',
'Ą' => 'A', 'ą' => 'a',
'Ć' => 'C', 'ć' => 'c',
'Ĉ' => 'C', 'ĉ' => 'c',
'Ċ' => 'C', 'ċ' => 'c',
'Č' => 'C', 'č' => 'c',
'Ď' => 'D', 'ď' => 'd',
'Đ' => 'D', 'đ' => 'd',
'Ē' => 'E', 'ē' => 'e',
'Ĕ' => 'E', 'ĕ' => 'e',
'Ė' => 'E', 'ė' => 'e',
'Ę' => 'E', 'ę' => 'e',
'Ě' => 'E', 'ě' => 'e',
'Ĝ' => 'G', 'ĝ' => 'g',
'Ğ' => 'G', 'ğ' => 'g',
'Ġ' => 'G', 'ġ' => 'g',
'Ģ' => 'G', 'ģ' => 'g',
'Ĥ' => 'H', 'ĥ' => 'h',
'Ħ' => 'H', 'ħ' => 'h',
'Ĩ' => 'I', 'ĩ' => 'i',
'Ī' => 'I', 'ī' => 'i',
'Ĭ' => 'I', 'ĭ' => 'i',
'Į' => 'I', 'į' => 'i',
'İ' => 'I', 'ı' => 'i',
'IJ' => 'IJ', 'ij' => 'ij',
'Ĵ' => 'J', 'ĵ' => 'j',
'Ķ' => 'K', 'ķ' => 'k',
'ĸ' => 'k', 'Ĺ' => 'L',
'ĺ' => 'l', 'Ļ' => 'L',
'ļ' => 'l', 'Ľ' => 'L',
'ľ' => 'l', 'Ŀ' => 'L',
'ŀ' => 'l', 'Ł' => 'L',
'ł' => 'l', 'Ń' => 'N',
'ń' => 'n', 'Ņ' => 'N',
'ņ' => 'n', 'Ň' => 'N',
'ň' => 'n', 'ʼn' => 'n',
'Ŋ' => 'N', 'ŋ' => 'n',
'Ō' => 'O', 'ō' => 'o',
'Ŏ' => 'O', 'ŏ' => 'o',
'Ő' => 'O', 'ő' => 'o',
'Œ' => 'OE', 'œ' => 'oe',
'Ŕ' => 'R', 'ŕ' => 'r',
'Ŗ' => 'R', 'ŗ' => 'r',
'Ř' => 'R', 'ř' => 'r',
'Ś' => 'S', 'ś' => 's',
'Ŝ' => 'S', 'ŝ' => 's',
'Ş' => 'S', 'ş' => 's',
'Š' => 'S', 'š' => 's',
'Ţ' => 'T', 'ţ' => 't',
'Ť' => 'T', 'ť' => 't',
'Ŧ' => 'T', 'ŧ' => 't',
'Ũ' => 'U', 'ũ' => 'u',
'Ū' => 'U', 'ū' => 'u',
'Ŭ' => 'U', 'ŭ' => 'u',
'Ů' => 'U', 'ů' => 'u',
'Ű' => 'U', 'ű' => 'u',
'Ų' => 'U', 'ų' => 'u',
'Ŵ' => 'W', 'ŵ' => 'w',
'Ŷ' => 'Y', 'ŷ' => 'y',
'Ÿ' => 'Y', 'Ź' => 'Z',
'ź' => 'z', 'Ż' => 'Z',
'ż' => 'z', 'Ž' => 'Z',
'ž' => 'z', 'ſ' => 's',
// Decompositions for Latin Extended-B
'Ș' => 'S', 'ș' => 's',
'Ț' => 'T', 'ț' => 't',
// Euro Sign
'€' => 'E',
// GBP (Pound) Sign
'£' => '',
// Vowels with diacritic (Vietnamese)
// unmarked
'Ơ' => 'O', 'ơ' => 'o',
'Ư' => 'U', 'ư' => 'u',
// grave accent
'Ầ' => 'A', 'ầ' => 'a',
'Ằ' => 'A', 'ằ' => 'a',
'Ề' => 'E', 'ề' => 'e',
'Ồ' => 'O', 'ồ' => 'o',
'Ờ' => 'O', 'ờ' => 'o',
'Ừ' => 'U', 'ừ' => 'u',
'Ỳ' => 'Y', 'ỳ' => 'y',
// hook
'Ả' => 'A', 'ả' => 'a',
'Ẩ' => 'A', 'ẩ' => 'a',
'Ẳ' => 'A', 'ẳ' => 'a',
'Ẻ' => 'E', 'ẻ' => 'e',
'Ể' => 'E', 'ể' => 'e',
'Ỉ' => 'I', 'ỉ' => 'i',
'Ỏ' => 'O', 'ỏ' => 'o',
'Ổ' => 'O', 'ổ' => 'o',
'Ở' => 'O', 'ở' => 'o',
'Ủ' => 'U', 'ủ' => 'u',
'Ử' => 'U', 'ử' => 'u',
'Ỷ' => 'Y', 'ỷ' => 'y',
// tilde
'Ẫ' => 'A', 'ẫ' => 'a',
'Ẵ' => 'A', 'ẵ' => 'a',
'Ẽ' => 'E', 'ẽ' => 'e',
'Ễ' => 'E', 'ễ' => 'e',
'Ỗ' => 'O', 'ỗ' => 'o',
'Ỡ' => 'O', 'ỡ' => 'o',
'Ữ' => 'U', 'ữ' => 'u',
'Ỹ' => 'Y', 'ỹ' => 'y',
// acute accent
'Ấ' => 'A', 'ấ' => 'a',
'Ắ' => 'A', 'ắ' => 'a',
'Ế' => 'E', 'ế' => 'e',
'Ố' => 'O', 'ố' => 'o',
'Ớ' => 'O', 'ớ' => 'o',
'Ứ' => 'U', 'ứ' => 'u',
// dot below
'Ạ' => 'A', 'ạ' => 'a',
'Ậ' => 'A', 'ậ' => 'a',
'Ặ' => 'A', 'ặ' => 'a',
'Ẹ' => 'E', 'ẹ' => 'e',
'Ệ' => 'E', 'ệ' => 'e',
'Ị' => 'I', 'ị' => 'i',
'Ọ' => 'O', 'ọ' => 'o',
'Ộ' => 'O', 'ộ' => 'o',
'Ợ' => 'O', 'ợ' => 'o',
'Ụ' => 'U', 'ụ' => 'u',
'Ự' => 'U', 'ự' => 'u',
'Ỵ' => 'Y', 'ỵ' => 'y',
// Vowels with diacritic (Chinese, Hanyu Pinyin)
'ɑ' => 'a',
// macron
'Ǖ' => 'U', 'ǖ' => 'u',
// acute accent
'Ǘ' => 'U', 'ǘ' => 'u',
// caron
'Ǎ' => 'A', 'ǎ' => 'a',
'Ǐ' => 'I', 'ǐ' => 'i',
'Ǒ' => 'O', 'ǒ' => 'o',
'Ǔ' => 'U', 'ǔ' => 'u',
'Ǚ' => 'U', 'ǚ' => 'u',
// grave accent
'Ǜ' => 'U', 'ǜ' => 'u',
];
return $string;
}
$string = strtr($string, $chars);
return $string;
}
}

View File

@@ -1,432 +1,440 @@
<?php
class BadDragonBridge extends BridgeAbstract {
const NAME = 'Bad Dragon Bridge';
const URI = 'https://bad-dragon.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Returns sales or new clearance items';
const MAINTAINER = 'Roliga';
const PARAMETERS = array(
'Sales' => array(
),
'Clearance' => array(
'ready_made' => array(
'name' => 'Ready Made',
'type' => 'checkbox'
),
'flop' => array(
'name' => 'Flops',
'type' => 'checkbox'
),
'skus' => array(
'name' => 'Products',
'exampleValue' => 'chanceflared, crackers',
'title' => 'Comma separated list of product SKUs'
),
'onesize' => array(
'name' => 'One-Size',
'type' => 'checkbox'
),
'mini' => array(
'name' => 'Mini',
'type' => 'checkbox'
),
'small' => array(
'name' => 'Small',
'type' => 'checkbox'
),
'medium' => array(
'name' => 'Medium',
'type' => 'checkbox'
),
'large' => array(
'name' => 'Large',
'type' => 'checkbox'
),
'extralarge' => array(
'name' => 'Extra Large',
'type' => 'checkbox'
),
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All' => 'all',
'Accessories' => 'accessories',
'Merchandise' => 'merchandise',
'Dildos' => 'insertable',
'Masturbators' => 'penetrable',
'Packers' => 'packer',
'Lil\' Squirts' => 'shooter',
'Lil\' Vibes' => 'vibrator',
'Wearables' => 'wearable'
),
'defaultValue' => 'all',
),
'soft' => array(
'name' => 'Soft Firmness',
'type' => 'checkbox'
),
'med_firm' => array(
'name' => 'Medium Firmness',
'type' => 'checkbox'
),
'firm' => array(
'name' => 'Firm',
'type' => 'checkbox'
),
'split' => array(
'name' => 'Split Firmness',
'type' => 'checkbox'
),
'maxprice' => array(
'name' => 'Max Price',
'type' => 'number',
'required' => true,
'defaultValue' => 300
),
'minprice' => array(
'name' => 'Min Price',
'type' => 'number',
'defaultValue' => 0
),
'cumtube' => array(
'name' => 'Cumtube',
'type' => 'checkbox'
),
'suctionCup' => array(
'name' => 'Suction Cup',
'type' => 'checkbox'
),
'noAccessories' => array(
'name' => 'No Accessories',
'type' => 'checkbox'
)
)
);
/*
* This sets index $strFrom (or $strTo if set) in $outArr to 'on' if
* $inArr[$param] contains $strFrom.
* It is used for translating BD's shop filter URLs into something we can use.
*
* For the query '?type[]=ready_made&type[]=flop' we would have an array like:
* Array (
* [type] => Array (
* [0] => ready_made
* [1] => flop
* )
* )
* which could be translated into:
* Array (
* [ready_made] => on
* [flop] => on
* )
* */
private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null) {
if(isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) {
$outArr[($strTo ?: $strFrom)] = 'on';
}
}
class BadDragonBridge extends BridgeAbstract
{
const NAME = 'Bad Dragon Bridge';
const URI = 'https://bad-dragon.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Returns sales or new clearance items';
const MAINTAINER = 'Roliga';
const PARAMETERS = [
'Sales' => [
],
'Clearance' => [
'ready_made' => [
'name' => 'Ready Made',
'type' => 'checkbox'
],
'flop' => [
'name' => 'Flops',
'type' => 'checkbox'
],
'skus' => [
'name' => 'Products',
'exampleValue' => 'chanceflared, crackers',
'title' => 'Comma separated list of product SKUs'
],
'onesize' => [
'name' => 'One-Size',
'type' => 'checkbox'
],
'mini' => [
'name' => 'Mini',
'type' => 'checkbox'
],
'small' => [
'name' => 'Small',
'type' => 'checkbox'
],
'medium' => [
'name' => 'Medium',
'type' => 'checkbox'
],
'large' => [
'name' => 'Large',
'type' => 'checkbox'
],
'extralarge' => [
'name' => 'Extra Large',
'type' => 'checkbox'
],
'category' => [
'name' => 'Category',
'type' => 'list',
'values' => [
'All' => 'all',
'Accessories' => 'accessories',
'Merchandise' => 'merchandise',
'Dildos' => 'insertable',
'Masturbators' => 'penetrable',
'Packers' => 'packer',
'Lil\' Squirts' => 'shooter',
'Lil\' Vibes' => 'vibrator',
'Wearables' => 'wearable'
],
'defaultValue' => 'all',
],
'soft' => [
'name' => 'Soft Firmness',
'type' => 'checkbox'
],
'med_firm' => [
'name' => 'Medium Firmness',
'type' => 'checkbox'
],
'firm' => [
'name' => 'Firm',
'type' => 'checkbox'
],
'split' => [
'name' => 'Split Firmness',
'type' => 'checkbox'
],
'maxprice' => [
'name' => 'Max Price',
'type' => 'number',
'required' => true,
'defaultValue' => 300
],
'minprice' => [
'name' => 'Min Price',
'type' => 'number',
'defaultValue' => 0
],
'cumtube' => [
'name' => 'Cumtube',
'type' => 'checkbox'
],
'suctionCup' => [
'name' => 'Suction Cup',
'type' => 'checkbox'
],
'noAccessories' => [
'name' => 'No Accessories',
'type' => 'checkbox'
]
]
];
public function detectParameters($url) {
$params = array();
/*
* This sets index $strFrom (or $strTo if set) in $outArr to 'on' if
* $inArr[$param] contains $strFrom.
* It is used for translating BD's shop filter URLs into something we can use.
*
* For the query '?type[]=ready_made&type[]=flop' we would have an array like:
* Array (
* [type] => Array (
* [0] => ready_made
* [1] => flop
* )
* )
* which could be translated into:
* Array (
* [ready_made] => on
* [flop] => on
* )
* */
private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null)
{
if (isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) {
$outArr[($strTo ?: $strFrom)] = 'on';
}
}
// Sale
$regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/';
if(preg_match($regex, $url, $matches) > 0) {
return $params;
}
public function detectParameters($url)
{
$params = [];
// Clearance
$regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/';
if(preg_match($regex, $url, $matches) > 0) {
parse_str(parse_url($url, PHP_URL_QUERY), $urlParams);
// Sale
$regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/';
if (preg_match($regex, $url, $matches) > 0) {
return $params;
}
$this->setParam($urlParams, $params, 'type', 'ready_made');
$this->setParam($urlParams, $params, 'type', 'flop');
// Clearance
$regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/';
if (preg_match($regex, $url, $matches) > 0) {
parse_str(parse_url($url, PHP_URL_QUERY), $urlParams);
if(isset($urlParams['skus'])) {
$skus = array();
foreach($urlParams['skus'] as $sku) {
is_string($sku) && $skus[] = $sku;
is_array($sku) && $skus[] = $sku[0];
}
$params['skus'] = implode(',', $skus);
}
$this->setParam($urlParams, $params, 'type', 'ready_made');
$this->setParam($urlParams, $params, 'type', 'flop');
$this->setParam($urlParams, $params, 'sizes', 'onesize');
$this->setParam($urlParams, $params, 'sizes', 'mini');
$this->setParam($urlParams, $params, 'sizes', 'small');
$this->setParam($urlParams, $params, 'sizes', 'medium');
$this->setParam($urlParams, $params, 'sizes', 'large');
$this->setParam($urlParams, $params, 'sizes', 'extralarge');
if (isset($urlParams['skus'])) {
$skus = [];
foreach ($urlParams['skus'] as $sku) {
is_string($sku) && $skus[] = $sku;
is_array($sku) && $skus[] = $sku[0];
}
$params['skus'] = implode(',', $skus);
}
if(isset($urlParams['category'])) {
$params['category'] = strtolower($urlParams['category']);
} else{
$params['category'] = 'all';
}
$this->setParam($urlParams, $params, 'sizes', 'onesize');
$this->setParam($urlParams, $params, 'sizes', 'mini');
$this->setParam($urlParams, $params, 'sizes', 'small');
$this->setParam($urlParams, $params, 'sizes', 'medium');
$this->setParam($urlParams, $params, 'sizes', 'large');
$this->setParam($urlParams, $params, 'sizes', 'extralarge');
$this->setParam($urlParams, $params, 'firmnessValues', 'soft');
$this->setParam($urlParams, $params, 'firmnessValues', 'medium', 'med_firm');
$this->setParam($urlParams, $params, 'firmnessValues', 'firm');
$this->setParam($urlParams, $params, 'firmnessValues', 'split');
if (isset($urlParams['category'])) {
$params['category'] = strtolower($urlParams['category']);
} else {
$params['category'] = 'all';
}
if(isset($urlParams['price'])) {
isset($urlParams['price']['max'])
&& $params['maxprice'] = $urlParams['price']['max'];
isset($urlParams['price']['min'])
&& $params['minprice'] = $urlParams['price']['min'];
}
$this->setParam($urlParams, $params, 'firmnessValues', 'soft');
$this->setParam($urlParams, $params, 'firmnessValues', 'medium', 'med_firm');
$this->setParam($urlParams, $params, 'firmnessValues', 'firm');
$this->setParam($urlParams, $params, 'firmnessValues', 'split');
isset($urlParams['cumtube'])
&& $urlParams['cumtube'] === '1'
&& $params['cumtube'] = 'on';
isset($urlParams['suctionCup'])
&& $urlParams['suctionCup'] === '1'
&& $params['suctionCup'] = 'on';
isset($urlParams['noAccessories'])
&& $urlParams['noAccessories'] === '1'
&& $params['noAccessories'] = 'on';
if (isset($urlParams['price'])) {
isset($urlParams['price']['max'])
&& $params['maxprice'] = $urlParams['price']['max'];
isset($urlParams['price']['min'])
&& $params['minprice'] = $urlParams['price']['min'];
}
return $params;
}
isset($urlParams['cumtube'])
&& $urlParams['cumtube'] === '1'
&& $params['cumtube'] = 'on';
isset($urlParams['suctionCup'])
&& $urlParams['suctionCup'] === '1'
&& $params['suctionCup'] = 'on';
isset($urlParams['noAccessories'])
&& $urlParams['noAccessories'] === '1'
&& $params['noAccessories'] = 'on';
return null;
}
return $params;
}
public function getName() {
switch($this->queriedContext) {
case 'Sales':
return 'Bad Dragon Sales';
case 'Clearance':
return 'Bad Dragon Clearance Search';
default:
return parent::getName();
}
}
return null;
}
public function getURI() {
switch($this->queriedContext) {
case 'Sales':
return self::URI . 'sales';
case 'Clearance':
return $this->inputToURL();
default:
return parent::getURI();
}
}
public function getName()
{
switch ($this->queriedContext) {
case 'Sales':
return 'Bad Dragon Sales';
case 'Clearance':
return 'Bad Dragon Clearance Search';
default:
return parent::getName();
}
}
public function collectData() {
switch($this->queriedContext) {
case 'Sales':
$sales = json_decode(getContents(self::URI . 'api/sales'));
public function getURI()
{
switch ($this->queriedContext) {
case 'Sales':
return self::URI . 'sales';
case 'Clearance':
return $this->inputToURL();
default:
return parent::getURI();
}
}
foreach($sales as $sale) {
$item = array();
public function collectData()
{
switch ($this->queriedContext) {
case 'Sales':
$sales = json_decode(getContents(self::URI . 'api/sales'));
$item['title'] = $sale->title;
$item['timestamp'] = strtotime($sale->startDate);
foreach ($sales as $sale) {
$item = [];
$item['uri'] = $this->getURI() . '/' . $sale->slug;
$item['title'] = $sale->title;
$item['timestamp'] = strtotime($sale->startDate);
$contentHTML = '<p><img src="' . $sale->image->url . '"></p>';
if(isset($sale->endDate)) {
$contentHTML .= '<p><b>This promotion ends on '
. gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate))
. '</b></p>';
} else {
$contentHTML .= '<p><b>This promotion never ends</b></p>';
}
$ul = false;
$content = json_decode($sale->content);
foreach($content->blocks as $block) {
switch($block->type) {
case 'header-one':
$contentHTML .= '<h1>' . $block->text . '</h1>';
break;
case 'header-two':
$contentHTML .= '<h2>' . $block->text . '</h2>';
break;
case 'header-three':
$contentHTML .= '<h3>' . $block->text . '</h3>';
break;
case 'unordered-list-item':
if(!$ul) {
$contentHTML .= '<ul>';
$ul = true;
}
$contentHTML .= '<li>' . $block->text . '</li>';
break;
default:
if($ul) {
$contentHTML .= '</ul>';
$ul = false;
}
$contentHTML .= '<p>' . $block->text . '</p>';
break;
}
}
$item['content'] = $contentHTML;
$item['uri'] = $this->getURI() . '/' . $sale->slug;
$this->items[] = $item;
}
break;
case 'Clearance':
$toyData = json_decode(getContents($this->inputToURL(true)));
$contentHTML = '<p><img src="' . $sale->image->url . '"></p>';
if (isset($sale->endDate)) {
$contentHTML .= '<p><b>This promotion ends on '
. gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate))
. '</b></p>';
} else {
$contentHTML .= '<p><b>This promotion never ends</b></p>';
}
$ul = false;
$content = json_decode($sale->content);
foreach ($content->blocks as $block) {
switch ($block->type) {
case 'header-one':
$contentHTML .= '<h1>' . $block->text . '</h1>';
break;
case 'header-two':
$contentHTML .= '<h2>' . $block->text . '</h2>';
break;
case 'header-three':
$contentHTML .= '<h3>' . $block->text . '</h3>';
break;
case 'unordered-list-item':
if (!$ul) {
$contentHTML .= '<ul>';
$ul = true;
}
$contentHTML .= '<li>' . $block->text . '</li>';
break;
default:
if ($ul) {
$contentHTML .= '</ul>';
$ul = false;
}
$contentHTML .= '<p>' . $block->text . '</p>';
break;
}
}
$item['content'] = $contentHTML;
$productList = json_decode(getContents(self::URI
. 'api/inventory-toy/product-list'));
$this->items[] = $item;
}
break;
case 'Clearance':
$toyData = json_decode(getContents($this->inputToURL(true)));
foreach($toyData->toys as $toy) {
$item = array();
$productList = json_decode(getContents(self::URI
. 'api/inventory-toy/product-list'));
$item['uri'] = $this->getURI()
. '#'
. $toy->id;
$item['timestamp'] = strtotime($toy->created);
foreach ($toyData->toys as $toy) {
$item = [];
foreach($productList as $product) {
if($product->sku == $toy->sku) {
$item['title'] = $product->name;
break;
}
}
$item['uri'] = $this->getURI()
. '#'
. $toy->id;
$item['timestamp'] = strtotime($toy->created);
// images
$content = '<p>';
foreach($toy->images as $image) {
$content .= '<a href="'
. $image->fullFilename
. '"><img src="'
. $image->thumbFilename
. '" /></a>';
}
// price
$content .= '</p><p><b>Price:</b> $'
. $toy->price
// size
. '<br /><b>Size:</b> '
. $toy->size
// color
. '<br /><b>Color:</b> '
. $toy->color
// features
. '<br /><b>Features:</b> '
. ($toy->suction_cup ? 'Suction cup' : '')
. ($toy->suction_cup && $toy->cumtube ? ', ' : '')
. ($toy->cumtube ? 'Cumtube' : '')
. ($toy->suction_cup || $toy->cumtube ? '' : 'None');
// firmness
$firmnessTexts = array(
'2' => 'Extra soft',
'3' => 'Soft',
'5' => 'Medium',
'8' => 'Firm'
);
$firmnesses = explode('/', $toy->firmness);
if(count($firmnesses) === 2) {
$content .= '<br /><b>Firmness:</b> '
. $firmnessTexts[$firmnesses[0]]
. ', '
. $firmnessTexts[$firmnesses[1]];
} else{
$content .= '<br /><b>Firmness:</b> '
. $firmnessTexts[$firmnesses[0]];
}
// flop
if($toy->type === 'flop') {
$content .= '<br /><b>Flop reason:</b> '
. $toy->flop_reason;
}
$content .= '</p>';
$item['content'] = $content;
foreach ($productList as $product) {
if ($product->sku == $toy->sku) {
$item['title'] = $product->name;
break;
}
}
$enclosures = array();
foreach($toy->images as $image) {
$enclosures[] = $image->fullFilename;
}
$item['enclosures'] = $enclosures;
// images
$content = '<p>';
foreach ($toy->images as $image) {
$content .= '<a href="'
. $image->fullFilename
. '"><img src="'
. $image->thumbFilename
. '" /></a>';
}
// price
$content .= '</p><p><b>Price:</b> $'
. $toy->price
// size
. '<br /><b>Size:</b> '
. $toy->size
// color
. '<br /><b>Color:</b> '
. $toy->color
// features
. '<br /><b>Features:</b> '
. ($toy->suction_cup ? 'Suction cup' : '')
. ($toy->suction_cup && $toy->cumtube ? ', ' : '')
. ($toy->cumtube ? 'Cumtube' : '')
. ($toy->suction_cup || $toy->cumtube ? '' : 'None');
// firmness
$firmnessTexts = [
'2' => 'Extra soft',
'3' => 'Soft',
'5' => 'Medium',
'8' => 'Firm'
];
$firmnesses = explode('/', $toy->firmness);
if (count($firmnesses) === 2) {
$content .= '<br /><b>Firmness:</b> '
. $firmnessTexts[$firmnesses[0]]
. ', '
. $firmnessTexts[$firmnesses[1]];
} else {
$content .= '<br /><b>Firmness:</b> '
. $firmnessTexts[$firmnesses[0]];
}
// flop
if ($toy->type === 'flop') {
$content .= '<br /><b>Flop reason:</b> '
. $toy->flop_reason;
}
$content .= '</p>';
$item['content'] = $content;
$categories = array();
$categories[] = $toy->sku;
$categories[] = $toy->type;
$categories[] = $toy->size;
if($toy->cumtube) {
$categories[] = 'cumtube';
}
if($toy->suction_cup) {
$categories[] = 'suction_cup';
}
$item['categories'] = $categories;
$enclosures = [];
foreach ($toy->images as $image) {
$enclosures[] = $image->fullFilename;
}
$item['enclosures'] = $enclosures;
$this->items[] = $item;
}
break;
}
}
$categories = [];
$categories[] = $toy->sku;
$categories[] = $toy->type;
$categories[] = $toy->size;
if ($toy->cumtube) {
$categories[] = 'cumtube';
}
if ($toy->suction_cup) {
$categories[] = 'suction_cup';
}
$item['categories'] = $categories;
private function inputToURL($api = false) {
$url = self::URI;
$url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?');
$this->items[] = $item;
}
break;
}
}
// Default parameters
$url .= 'limit=60';
$url .= '&page=1';
$url .= '&sort[field]=created';
$url .= '&sort[direction]=desc';
private function inputToURL($api = false)
{
$url = self::URI;
$url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?');
// Product types
$url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : '');
$url .= ($this->getInput('flop') ? '&type[]=flop' : '');
// Default parameters
$url .= 'limit=60';
$url .= '&page=1';
$url .= '&sort[field]=created';
$url .= '&sort[direction]=desc';
// Product names
foreach(array_filter(explode(',', $this->getInput('skus'))) as $sku) {
$url .= '&skus[]=' . urlencode(trim($sku));
}
// Product types
$url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : '');
$url .= ($this->getInput('flop') ? '&type[]=flop' : '');
// Size
$url .= ($this->getInput('onesize') ? '&sizes[]=onesize' : '');
$url .= ($this->getInput('mini') ? '&sizes[]=mini' : '');
$url .= ($this->getInput('small') ? '&sizes[]=small' : '');
$url .= ($this->getInput('medium') ? '&sizes[]=medium' : '');
$url .= ($this->getInput('large') ? '&sizes[]=large' : '');
$url .= ($this->getInput('extralarge') ? '&sizes[]=extralarge' : '');
// Product names
foreach (array_filter(explode(',', $this->getInput('skus'))) as $sku) {
$url .= '&skus[]=' . urlencode(trim($sku));
}
// Category
$url .= ($this->getInput('category') ? '&category='
. urlencode($this->getInput('category')) : '');
// Size
$url .= ($this->getInput('onesize') ? '&sizes[]=onesize' : '');
$url .= ($this->getInput('mini') ? '&sizes[]=mini' : '');
$url .= ($this->getInput('small') ? '&sizes[]=small' : '');
$url .= ($this->getInput('medium') ? '&sizes[]=medium' : '');
$url .= ($this->getInput('large') ? '&sizes[]=large' : '');
$url .= ($this->getInput('extralarge') ? '&sizes[]=extralarge' : '');
// Firmness
if($api) {
$url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : '');
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : '');
$url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : '');
if($this->getInput('split')) {
$url .= '&firmnessValues[]=3/5';
$url .= '&firmnessValues[]=3/8';
$url .= '&firmnessValues[]=8/3';
$url .= '&firmnessValues[]=5/8';
$url .= '&firmnessValues[]=8/5';
}
} else{
$url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : '');
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : '');
$url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : '');
$url .= ($this->getInput('split') ? '&firmnessValues[]=split' : '');
}
// Category
$url .= ($this->getInput('category') ? '&category='
. urlencode($this->getInput('category')) : '');
// Price
$url .= ($this->getInput('maxprice') ? '&price[max]='
. $this->getInput('maxprice') : '&price[max]=300');
$url .= ($this->getInput('minprice') ? '&price[min]='
. $this->getInput('minprice') : '&price[min]=0');
// Firmness
if ($api) {
$url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : '');
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : '');
$url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : '');
if ($this->getInput('split')) {
$url .= '&firmnessValues[]=3/5';
$url .= '&firmnessValues[]=3/8';
$url .= '&firmnessValues[]=8/3';
$url .= '&firmnessValues[]=5/8';
$url .= '&firmnessValues[]=8/5';
}
} else {
$url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : '');
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : '');
$url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : '');
$url .= ($this->getInput('split') ? '&firmnessValues[]=split' : '');
}
// Features
$url .= ($this->getInput('cumtube') ? '&cumtube=1' : '');
$url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : '');
$url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : '');
// Price
$url .= ($this->getInput('maxprice') ? '&price[max]='
. $this->getInput('maxprice') : '&price[max]=300');
$url .= ($this->getInput('minprice') ? '&price[min]='
. $this->getInput('minprice') : '&price[min]=0');
return $url;
}
// Features
$url .= ($this->getInput('cumtube') ? '&cumtube=1' : '');
$url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : '');
$url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : '');
return $url;
}
}

View File

@@ -1,186 +1,211 @@
<?php
class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
const NAME = 'Baka Updates Manga Releases';
const URI = 'https://www.mangaupdates.com/';
const DESCRIPTION = 'Get the latest series releases';
const MAINTAINER = 'fulmeek, KamaleiZestri';
const PARAMETERS = array(
'By series' => array(
'series_id' => array(
'name' => 'Series ID',
'type' => 'number',
'required' => true,
'exampleValue' => '188066'
)
),
'By list' => array(
'list_id' => array(
'name' => 'List ID and Type',
'type' => 'text',
'required' => true,
'exampleValue' => '4395&list=read'
)
)
);
const LIMIT_COLS = 5;
const LIMIT_ITEMS = 10;
const RELEASES_URL = 'https://www.mangaupdates.com/releases.html';
private $feedName = '';
class BakaUpdatesMangaReleasesBridge extends BridgeAbstract
{
const NAME = 'Baka Updates Manga Releases';
const URI = 'https://www.mangaupdates.com/';
const DESCRIPTION = 'Get the latest series releases';
const MAINTAINER = 'fulmeek, KamaleiZestri';
const PARAMETERS = [
'By series' => [
'series_id' => [
'name' => 'Series ID',
'type' => 'number',
'required' => true,
'exampleValue' => '188066'
]
],
'By list' => [
'list_id' => [
'name' => 'List ID and Type',
'type' => 'text',
'required' => true,
'exampleValue' => '4395&list=read'
]
]
];
const LIMIT_COLS = 5;
const LIMIT_ITEMS = 10;
const RELEASES_URL = 'https://www.mangaupdates.com/releases.html';
public function collectData() {
if($this -> queriedContext == 'By series')
$this -> collectDataBySeries();
else //queriedContext == 'By list'
$this -> collectDataByList();
}
private $feedName = '';
public function getURI(){
if($this -> queriedContext == 'By series') {
$series_id = $this->getInput('series_id');
if (!empty($series_id)) {
return self::URI . 'releases.html?search=' . $series_id . '&stype=series';
}
} else //queriedContext == 'By list'
return self::RELEASES_URL;
public function collectData()
{
if ($this -> queriedContext == 'By series') {
$this -> collectDataBySeries();
} else { //queriedContext == 'By list'
$this -> collectDataByList();
}
}
return self::URI;
}
public function getURI()
{
if ($this -> queriedContext == 'By series') {
$series_id = $this->getInput('series_id');
if (!empty($series_id)) {
return self::URI . 'releases.html?search=' . $series_id . '&stype=series';
}
} else { //queriedContext == 'By list'
return self::RELEASES_URL;
}
public function getName(){
if(!empty($this->feedName)) {
return $this->feedName . ' - ' . self::NAME;
}
return parent::getName();
}
return self::URI;
}
private function getSanitizedHash($string) {
return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
}
public function getName()
{
if (!empty($this->feedName)) {
return $this->feedName . ' - ' . self::NAME;
}
return parent::getName();
}
private function filterText($text) {
return rtrim($text, '* ');
}
private function getSanitizedHash($string)
{
return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
}
private function filterHTML($text) {
return $this->filterText(html_entity_decode($text));
}
private function filterText($text)
{
return rtrim($text, '* ');
}
private function findID($manga) {
// sometimes new series are on the release list that have no ID. just drop them.
if(@$this -> filterHTML($manga -> find('a', 0) -> href) != null) {
preg_match('/id=([0-9]*)/', $this -> filterHTML($manga -> find('a', 0) -> href), $match);
return $match[1];
} else
return 0;
}
private function filterHTML($text)
{
return $this->filterText(html_entity_decode($text));
}
private function collectDataBySeries() {
$html = getSimpleHTMLDOM($this->getURI());
private function findID($manga)
{
// sometimes new series are on the release list that have no ID. just drop them.
if (@$this -> filterHTML($manga -> find('a', 0) -> href) != null) {
preg_match('/id=([0-9]*)/', $this -> filterHTML($manga -> find('a', 0) -> href), $match);
return $match[1];
} else {
return 0;
}
}
// content is an unstructured pile of divs, ugly to parse
$cols = $html->find('div#main_content div.row > div.text');
if (!$cols)
returnServerError('No releases');
private function collectDataBySeries()
{
$html = getSimpleHTMLDOM($this->getURI());
$rows = array_slice(
array_chunk($cols, self::LIMIT_COLS), 0, self::LIMIT_ITEMS
);
// content is an unstructured pile of divs, ugly to parse
$cols = $html->find('div#main_content div.row > div.text');
if (!$cols) {
returnServerError('No releases');
}
if (isset($rows[0][1])) {
$this->feedName = $this->filterHTML($rows[0][1]->plaintext);
}
$rows = array_slice(
array_chunk($cols, self::LIMIT_COLS),
0,
self::LIMIT_ITEMS
);
foreach($rows as $cols) {
if (count($cols) < self::LIMIT_COLS) continue;
if (isset($rows[0][1])) {
$this->feedName = $this->filterHTML($rows[0][1]->plaintext);
}
$item = array();
$title = array();
foreach ($rows as $cols) {
if (count($cols) < self::LIMIT_COLS) {
continue;
}
$item['content'] = '';
$item = [];
$title = [];
$objDate = $cols[0];
if ($objDate)
$item['timestamp'] = strtotime($objDate->plaintext);
$item['content'] = '';
$objTitle = $cols[1];
if ($objTitle) {
$title[] = $this->filterHTML($objTitle->plaintext);
$item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>';
}
$objDate = $cols[0];
if ($objDate) {
$item['timestamp'] = strtotime($objDate->plaintext);
}
$objVolume = $cols[2];
if ($objVolume && !empty($objVolume->plaintext))
$title[] = 'Vol.' . $objVolume->plaintext;
$objTitle = $cols[1];
if ($objTitle) {
$title[] = $this->filterHTML($objTitle->plaintext);
$item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>';
}
$objChapter = $cols[3];
if ($objChapter && !empty($objChapter->plaintext))
$title[] = 'Chp.' . $objChapter->plaintext;
$objVolume = $cols[2];
if ($objVolume && !empty($objVolume->plaintext)) {
$title[] = 'Vol.' . $objVolume->plaintext;
}
$objAuthor = $cols[4];
if ($objAuthor && !empty($objAuthor->plaintext)) {
$item['author'] = $this->filterHTML($objAuthor->plaintext);
$item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>';
}
$objChapter = $cols[3];
if ($objChapter && !empty($objChapter->plaintext)) {
$title[] = 'Chp.' . $objChapter->plaintext;
}
$item['title'] = implode(' ', $title);
$item['uri'] = $this->getURI();
$item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
$objAuthor = $cols[4];
if ($objAuthor && !empty($objAuthor->plaintext)) {
$item['author'] = $this->filterHTML($objAuthor->plaintext);
$item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>';
}
$this->items[] = $item;
}
}
$item['title'] = implode(' ', $title);
$item['uri'] = $this->getURI();
$item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
private function collectDataByList() {
$this -> feedName = 'Releases';
$list = array();
$this->items[] = $item;
}
}
$releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL);
private function collectDataByList()
{
$this -> feedName = 'Releases';
$list = [];
$list_id = $this -> getInput('list_id');
$listHTML = getSimpleHTMLDOM('https://www.mangaupdates.com/mylist.html?id=' . $list_id);
$releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL);
//get ids of the manga that the user follows,
$parts = $listHTML -> find('table#ptable tr > td.pl');
foreach($parts as $part) {
$list[] = $this -> findID($part);
}
$list_id = $this -> getInput('list_id');
$listHTML = getSimpleHTMLDOM('https://www.mangaupdates.com/mylist.html?id=' . $list_id);
//similar to above, but the divs are in groups of 3.
$cols = $releasesHTML -> find('div#main_content div.row > div.pbreak');
$rows = array_slice(array_chunk($cols, 3), 0);
//get ids of the manga that the user follows,
$parts = $listHTML -> find('table#ptable tr > td.pl');
foreach ($parts as $part) {
$list[] = $this -> findID($part);
}
foreach($rows as $cols) {
//check if current manga is in user's list.
$id = $this -> findId($cols[0]);
if(!array_search($id, $list)) continue;
//similar to above, but the divs are in groups of 3.
$cols = $releasesHTML -> find('div#main_content div.row > div.pbreak');
$rows = array_slice(array_chunk($cols, 3), 0);
$item = array();
$title = array();
foreach ($rows as $cols) {
//check if current manga is in user's list.
$id = $this -> findId($cols[0]);
if (!array_search($id, $list)) {
continue;
}
$item['content'] = '';
$item = [];
$title = [];
$objTitle = $cols[0];
if ($objTitle) {
$title[] = $this->filterHTML($objTitle->plaintext);
$item['content'] .= '<p>Series: ' . $this->filterHTML($objTitle -> innertext) . '</p>';
}
$item['content'] = '';
$objVolChap = $cols[1];
if ($objVolChap && !empty($objVolChap->plaintext))
$title[] = $this -> filterHTML($objVolChap -> innertext);
$objTitle = $cols[0];
if ($objTitle) {
$title[] = $this->filterHTML($objTitle->plaintext);
$item['content'] .= '<p>Series: ' . $this->filterHTML($objTitle -> innertext) . '</p>';
}
$objAuthor = $cols[2];
if ($objAuthor && !empty($objAuthor->plaintext)) {
$item['author'] = $this->filterHTML($objAuthor -> plaintext);
$item['content'] .= '<p>Groups: ' . $this->filterHTML($objAuthor -> innertext) . '</p>';
}
$objVolChap = $cols[1];
if ($objVolChap && !empty($objVolChap->plaintext)) {
$title[] = $this -> filterHTML($objVolChap -> innertext);
}
$item['title'] = implode(' ', $title);
$item['uri'] = self::URI . 'releases.html?search=' . $id . '&stype=series';
$item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
$objAuthor = $cols[2];
if ($objAuthor && !empty($objAuthor->plaintext)) {
$item['author'] = $this->filterHTML($objAuthor -> plaintext);
$item['content'] .= '<p>Groups: ' . $this->filterHTML($objAuthor -> innertext) . '</p>';
}
$this->items[] = $item;
}
}
$item['title'] = implode(' ', $title);
$item['uri'] = self::URI . 'releases.html?search=' . $id . '&stype=series';
$item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
$this->items[] = $item;
}
}
}

View File

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

View File

@@ -1,155 +1,164 @@
<?php
class BandcampDailyBridge extends BridgeAbstract {
const NAME = 'Bandcamp Daily Bridge';
const URI = 'https://daily.bandcamp.com';
const DESCRIPTION = 'Returns newest articles';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(
'Latest articles' => array(),
'Best of' => array(
'content' => array(
'name' => 'content',
'type' => 'list',
'values' => array(
'Best Ambient' => 'best-ambient',
'Best Beat Tapes' => 'best-beat-tapes',
'Best Dance 12\'s' => 'best-dance-12s',
'Best Contemporary Classical' => 'best-contemporary-classical',
'Best Electronic' => 'best-electronic',
'Best Experimental' => 'best-experimental',
'Best Hip-Hop' => 'best-hip-hop',
'Best Jazz' => 'best-jazz',
'Best Metal' => 'best-metal',
'Best Punk' => 'best-punk',
'Best Reissues' => 'best-reissues',
'Best Soul' => 'best-soul',
),
'defaultValue' => 'best-ambient',
),
),
'Genres' => array(
'content' => array(
'name' => 'content',
'type' => 'list',
'values' => array(
'Acoustic' => 'genres/acoustic',
'Alternative' => 'genres/alternative',
'Ambient' => 'genres/ambient',
'Blues' => 'genres/blues',
'Classical' => 'genres/classical',
'Comedy' => 'genres/comedy',
'Country' => 'genres/country',
'Devotional' => 'genres/devotional',
'Electronic' => 'genres/electronic',
'Experimental' => 'genres/experimental',
'Folk' => 'genres/folk',
'Funk' => 'genres/funk',
'Hip-Hop/Rap' => 'genres/hip-hop-rap',
'Jazz' => 'genres/jazz',
'Kids' => 'genres/kids',
'Latin' => 'genres/latin',
'Metal' => 'genres/metal',
'Pop' => 'genres/pop',
'Punk' => 'genres/punk',
'R&B/Soul' => 'genres/r-b-soul',
'Reggae' => 'genres/reggae',
'Rock' => 'genres/rock',
'Soundtrack' => 'genres/soundtrack',
'Spoken Word' => 'genres/spoken-word',
'World' => 'genres/world',
),
'defaultValue' => 'genres/acoustic',
),
),
'Franchises' => array(
'content' => array(
'name' => 'content',
'type' => 'list',
'values' => array(
'Lists' => 'lists',
'Features' => 'features',
'Album of the Day' => 'album-of-the-day',
'Acid Test' => 'acid-test',
'Bandcamp Navigator' => 'bandcamp-navigator',
'Big Ups' => 'big-ups',
'Certified' => 'certified',
'Gallery' => 'gallery',
'Hidden Gems' => 'hidden-gems',
'High Scores' => 'high-scores',
'Label Profile' => 'label-profile',
'Lifetime Achievement' => 'lifetime-achievement',
'Scene Report' => 'scene-report',
'Seven Essential Releases' => 'seven-essential-releases',
'The Merch Table' => 'the-merch-table',
),
'defaultValue' => 'lists',
),
)
);
const CACHE_TIMEOUT = 3600; // 1 hour
class BandcampDailyBridge extends BridgeAbstract
{
const NAME = 'Bandcamp Daily Bridge';
const URI = 'https://daily.bandcamp.com';
const DESCRIPTION = 'Returns newest articles';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = [
'Latest articles' => [],
'Best of' => [
'best-content' => [
'name' => 'content',
'type' => 'list',
'values' => [
'Best Ambient' => 'best-ambient',
'Best Beat Tapes' => 'best-beat-tapes',
'Best Dance 12\'s' => 'best-dance-12s',
'Best Contemporary Classical' => 'best-contemporary-classical',
'Best Electronic' => 'best-electronic',
'Best Experimental' => 'best-experimental',
'Best Hip-Hop' => 'best-hip-hop',
'Best Jazz' => 'best-jazz',
'Best Metal' => 'best-metal',
'Best Punk' => 'best-punk',
'Best Reissues' => 'best-reissues',
'Best Soul' => 'best-soul',
],
'defaultValue' => 'best-ambient',
],
],
'Genres' => [
'genres-content' => [
'name' => 'content',
'type' => 'list',
'values' => [
'Acoustic' => 'genres/acoustic',
'Alternative' => 'genres/alternative',
'Ambient' => 'genres/ambient',
'Blues' => 'genres/blues',
'Classical' => 'genres/classical',
'Comedy' => 'genres/comedy',
'Country' => 'genres/country',
'Devotional' => 'genres/devotional',
'Electronic' => 'genres/electronic',
'Experimental' => 'genres/experimental',
'Folk' => 'genres/folk',
'Funk' => 'genres/funk',
'Hip-Hop/Rap' => 'genres/hip-hop-rap',
'Jazz' => 'genres/jazz',
'Kids' => 'genres/kids',
'Latin' => 'genres/latin',
'Metal' => 'genres/metal',
'Pop' => 'genres/pop',
'Punk' => 'genres/punk',
'R&B/Soul' => 'genres/r-b-soul',
'Reggae' => 'genres/reggae',
'Rock' => 'genres/rock',
'Soundtrack' => 'genres/soundtrack',
'Spoken Word' => 'genres/spoken-word',
'World' => 'genres/world',
],
'defaultValue' => 'genres/acoustic',
],
],
'Franchises' => [
'franchises-content' => [
'name' => 'content',
'type' => 'list',
'values' => [
'Lists' => 'lists',
'Features' => 'features',
'Album of the Day' => 'album-of-the-day',
'Acid Test' => 'acid-test',
'Bandcamp Navigator' => 'bandcamp-navigator',
'Big Ups' => 'big-ups',
'Certified' => 'certified',
'Gallery' => 'gallery',
'Hidden Gems' => 'hidden-gems',
'High Scores' => 'high-scores',
'Label Profile' => 'label-profile',
'Lifetime Achievement' => 'lifetime-achievement',
'Scene Report' => 'scene-report',
'Seven Essential Releases' => 'seven-essential-releases',
'The Merch Table' => 'the-merch-table',
],
'defaultValue' => 'lists',
],
]
];
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
const CACHE_TIMEOUT = 3600; // 1 hour
$html = defaultLinkTo($html, self::URI);
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
$articles = $html->find('articles-list', 0);
$html = defaultLinkTo($html, self::URI);
foreach($articles->find('div.list-article') as $index => $article) {
$item = array();
$articles = $html->find('articles-list', 0);
$articlePath = $article->find('a.title', 0)->href;
foreach ($articles->find('div.list-article') as $index => $article) {
$item = [];
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600)
or returnServerError('Could not request: ' . $articlePath);
$articlePath = $article->find('a.title', 0)->href;
$item['uri'] = $articlePath;
$item['title'] = $articlePageHtml->find('article-title', 0)->innertext;
$item['author'] = $articlePageHtml->find('article-credits > a', 0)->innertext;
$item['content'] = html_entity_decode($articlePageHtml->find('meta[name="description"]', 0)->content, ENT_QUOTES);
$item['timestamp'] = $articlePageHtml->find('meta[property="article:published_time"]', 0)->content;
$item['categories'][] = $articlePageHtml->find('meta[property="article:section"]', 0)->content;
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600)
or returnServerError('Could not request: ' . $articlePath);
if ($articlePageHtml->find('meta[property="article:tag"]', 0)) {
$item['categories'][] = $articlePageHtml->find('meta[property="article:tag"]', 0)->content;
}
$item['uri'] = $articlePath;
$item['title'] = $articlePageHtml->find('article-title', 0)->innertext;
$item['author'] = $articlePageHtml->find('article-credits > a', 0)->innertext;
$item['content'] = html_entity_decode($articlePageHtml->find('meta[name="description"]', 0)->content, ENT_QUOTES);
$item['timestamp'] = $articlePageHtml->find('meta[property="article:published_time"]', 0)->content;
$item['categories'][] = $articlePageHtml->find('meta[property="article:section"]', 0)->content;
$item['enclosures'][] = $articlePageHtml->find('meta[name="twitter:image"]', 0)->content;
if ($articlePageHtml->find('meta[property="article:tag"]', 0)) {
$item['categories'][] = $articlePageHtml->find('meta[property="article:tag"]', 0)->content;
}
$this->items[] = $item;
$item['enclosures'][] = $articlePageHtml->find('meta[name="twitter:image"]', 0)->content;
if (count($this->items) >= 10) {
break;
}
}
}
$this->items[] = $item;
public function getURI() {
switch($this->queriedContext) {
case 'Latest articles':
return self::URI . '/latest';
case 'Best of':
case 'Genres':
case 'Franchises':
return self::URI . '/' . $this->getInput('content');
default:
return parent::getURI();
}
}
if (count($this->items) >= 10) {
break;
}
}
}
public function getName() {
if ($this->queriedContext === 'Latest articles') {
return $this->queriedContext . ' - Bandcamp Daily';
}
public function getURI()
{
switch ($this->queriedContext) {
case 'Latest articles':
return self::URI . '/latest';
case 'Best of':
case 'Genres':
case 'Franchises':
// TODO Switch to array_key_first once php >= 7.3
$contentKey = key(self::PARAMETERS[$this->queriedContext]);
return self::URI . '/' . $this->getInput($contentKey);
default:
return parent::getURI();
}
}
if (!is_null($this->getInput('content'))) {
$contentValues = array_flip(self::PARAMETERS[$this->queriedContext]['content']['values']);
public function getName()
{
switch ($this->queriedContext) {
case 'Latest articles':
return $this->queriedContext . ' - Bandcamp Daily';
case 'Best of':
case 'Genres':
case 'Franchises':
$contentKey = array_key_first(self::PARAMETERS[$this->queriedContext]);
$contentValues = array_flip(self::PARAMETERS[$this->queriedContext][$contentKey]['values']);
return $contentValues[$this->getInput('content')] . ' - Bandcamp Daily';
}
return parent::getName();
}
return $contentValues[$this->getInput($contentKey)] . ' - Bandcamp Daily';
default:
return parent::getName();
}
}
}

View File

@@ -1,31 +1,33 @@
<?php
class BastaBridge extends BridgeAbstract {
const MAINTAINER = 'qwertygc';
const NAME = 'Bastamag Bridge';
const URI = 'https://www.bastamag.net/';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns the newest articles.';
class BastaBridge extends BridgeAbstract
{
const MAINTAINER = 'qwertygc';
const NAME = 'Bastamag Bridge';
const URI = 'https://www.bastamag.net/';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns the newest articles.';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend');
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend');
$limit = 0;
$limit = 0;
foreach($html->find('item') as $element) {
if($limit < 10) {
$item = array();
$item['title'] = $element->find('title', 0)->innertext;
$item['uri'] = $element->find('guid', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
foreach ($html->find('item') as $element) {
if ($limit < 10) {
$item = [];
$item['title'] = $element->find('title', 0)->innertext;
$item['uri'] = $element->find('guid', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
$html = getSimpleHTMLDOM($item['uri']);
$html = defaultLinkTo($html, self::URI);
$html = getSimpleHTMLDOM($item['uri']);
$html = defaultLinkTo($html, self::URI);
$item['content'] = $html->find('div.texte', 0)->innertext;
$this->items[] = $item;
$limit++;
}
}
}
$item['content'] = $html->find('div.texte', 0)->innertext;
$this->items[] = $item;
$limit++;
}
}
}
}

View File

@@ -1,41 +1,55 @@
<?php
class BinanceBridge extends BridgeAbstract {
const NAME = 'Binance Blog';
const URI = 'https://www.binance.com/en/blog';
const DESCRIPTION = 'Subscribe to the Binance blog.';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
public function getIcon() {
return 'https://bin.bnbstatic.com/static/images/common/favicon.ico';
}
class BinanceBridge extends BridgeAbstract
{
const NAME = 'Binance Blog';
const URI = 'https://www.binance.com/en/blog';
const DESCRIPTION = 'Subscribe to the Binance blog.';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
public function collectData() {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not fetch Binance blog data.');
public function getIcon()
{
return 'https://bin.bnbstatic.com/static/images/common/favicon.ico';
}
$appData = $html->find('script[id="__APP_DATA"]');
$appDataJson = json_decode($appData[0]->innertext);
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not fetch Binance blog data.');
foreach($appDataJson->pageData->redux->blogList->blogList as $element) {
$appData = $html->find('script[id="__APP_DATA"]');
$appDataJson = json_decode($appData[0]->innertext);
$allposts = $appDataJson->routeProps->f3ac->blogListRes->list;
$date = $element->postTime;
$abstract = $element->brief;
$uri = self::URI . '/' . $element->lang . '/blog/' . $element->idStr;
$title = $element->title;
$content = $element->content;
foreach ($allposts as $element) {
$date = $element->releasedTime;
$title = $element->title;
$category = $element->category->name;
$item = array();
$item['title'] = $title;
$item['uri'] = $uri;
$item['timestamp'] = substr($date, 0, -3);
$item['author'] = 'Binance';
$item['content'] = $content;
$suburl = strtolower($category);
$suburl = str_replace(' ', '_', $suburl);
$this->items[] = $item;
$uri = self::URI . '/' . $suburl . '/' . $element->idStr;
if (count($this->items) >= 10)
break;
}
}
$contentHTML = getSimpleHTMLDOMCached($uri);
$contentAppData = $contentHTML->find('script[id="__APP_DATA"]');
$contentAppDataJson = json_decode($contentAppData[0]->innertext);
$content = $contentAppDataJson->routeProps->a106->blogDetail->content;
$item = [];
$item['title'] = $title;
$item['uri'] = $uri;
$item['timestamp'] = substr($date, 0, -3);
$item['author'] = 'Binance';
$item['content'] = $content;
$item['categories'] = $category;
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
}
}

View File

@@ -1,45 +1,44 @@
<?php
class BlaguesDeMerdeBridge extends BridgeAbstract {
const MAINTAINER = 'superbaillot.net, logmanoriginal';
const NAME = 'Blagues De Merde';
const URI = 'http://www.blaguesdemerde.fr/';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Blagues De Merde';
class BlaguesDeMerdeBridge extends BridgeAbstract
{
const MAINTAINER = 'superbaillot.net, logmanoriginal';
const NAME = 'Blagues De Merde';
const URI = 'http://www.blaguesdemerde.fr/';
const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Blagues De Merde';
public function getIcon() {
return self::URI . 'assets/img/favicon.ico';
}
public function getIcon()
{
return self::URI . 'assets/img/favicon.ico';
}
public function collectData(){
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI);
$html = getSimpleHTMLDOM(self::URI);
foreach ($html->find('div.blague') as $element) {
$item = [];
foreach($html->find('div.blague') as $element) {
$item['uri'] = static::URI . '#' . $element->id;
$item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext;
$item = array();
// Let the title be everything up to the first <br>
$item['title'] = trim(explode("\n", $element->find('div.text', 0)->plaintext)[0]);
$item['uri'] = static::URI . '#' . $element->id;
$item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext;
$item['content'] = strip_tags($element->find('div.text', 0));
// Let the title be everything up to the first <br>
$item['title'] = trim(explode("\n", $element->find('div.text', 0)->plaintext)[0]);
// timestamp is part of:
// <p>Par <strong>{author}</strong> le {date} dans <strong>{category}</strong></p>
preg_match(
'/.+le(.+)dans.*/',
$element->find('div[class="blague-footer"]', 0)->plaintext,
$matches
);
$item['content'] = strip_tags($element->find('div.text', 0));
$item['timestamp'] = strtotime($matches[1]);
// timestamp is part of:
// <p>Par <strong>{author}</strong> le {date} dans <strong>{category}</strong></p>
preg_match(
'/.+le(.+)dans.*/',
$element->find('div[class="blague-footer"]', 0)->plaintext,
$matches
);
$item['timestamp'] = strtotime($matches[1]);
$this->items[] = $item;
}
}
$this->items[] = $item;
}
}
}

View File

@@ -1,29 +1,32 @@
<?php
class BleepingComputerBridge extends FeedExpander {
const MAINTAINER = 'csisoap';
const NAME = 'Bleeping Computer';
const URI = 'https://www.bleepingcomputer.com/';
const DESCRIPTION = 'Returns the newest articles.';
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);
protected function parseItem($item)
{
$item = parent::parseItem($item);
$article_html = getSimpleHTMLDOMCached($item['uri']);
if(!$article_html) {
$item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
return $item;
}
$article_html = 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);
$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;
}
return $item;
}
public function collectData(){
$feed = static::URI . 'feed/';
$this->collectExpandableDatas($feed);
}
public function collectData()
{
$feed = static::URI . 'feed/';
$this->collectExpandableDatas($feed);
}
}

View File

@@ -1,60 +1,60 @@
<?php
class BlizzardNewsBridge extends XPathAbstract {
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 = [
'' => [
'locale' => [
'name' => 'Language',
'type' => 'list',
'values' => [
'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 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;
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;
}
/**
* 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;
}
}

1478
bridges/BookMyShowBridge.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,77 @@
<?php
require_once('DanbooruBridge.php');
class BooruprojectBridge extends DanbooruBridge {
class BooruprojectBridge extends DanbooruBridge
{
const MAINTAINER = 'mitsukarenai';
const NAME = 'Booruproject';
const URI = 'https://booru.org/';
const DESCRIPTION = 'Returns images from given page of booruproject';
const PARAMETERS = [
'global' => [
'p' => [
'name' => 'page',
'defaultValue' => 0,
'type' => 'number'
],
't' => [
'name' => 'tags',
'required' => true,
'exampleValue' => 'tagme',
'title' => 'Use "all" to get all posts'
]
],
'Booru subdomain (subdomain.booru.org)' => [
'i' => [
'name' => 'Subdomain',
'required' => true,
'exampleValue' => 'rm'
]
]
];
const MAINTAINER = 'mitsukarenai';
const NAME = 'Booruproject';
const URI = 'https://booru.org/';
const DESCRIPTION = 'Returns images from given page of booruproject';
const PARAMETERS = array(
'global' => array(
'p' => array(
'name' => 'page',
'defaultValue' => 0,
'type' => 'number'
),
't' => array(
'name' => 'tags',
'required' => true,
'exampleValue' => 'tagme',
'title' => 'Use "all" to get all posts'
)
),
'Booru subdomain (subdomain.booru.org)' => array(
'i' => array(
'name' => 'Subdomain',
'required' => true,
'exampleValue' => 'rm'
)
)
);
const PATHTODATA = '.thumb';
const IDATTRIBUTE = 'id';
const TAGATTRIBUTE = 'title';
const PIDBYPAGE = 20;
const PATHTODATA = '.thumb';
const IDATTRIBUTE = 'id';
const TAGATTRIBUTE = 'title';
const PIDBYPAGE = 20;
protected function getFullURI()
{
return $this->getURI()
. 'index.php?page=post&s=list&pid='
. ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '')
. '&tags=' . urlencode($this->getInput('t'));
}
protected function getFullURI(){
return $this->getURI()
. 'index.php?page=post&s=list&pid='
. ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '')
. '&tags=' . urlencode($this->getInput('t'));
}
protected function getTags($element)
{
$tags = parent::getTags($element);
$tags = explode(' ', $tags);
protected function getTags($element){
$tags = parent::getTags($element);
$tags = explode(' ', $tags);
// Remove statistics from the tags list (identified by colon)
foreach ($tags as $key => $tag) {
if (strpos($tag, ':') !== false) {
unset($tags[$key]);
}
}
// Remove statistics from the tags list (identified by colon)
foreach($tags as $key => $tag) {
if(strpos($tag, ':') !== false) unset($tags[$key]);
}
return implode(' ', $tags);
}
return implode(' ', $tags);
}
public function getURI()
{
if (!is_null($this->getInput('i'))) {
return 'https://' . $this->getInput('i') . '.booru.org/';
}
public function getURI(){
if(!is_null($this->getInput('i'))) {
return 'https://' . $this->getInput('i') . '.booru.org/';
}
return parent::getURI();
}
return parent::getURI();
}
public function getName()
{
if (!is_null($this->getInput('i'))) {
return static::NAME . ' ' . $this->getInput('i');
}
public function getName(){
if(!is_null($this->getInput('i'))) {
return static::NAME . ' ' . $this->getInput('i');
}
return parent::getName();
}
return parent::getName();
}
}

View File

@@ -1,157 +1,124 @@
<?php
class BrutBridge extends BridgeAbstract {
const NAME = 'Brut Bridge';
const URI = 'https://www.brut.media';
const DESCRIPTION = 'Returns 5 newest videos by category and edition';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'News' => 'news',
'International' => 'international',
'Economy' => 'economy',
'Science and Technology' => 'science-and-technology',
'Entertainment' => 'entertainment',
'Sports' => 'sport',
'Nature' => 'nature',
'Health' => 'health',
),
'defaultValue' => 'news',
),
'edition' => array(
'name' => ' Edition',
'type' => 'list',
'values' => array(
'United States' => 'us',
'United Kingdom' => 'uk',
'France' => 'fr',
'Spain' => 'es',
'India' => 'in',
'Mexico' => 'mx',
),
'defaultValue' => 'us',
)
)
);
const CACHE_TIMEOUT = 1800; // 30 mins
class BrutBridge extends BridgeAbstract
{
const NAME = 'Brut Bridge';
const URI = 'https://www.brut.media';
const DESCRIPTION = 'Returns 10 newest videos by category and edition';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = [[
'category' => [
'name' => 'Category',
'type' => 'list',
'values' => [
'News' => 'news',
'International' => 'international',
'Economy' => 'economy',
'Science and Technology' => 'science-and-technology',
'Entertainment' => 'entertainment',
'Sports' => 'sport',
'Nature' => 'nature',
'Health' => 'health',
],
'defaultValue' => 'news',
],
'edition' => [
'name' => ' Edition',
'type' => 'list',
'values' => [
'United States' => 'us',
'United Kingdom' => 'uk',
'France' => 'fr',
'Spain' => 'es',
'India' => 'in',
'Mexico' => 'mx',
],
'defaultValue' => 'us',
]
]
];
private $videoId = '';
private $videoType = '';
private $videoImage = '';
const CACHE_TIMEOUT = 1800; // 30 mins
public function collectData() {
private $jsonRegex = '/window\.__PRELOADED_STATE__ = ((?:.*)});/';
$html = getSimpleHTMLDOM($this->getURI());
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
$results = $html->find('div.results', 0);
$results = $html->find('div.results', 0);
foreach($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $index => $li) {
$item = array();
foreach ($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $li) {
$item = [];
$videoPath = self::URI . $li->children(0)->href;
$videoPath = self::URI . $li->children(0)->href;
$videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600);
$videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600);
$json = $this->extractJson($videoPageHtml);
$id = array_keys((array) $json->media->index)[0];
$this->videoImage = $videoPageHtml->find('meta[name="twitter:image"]', 0)->content;
$item['uri'] = $videoPath;
$item['title'] = $json->media->index->$id->title;
$item['timestamp'] = $json->media->index->$id->published_at;
$item['enclosures'][] = $json->media->index->$id->media->thumbnail;
$this->processTwitterImage();
$description = $json->media->index->$id->description;
$article = '';
$description = $videoPageHtml->find('div.description', 0);
if (is_null($json->media->index->$id->media->seo_article) === false) {
$article = markdownToHtml($json->media->index->$id->media->seo_article);
}
$item['uri'] = $videoPath;
$item['title'] = $description->find('h1', 0)->plaintext;
$item['content'] = <<<EOD
<video controls poster="{$json->media->index->$id->media->thumbnail}" preload="none">
<source src="{$json->media->index->$id->media->mp4_url}" type="video/mp4">
</video>
<p>{$description}</p>
{$article}
EOD;
if ($description->find('div.date', 0)->children(0)) {
$description->find('div.date', 0)->children(0)->outertext = '';
}
$this->items[] = $item;
$item['content'] = $this->processContent(
$description
);
if (count($this->items) >= 10) {
break;
}
}
}
$item['timestamp'] = $this->processDate($description);
$item['enclosures'][] = $this->videoImage;
public function getURI()
{
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category');
}
$this->items[] = $item;
return parent::getURI();
}
if (count($this->items) >= 5) {
break;
}
}
}
public function getName()
{
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
return $this->getKey('category') . ' - ' .
$this->getKey('edition') . ' - Brut.';
}
public function getURI() {
return parent::getName();
}
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category');
}
/**
* Extract JSON from page
*/
private function extractJson($html)
{
if (!preg_match($this->jsonRegex, $html, $parts)) {
returnServerError('Failed to extract data from page');
}
return parent::getURI();
}
$data = json_decode($parts[1]);
public function getName() {
if ($data === false) {
returnServerError('Failed to decode extracted data');
}
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
$parameters = $this->getParameters();
$editionValues = array_flip($parameters[0]['edition']['values']);
$categoryValues = array_flip($parameters[0]['category']['values']);
return $categoryValues[$this->getInput('category')] . ' - ' .
$editionValues[$this->getInput('edition')] . ' - Brut.';
}
return parent::getName();
}
private function processDate($description) {
if ($this->getInput('edition') === 'uk') {
$date = DateTime::createFromFormat('d/m/Y H:i', $description->find('div.date', 0)->innertext);
return strtotime($date->format('Y-m-d H:i:s'));
}
return strtotime($description->find('div.date', 0)->innertext);
}
private function processContent($description) {
$content = '<video controls poster="' . $this->videoImage . '" preload="none">
<source src="https://content.brut.media/video/' . $this->videoId . '-' . $this->videoType . '-web.mp4"
type="video/mp4">
</video>';
$content .= '<p>' . $description->find('h2.mb-1', 0)->innertext . '</p>';
if ($description->find('div.text.pb-3', 0)->children(1)->class != 'date') {
$content .= '<p>' . $description->find('div.text.pb-3', 0)->children(1)->innertext . '</p>';
}
return $content;
}
private function processTwitterImage() {
/**
* Extract video ID + type from twitter image
*
* Example (wrapped):
* https://img.brut.media/thumbnail/
* the-life-of-rita-moreno-2cce75b5-d448-44d2-a97c-ca50d6470dd4-square.jpg
* ?ts=1559337892
*/
$fpath = parse_url($this->videoImage, PHP_URL_PATH);
$fname = basename($fpath);
$fname = substr($fname, 0, strrpos($fname, '.'));
$parts = explode('-', $fname);
if (end($parts) === 'auto') {
$key = array_search('auto', $parts);
unset($parts[$key]);
}
$this->videoId = implode('-', array_splice($parts, -6, 5));
$this->videoType = end($parts);
}
return $data;
}
}

198
bridges/BugzillaBridge.php Normal file
View File

@@ -0,0 +1,198 @@
<?php
class BugzillaBridge extends BridgeAbstract
{
const NAME = 'Bugzilla Bridge';
const URI = 'https://www.bugzilla.org/';
const DESCRIPTION = 'Bridge for any Bugzilla instance';
const MAINTAINER = 'Yaman Qalieh';
const PARAMETERS = [
'global' => [
'instance' => [
'name' => 'Instance URL',
'required' => true,
'exampleValue' => 'https://bugzilla.mozilla.org'
]
],
'Bug comments' => [
'id' => [
'name' => 'Bug tracking ID',
'type' => 'number',
'required' => true,
'title' => 'Insert bug tracking ID',
'exampleValue' => 121241
],
'limit' => [
'name' => 'Number of comments to return',
'type' => 'number',
'required' => false,
'title' => 'Specify number of comments to return',
'defaultValue' => -1
],
'skiptags' => [
'name' => 'Skip offtopic comments',
'type' => 'checkbox',
'title' => 'Excludes comments tagged as advocacy, metoo, or offtopic from the feed'
]
]
];
const SKIPPED_ACTIVITY = [
'cc' => true,
'comment_tag' => true
];
const SKIPPED_TAGS = ['advocacy', 'metoo', 'offtopic'];
private $instance;
private $bugid;
private $buguri;
private $title;
public function getName()
{
if (!is_null($this->title)) {
return $this->title;
}
return parent::getName();
}
public function getURI()
{
return $this->buguri ?? parent::getURI();
}
public function collectData()
{
$this->instance = rtrim($this->getInput('instance'), '/');
$this->bugid = $this->getInput('id');
$this->buguri = $this->instance . '/show_bug.cgi?id=' . $this->bugid;
$url = $this->instance . '/rest/bug/' . $this->bugid;
$this->getTitle($url);
$this->collectComments($url . '/comment');
$this->collectUpdates($url . '/history');
usort($this->items, function ($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
if ($this->getInput('limit') > 0) {
$this->items = array_slice($this->items, 0, $this->getInput('limit'));
}
}
protected function getTitle($url)
{
// Only request the summary for a faster request
$json = self::getJSON($url . '?include_fields=summary');
$this->title = 'Bug ' . $this->bugid . ' - ' .
$json['bugs'][0]['summary'] . ' - ' .
// Remove https://
substr($this->instance, 8);
}
protected function collectComments($url)
{
$json = self::getJSON($url);
// Array of comments is here
if (!isset($json['bugs'][$this->bugid]['comments'])) {
returnClientError('Cannot find REST endpoint');
}
foreach ($json['bugs'][$this->bugid]['comments'] as $comment) {
$item = [];
if (
$this->getInput('skiptags') and
array_intersect(self::SKIPPED_TAGS, $comment['tags'])
) {
continue;
}
$item['categories'] = $comment['tags'];
$item['uri'] = $this->buguri . '#c' . $comment['count'];
$item['title'] = 'Comment ' . $comment['count'];
$item['timestamp'] = $comment['creation_time'];
$item['author'] = $this->getUser($comment['creator']);
$item['content'] = $comment['text'];
if (isset($comment['is_markdown']) and $comment['is_markdown']) {
$item['content'] = markdownToHtml($item['content']);
}
if (!is_null($comment['attachment_id'])) {
$item['enclosures'] = [$this->instance . '/attachment.cgi?id=' . $comment['attachment_id']];
}
$this->items[] = $item;
}
}
protected function collectUpdates($url)
{
$json = self::getJSON($url);
// Array of changesets which contain an array of changes
if (!isset($json['bugs']['0']['history'])) {
returnClientError('Cannot find REST endpoint');
}
foreach ($json['bugs']['0']['history'] as $changeset) {
$author = $this->getUser($changeset['who']);
$timestamp = $changeset['when'];
foreach ($changeset['changes'] as $change) {
// Skip updates to the cc list and comment tagging
if (isset(self::SKIPPED_ACTIVITY[$change['field_name']])) {
continue;
}
$item = [];
$item['uri'] = $this->buguri;
$item['title'] = 'Updated';
$item['timestamp'] = $timestamp;
$item['author'] = $author;
$item['content'] = ucfirst($change['field_name']) . ': ' .
($change['removed'] === '' ? '[nothing]' : $change['removed']) . ' -> ' .
($change['added'] === '' ? '[nothing]' : $change['added']);
$this->items[] = $item;
}
}
}
protected function getUser($user)
{
// Check if the user endpoint is available
if ($this->loadCacheValue($this->instance . 'userEndpointClosed', 86400)) {
return $user;
}
$cache = $this->loadCacheValue($this->instance . $user);
if (!is_null($cache)) {
return $cache;
}
$url = $this->instance . '/rest/user/' . $user . '?include_fields=real_name';
try {
$json = self::getJSON($url);
if (isset($json['error']) and $json['error']) {
throw new Exception();
}
} catch (Exception $e) {
$this->saveCacheValue($this->instance . 'userEndpointClosed', true);
return $user;
}
$username = $json['users']['0']['real_name'];
if (empty($username)) {
$username = $user;
}
$this->saveCacheValue($this->instance . $user, $username);
return $username;
}
protected static function getJSON($url)
{
$headers = [
'Accept: application/json',
];
return json_decode(getContents($url, $headers), true);
}
}

View File

@@ -2,217 +2,219 @@
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 NAME = 'Bukowskis';
const URI = 'https://www.bukowskis.com';
const DESCRIPTION = 'Fetches info about auction objects from Bukowskis auction house';
const MAINTAINER = 'Qluxzz';
const PARAMETERS = [[
'category' => [
'name' => 'Category',
'type' => 'list',
'values' => [
'All categories' => '',
'Art' => [
'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' => [
'All' => 'asian-ceramics-works-of-art',
'Other' => 'asian-ceramics-works-of-art.other',
'Porcelain' => 'asian-ceramics-works-of-art.porcelain',
],
'Books & Manuscripts' => [
'All' => 'books-manuscripts',
'Books' => 'books-manuscripts.books',
],
'Carpets, rugs & textiles' => [
'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' => [
'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' => [
'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' => [
'All' => 'design',
'Art glass' => 'design.art-glass',
'Furniture' => 'design.furniture',
'Other' => 'design.other',
],
'Folk art' => [
'All' => 'folk-art',
'All categories' => 'lots',
],
'Furniture' => [
'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' => [
'All' => 'glassware',
'Glassware' => 'glassware.glassware',
'Other' => 'glassware.other',
],
'Jewellery' => [
'All' => 'jewellery',
'Bracelets' => 'jewellery.bracelets',
'Brooches' => 'jewellery.brooches',
'Earrings' => 'jewellery.earrings',
'Necklaces & Pendants' => 'jewellery.necklaces-pendants',
'Other' => 'jewellery.other',
'Rings' => 'jewellery.rings',
],
'Lighting' => [
'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' => [
'All' => 'militaria',
'Honors & Medals' => 'militaria.honors-medals',
'Other militaria' => 'militaria.other-militaria',
'Weaponry' => 'militaria.weaponry',
],
'Miscellaneous' => [
'All' => 'miscellaneous',
'Brass, Copper & Pewter' => 'miscellaneous.brass-copper-pewter',
'Nickel silver' => 'miscellaneous.nickel-silver',
'Oriental' => 'miscellaneous.oriental',
'Other' => 'miscellaneous.other',
],
'Silver' => [
'All' => 'silver',
'Candle sticks' => 'silver.candle-sticks',
'Cups & Bowls' => 'silver.cups-bowls',
'Cutlery' => 'silver.cutlery',
'Other' => 'silver.other',
],
'Timepieces' => [
'All' => 'timepieces',
'Other' => 'timepieces.other',
'Pocket watches' => 'timepieces.pocket-watches',
'Table clocks' => 'timepieces.table-clocks',
'Wrist watches' => 'timepieces.wrist-watches',
],
'Vintage & Fashion' => [
'All' => 'vintage-fashion',
'Accessories' => 'vintage-fashion.accessories',
'Bags & Trunks' => 'vintage-fashion.bags-trunks',
'Clothes' => 'vintage-fashion.clothes',
],
]
],
'sort_order' => [
'name' => 'Sort order',
'type' => 'list',
'values' => [
'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' => [
'name' => 'Language',
'type' => 'list',
'values' => [
'English' => 'en',
'Swedish' => 'sv',
'Finnish' => 'fi'
],
],
]];
const CACHE_TIMEOUT = 3600; // 1 hour
const CACHE_TIMEOUT = 3600; // 1 hour
private $title;
private $title;
public function collectData()
{
$baseUrl = 'https://www.bukowskis.com';
$category = $this->getInput('category');
$language = $this->getInput('language');
$sort_order = $this->getInput('sort_order');
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';
$url = $baseUrl . '/' . $language . '/lots';
if ($category)
$url = $url . '/category/' . $category;
if ($category) {
$url = $url . '/category/' . $category;
}
if ($sort_order)
$url = $url . '/sort/' . $sort_order;
if ($sort_order) {
$url = $url . '/sort/' . $sort_order;
}
$html = getSimpleHTMLDOM($url);
$html = getSimpleHTMLDOM($url);
$this->title = htmlspecialchars_decode($html->find('title', 0)->innertext);
$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')
)
);
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),
);
}
}
$this->items[] = [
'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();
}
public function getName()
{
return $this->title ?: parent::getName();
}
}

View File

@@ -1,84 +1,88 @@
<?php
class BundesbankBridge extends BridgeAbstract {
const PARAM_LANG = 'lang';
class BundesbankBridge extends BridgeAbstract
{
const PARAM_LANG = 'lang';
const LANG_EN = 'en';
const LANG_DE = 'de';
const LANG_EN = 'en';
const LANG_DE = 'de';
const NAME = 'Bundesbank Bridge';
const URI = 'https://www.bundesbank.de/';
const DESCRIPTION = 'Returns the latest studies of the Bundesbank (Germany)';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 86400; // 24 hours
const NAME = 'Bundesbank Bridge';
const URI = 'https://www.bundesbank.de/';
const DESCRIPTION = 'Returns the latest studies of the Bundesbank (Germany)';
const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 86400; // 24 hours
const PARAMETERS = array(
array(
self::PARAM_LANG => array(
'name' => 'Language',
'type' => 'list',
'defaultValue' => self::LANG_DE,
'values' => array(
'English' => self::LANG_EN,
'Deutsch' => self::LANG_DE
)
)
)
);
const PARAMETERS = [
[
self::PARAM_LANG => [
'name' => 'Language',
'type' => 'list',
'defaultValue' => self::LANG_DE,
'values' => [
'English' => self::LANG_EN,
'Deutsch' => self::LANG_DE
]
]
]
];
public function getIcon() {
return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico';
}
public function getIcon()
{
return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico';
}
public function getURI() {
switch($this->getInput(self::PARAM_LANG)) {
case self::LANG_EN: return self::URI . 'en/publications/reports/studies';
case self::LANG_DE: return self::URI . 'de/publikationen/berichte/studien';
}
public function getURI()
{
switch ($this->getInput(self::PARAM_LANG)) {
case self::LANG_EN:
return self::URI . 'en/publications/reports/studies';
case self::LANG_DE:
return self::URI . 'de/publikationen/berichte/studien';
}
return parent::getURI();
}
return parent::getURI();
}
public function collectData() {
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
$html = getSimpleHTMLDOM($this->getURI());
$html = defaultLinkTo($html, $this->getURI());
$html = defaultLinkTo($html, $this->getURI());
foreach ($html->find('ul.resultlist li') as $study) {
$item = [];
foreach($html->find('ul.resultlist li') as $study) {
$item = array();
$item['uri'] = $study->find('.teasable__link', 0)->href;
$item['uri'] = $study->find('.teasable__link', 0)->href;
// Get title without child elements (i.e. subtitle)
$title = $study->find('.teasable__title div.h2', 0);
// Get title without child elements (i.e. subtitle)
$title = $study->find('.teasable__title div.h2', 0);
foreach ($title->children as &$child) {
$child->outertext = '';
}
foreach($title->children as &$child) {
$child->outertext = '';
}
$item['title'] = $title->innertext;
$item['title'] = $title->innertext;
// Add subtitle to the content if it exists
$item['content'] = '';
// Add subtitle to the content if it exists
$item['content'] = '';
if ($subtitle = $study->find('.teasable__subtitle', 0)) {
$item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>';
}
if($subtitle = $study->find('.teasable__subtitle', 0)) {
$item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>';
}
$item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>';
$item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>';
$item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext);
$item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext);
// Downloads and older studies don't have images
if ($study->find('.teasable__image', 0)) {
$item['enclosures'] = [
$study->find('.teasable__image img', 0)->src
];
}
// Downloads and older studies don't have images
if($study->find('.teasable__image', 0)) {
$item['enclosures'] = array(
$study->find('.teasable__image img', 0)->src
);
}
$this->items[] = $item;
}
}
$this->items[] = $item;
}
}
}

View File

@@ -1,89 +1,94 @@
<?php
class BundestagParteispendenBridge extends BridgeAbstract {
const MAINTAINER = 'mibe';
const NAME = 'Deutscher Bundestag - Parteispenden';
const URI = 'https://www.bundestag.de/parlament/praesidium/parteienfinanzierung/fundstellen50000';
const CACHE_TIMEOUT = 86400; // 24h
const DESCRIPTION = 'Returns the latest "soft money" donations to parties represented in the German Bundestag.';
const CONTENT_TEMPLATE = <<<TMPL
class BundestagParteispendenBridge extends BridgeAbstract
{
const MAINTAINER = 'mibe';
const NAME = 'Deutscher Bundestag - Parteispenden';
const URI = 'https://www.bundestag.de/parlament/praesidium/parteienfinanzierung/fundstellen50000';
const CACHE_TIMEOUT = 86400; // 24h
const DESCRIPTION = 'Returns the latest "soft money" donations to parties represented in the German Bundestag.';
const CONTENT_TEMPLATE = <<<TMPL
<p><b>Partei:</b><br>%s</p>
<p><b>Spendenbetrag:</b><br>%s</p>
<p><b>Spender:</b><br>%s</p>
<p><b>Eingang der Spende:</b><br>%s</p>
TMPL;
public function getIcon()
{
return 'https://www.bundestag.de/static/appdata/includes/images/layout/favicon.ico';
}
public function getIcon()
{
return 'https://www.bundestag.de/static/appdata/includes/images/layout/favicon.ico';
}
public function collectData()
{
$ajaxUri = <<<URI
public function collectData()
{
$ajaxUri = <<<URI
https://www.bundestag.de/ajax/filterlist/de/parlament/praesidium/parteienfinanzierung/fundstellen50000/462002-462002
URI;
// Get the main page
$html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT)
or returnServerError('Could not request AJAX list.');
// Get the main page
$html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT)
or returnServerError('Could not request AJAX list.');
// Build the URL from the first anchor element. The list is sorted by year, descending, so the first element is the current year.
$firstAnchor = $html->find('a', 0)
or returnServerError('Could not find the proper HTML element.');
// Build the URL from the first anchor element. The list is sorted by year, descending, so the first element is the current year.
$firstAnchor = $html->find('a', 0)
or returnServerError('Could not find the proper HTML element.');
$url = 'https://www.bundestag.de' . $firstAnchor->href;
$url = 'https://www.bundestag.de' . $firstAnchor->href;
// Get the actual page with the soft money donations
$html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT)
or returnServerError('Could not request ' . $url);
// Get the actual page with the soft money donations
$html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT)
or returnServerError('Could not request ' . $url);
$rows = $html->find('table.table > tbody > tr')
or returnServerError('Could not find the proper HTML elements.');
$rows = $html->find('table.table > tbody > tr')
or returnServerError('Could not find the proper HTML elements.');
foreach($rows as $row) {
$item = $this->generateItemFromRow($row);
if (is_array($item)) {
$item['uri'] = $url;
$this->items[] = $item;
}
}
}
foreach ($rows as $row) {
$item = $this->generateItemFromRow($row);
if (is_array($item)) {
$item['uri'] = $url;
$this->items[] = $item;
}
}
}
private function generateItemFromRow(simple_html_dom_node $row)
{
// The row must have 5 columns. There are monthly header rows, which are ignored here.
if(count($row->children) != 5)
return null;
private function generateItemFromRow(simple_html_dom_node $row)
{
// The row must have 5 columns. There are monthly header rows, which are ignored here.
if (count($row->children) != 5) {
return null;
}
$item = array();
$item = [];
// | column | paragraph inside column
$party = $row->children[0]->children[0]->innertext;
$amount = $row->children[1]->children[0]->innertext . ' €';
$donor = $row->children[2]->children[0]->innertext;
$date = $row->children[3]->children[0]->innertext;
$dip = $row->children[4]->children[0]->find('a.dipLink', 0);
// | column | paragraph inside column
$party = $row->children[0]->children[0]->innertext;
$amount = $row->children[1]->children[0]->innertext . ' €';
$donor = $row->children[2]->children[0]->innertext;
$date = $row->children[3]->children[0]->innertext;
$dip = $row->children[4]->children[0]->find('a.dipLink', 0);
// Strip whitespace from date string.
$date = str_replace(' ', '', $date);
// Strip whitespace from date string.
$date = str_replace(' ', '', $date);
$content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date);
$content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date);
$item = array(
'title' => $party . ': ' . $amount,
'content' => $content,
'uid' => sha1($content),
);
$item = [
'title' => $party . ': ' . $amount,
'content' => $content,
'uid' => sha1($content),
];
// Try to get the link to the official document
if ($dip != null)
$item['enclosures'] = array($dip->href);
// Try to get the link to the official document
if ($dip != null) {
$item['enclosures'] = [$dip->href];
}
// Try to parse the date
$dateTime = DateTime::createFromFormat('d.m.Y', $date);
if ($dateTime !== false)
$item['timestamp'] = $dateTime->getTimestamp();
// Try to parse the date
$dateTime = DateTime::createFromFormat('d.m.Y', $date);
if ($dateTime !== false) {
$item['timestamp'] = $dateTime->getTimestamp();
}
return $item;
}
return $item;
}
}

View File

@@ -1,36 +1,38 @@
<?php
class CBCEditorsBlogBridge extends BridgeAbstract {
const MAINTAINER = 'quickwick';
const NAME = 'CBC Editors Blog';
const URI = 'https://www.cbc.ca/news/editorsblog';
const DESCRIPTION = 'Recent CBC Editor\'s Blog posts';
class CBCEditorsBlogBridge extends BridgeAbstract
{
const MAINTAINER = 'quickwick';
const NAME = 'CBC Editors Blog';
const URI = 'https://www.cbc.ca/news/editorsblog';
const DESCRIPTION = 'Recent CBC Editor\'s Blog posts';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI);
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI);
// Loop on each blog post entry
foreach($html->find('div.contentListCards', 0)->find('a[data-test=type-story]') as $element) {
$headline = ($element->find('.headline', 0))->innertext;
$timestamp = ($element->find('time', 0))->datetime;
$articleUri = 'https://www.cbc.ca' . $element->href;
$summary = ($element->find('div.description', 0))->innertext;
$thumbnailUris = ($element->find('img[loading=lazy]', 0))->srcset;
$thumbnailUri = rtrim(explode(',', $thumbnailUris)[0], ' 300w');
// Loop on each blog post entry
foreach ($html->find('div.contentListCards', 0)->find('a[data-test=type-story]') as $element) {
$headline = ($element->find('.headline', 0))->innertext;
$timestamp = ($element->find('time', 0))->datetime;
$articleUri = 'https://www.cbc.ca' . $element->href;
$summary = ($element->find('div.description', 0))->innertext;
$thumbnailUris = ($element->find('img[loading=lazy]', 0))->srcset;
$thumbnailUri = rtrim(explode(',', $thumbnailUris)[0], ' 300w');
// Fill item
$item = array();
$item['uri'] = $articleUri;
$item['id'] = $item['uri'];
$item['timestamp'] = $timestamp;
$item['title'] = $headline;
$item['content'] = '<img src="'
. $thumbnailUri . '" /><br>' . $summary;
$item['author'] = 'Editor\'s Blog';
// Fill item
$item = [];
$item['uri'] = $articleUri;
$item['id'] = $item['uri'];
$item['timestamp'] = $timestamp;
$item['title'] = $headline;
$item['content'] = '<img src="'
. $thumbnailUri . '" /><br>' . $summary;
$item['author'] = 'Editor\'s Blog';
if(isset($item['title'])) {
$this->items[] = $item;
}
}
}
if (isset($item['title'])) {
$this->items[] = $item;
}
}
}
}

View File

@@ -1,108 +1,114 @@
<?php
class CNETBridge extends BridgeAbstract {
const MAINTAINER = 'ORelio';
const NAME = 'CNET News';
const URI = 'https://www.cnet.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns the newest articles.';
const PARAMETERS = array(
array(
'topic' => array(
'name' => 'Topic',
'type' => 'list',
'values' => array(
'All articles' => '',
'Apple' => 'apple',
'Google' => 'google',
'Microsoft' => 'tags-microsoft',
'Computers' => 'topics-computers',
'Mobile' => 'topics-mobile',
'Sci-Tech' => 'topics-sci-tech',
'Security' => 'topics-security',
'Internet' => 'topics-internet',
'Tech Industry' => 'topics-tech-industry'
)
)
)
);
class CNETBridge extends BridgeAbstract
{
const MAINTAINER = 'ORelio';
const NAME = 'CNET News';
const URI = 'https://www.cnet.com/';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns the newest articles.';
const PARAMETERS = [
[
'topic' => [
'name' => 'Topic',
'type' => 'list',
'values' => [
'All articles' => '',
'Apple' => 'apple',
'Google' => 'google',
'Microsoft' => 'tags-microsoft',
'Computers' => 'topics-computers',
'Mobile' => 'topics-mobile',
'Sci-Tech' => 'topics-sci-tech',
'Security' => 'topics-security',
'Internet' => 'topics-internet',
'Tech Industry' => 'topics-tech-industry'
]
]
]
];
private function cleanArticle($article_html) {
$offset_p = strpos($article_html, '<p>');
$offset_figure = strpos($article_html, '<figure');
$offset = ($offset_figure < $offset_p ? $offset_figure : $offset_p);
$article_html = substr($article_html, $offset);
$article_html = str_replace('href="/', 'href="' . self::URI, $article_html);
$article_html = str_replace(' height="0"', '', $article_html);
$article_html = str_replace('<noscript>', '', $article_html);
$article_html = str_replace('</noscript>', '', $article_html);
$article_html = StripWithDelimiters($article_html, '<a class="clickToEnlarge', '</a>');
$article_html = stripWithDelimiters($article_html, '<span class="nowPlaying', '</span>');
$article_html = stripWithDelimiters($article_html, '<span class="duration', '</span>');
$article_html = stripWithDelimiters($article_html, '<script', '</script>');
$article_html = stripWithDelimiters($article_html, '<svg', '</svg>');
return $article_html;
}
private function cleanArticle($article_html)
{
$offset_p = strpos($article_html, '<p>');
$offset_figure = strpos($article_html, '<figure');
$offset = ($offset_figure < $offset_p ? $offset_figure : $offset_p);
$article_html = substr($article_html, $offset);
$article_html = str_replace('href="/', 'href="' . self::URI, $article_html);
$article_html = str_replace(' height="0"', '', $article_html);
$article_html = str_replace('<noscript>', '', $article_html);
$article_html = str_replace('</noscript>', '', $article_html);
$article_html = StripWithDelimiters($article_html, '<a class="clickToEnlarge', '</a>');
$article_html = stripWithDelimiters($article_html, '<span class="nowPlaying', '</span>');
$article_html = stripWithDelimiters($article_html, '<span class="duration', '</span>');
$article_html = stripWithDelimiters($article_html, '<script', '</script>');
$article_html = stripWithDelimiters($article_html, '<svg', '</svg>');
return $article_html;
}
public function collectData() {
public function collectData()
{
// Retrieve and check user input
$topic = str_replace('-', '/', $this->getInput('topic'));
if (!empty($topic) && (substr_count($topic, '/') > 1 || !ctype_alpha(str_replace('/', '', $topic)))) {
returnClientError('Invalid topic: ' . $topic);
}
// Retrieve and check user input
$topic = str_replace('-', '/', $this->getInput('topic'));
if (!empty($topic) && (substr_count($topic, '/') > 1 || !ctype_alpha(str_replace('/', '', $topic))))
returnClientError('Invalid topic: ' . $topic);
// Retrieve webpage
$pageUrl = self::URI . (empty($topic) ? 'news/' : $topic . '/');
$html = getSimpleHTMLDOM($pageUrl);
// Retrieve webpage
$pageUrl = self::URI . (empty($topic) ? 'news/' : $topic . '/');
$html = getSimpleHTMLDOM($pageUrl);
// Process articles
foreach ($html->find('div.assetBody, div.riverPost') as $element) {
if (count($this->items) >= 10) {
break;
}
// Process articles
foreach($html->find('div.assetBody, div.riverPost') as $element) {
$article_title = trim($element->find('h2, h3', 0)->plaintext);
$article_uri = self::URI . substr($element->find('a', 0)->href, 1);
$article_thumbnail = $element->parent()->find('img[src]', 0)->src;
$article_timestamp = strtotime($element->find('time.assetTime, div.timeAgo', 0)->plaintext);
$article_author = trim($element->find('a[rel=author], a.name', 0)->plaintext);
$article_content = '<p><b>' . trim($element->find('p.dek', 0)->plaintext) . '</b></p>';
if(count($this->items) >= 10) {
break;
}
if (is_null($article_thumbnail)) {
$article_thumbnail = extractFromDelimiters($element->innertext, '<img src="', '"');
}
$article_title = trim($element->find('h2, h3', 0)->plaintext);
$article_uri = self::URI . substr($element->find('a', 0)->href, 1);
$article_thumbnail = $element->parent()->find('img[src]', 0)->src;
$article_timestamp = strtotime($element->find('time.assetTime, div.timeAgo', 0)->plaintext);
$article_author = trim($element->find('a[rel=author], a.name', 0)->plaintext);
$article_content = '<p><b>' . trim($element->find('p.dek', 0)->plaintext) . '</b></p>';
if (!empty($article_title) && !empty($article_uri) && strpos($article_uri, self::URI . 'news/') !== false) {
$article_html = getSimpleHTMLDOMCached($article_uri) or $article_html = null;
if (is_null($article_thumbnail))
$article_thumbnail = extractFromDelimiters($element->innertext, '<img src="', '"');
if (!is_null($article_html)) {
if (empty($article_thumbnail)) {
$article_thumbnail = $article_html->find('div.originalImage', 0);
}
if (empty($article_thumbnail)) {
$article_thumbnail = $article_html->find('span.imageContainer', 0);
}
if (is_object($article_thumbnail)) {
$article_thumbnail = $article_thumbnail->find('img', 0)->src;
}
if (!empty($article_title) && !empty($article_uri) && strpos($article_uri, self::URI . 'news/') !== false) {
$article_content .= trim(
$this->cleanArticle(
extractFromDelimiters(
$article_html,
'<article',
'<footer'
)
)
);
}
$article_html = getSimpleHTMLDOMCached($article_uri) or $article_html = null;
if (!is_null($article_html)) {
if (empty($article_thumbnail))
$article_thumbnail = $article_html->find('div.originalImage', 0);
if (empty($article_thumbnail))
$article_thumbnail = $article_html->find('span.imageContainer', 0);
if (is_object($article_thumbnail))
$article_thumbnail = $article_thumbnail->find('img', 0)->src;
$article_content .= trim(
$this->cleanArticle(
extractFromDelimiters(
$article_html, '<article', '<footer'
)
)
);
}
$item = array();
$item['uri'] = $article_uri;
$item['title'] = $article_title;
$item['author'] = $article_author;
$item['timestamp'] = $article_timestamp;
$item['enclosures'] = array($article_thumbnail);
$item['content'] = $article_content;
$this->items[] = $item;
}
}
}
$item = [];
$item['uri'] = $article_uri;
$item['title'] = $article_title;
$item['author'] = $article_author;
$item['timestamp'] = $article_timestamp;
$item['enclosures'] = [$article_thumbnail];
$item['content'] = $article_content;
$this->items[] = $item;
}
}
}
}

View File

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

View File

@@ -7,134 +7,139 @@
// it is not reliable and contain no useful information. This bridge create a
// sane feed with additional information like tags and a link to the CWE
// a description of the vulnerability.
class CVEDetailsBridge extends BridgeAbstract {
const MAINTAINER = 'Aaron Fischer';
const NAME = 'CVE Details';
const CACHE_TIMEOUT = 60 * 60 * 6; // 6 hours
const DESCRIPTION = 'Report new CVE vulnerabilities for a given vendor (and product)';
const URI = 'https://www.cvedetails.com';
class CVEDetailsBridge extends BridgeAbstract
{
const MAINTAINER = 'Aaron Fischer';
const NAME = 'CVE Details';
const CACHE_TIMEOUT = 60 * 60 * 6; // 6 hours
const DESCRIPTION = 'Report new CVE vulnerabilities for a given vendor (and product)';
const URI = 'https://www.cvedetails.com';
const PARAMETERS = array(array(
// The Vendor ID can be taken from the URL
'vendor_id' => array(
'name' => 'Vendor ID',
'type' => 'number',
'required' => true,
'exampleValue' => 74, // PHP
),
// The optional Product ID can be taken from the URL as well
'product_id' => array(
'name' => 'Product ID',
'type' => 'number',
'required' => false,
'exampleValue' => 128, // PHP
),
));
const PARAMETERS = [[
// The Vendor ID can be taken from the URL
'vendor_id' => [
'name' => 'Vendor ID',
'type' => 'number',
'required' => true,
'exampleValue' => 74, // PHP
],
// The optional Product ID can be taken from the URL as well
'product_id' => [
'name' => 'Product ID',
'type' => 'number',
'required' => false,
'exampleValue' => 128, // PHP
],
]];
private $html = null;
private $vendor = '';
private $product = '';
private $html = null;
private $vendor = '';
private $product = '';
// Return the URL to query.
// Because of the optional product ID, we need to attach it if it is
// set. The search result page has the exact same structure (with and
// without the product ID).
private function _buildURL() {
$url = self::URI . '/vulnerability-list/vendor_id-' . $this->getInput('vendor_id');
if ($this->getInput('product_id') !== '') {
$url .= '/product_id-' . $this->getInput('product_id');
}
// Sadly, there is no way (prove me wrong please) to sort the search
// result by publish date. So the nearest alternative is the CVE
// number, which should be mostly accurate.
$url .= '?order=1'; // Order by CVE number DESC
// Return the URL to query.
// Because of the optional product ID, we need to attach it if it is
// set. The search result page has the exact same structure (with and
// without the product ID).
private function buildUrl()
{
$url = self::URI . '/vulnerability-list/vendor_id-' . $this->getInput('vendor_id');
if ($this->getInput('product_id') !== '') {
$url .= '/product_id-' . $this->getInput('product_id');
}
// Sadly, there is no way (prove me wrong please) to sort the search
// result by publish date. So the nearest alternative is the CVE
// number, which should be mostly accurate.
$url .= '?order=1'; // Order by CVE number DESC
return $url;
}
return $url;
}
// Make the actual request to cvedetails.com and stores the response
// (HTML) for later use and extract vendor and product from it.
private function _fetchContent() {
$html = getSimpleHTMLDOM($this->_buildURL());
$this->html = defaultLinkTo($html, self::URI);
// Make the actual request to cvedetails.com and stores the response
// (HTML) for later use and extract vendor and product from it.
private function fetchContent()
{
$html = getSimpleHTMLDOM($this->buildUrl());
$this->html = defaultLinkTo($html, self::URI);
$vendor = $html->find('#contentdiv > h1 > a', 0);
if ($vendor == null) {
returnServerError('Invalid Vendor ID ' .
$this->getInput('vendor_id') .
' or Product ID ' .
$this->getInput('product_id'));
}
$this->vendor = $vendor->innertext;
$vendor = $html->find('#contentdiv > h1 > a', 0);
if ($vendor == null) {
returnServerError('Invalid Vendor ID ' .
$this->getInput('vendor_id') .
' or Product ID ' .
$this->getInput('product_id'));
}
$this->vendor = $vendor->innertext;
$product = $html->find('#contentdiv > h1 > a', 1);
if ($product != null) {
$this->product = $product->innertext;
}
}
$product = $html->find('#contentdiv > h1 > a', 1);
if ($product != null) {
$this->product = $product->innertext;
}
}
// Build the name of the feed.
public function getName() {
if ($this->getInput('vendor_id') == '') {
return self::NAME;
}
// Build the name of the feed.
public function getName()
{
if ($this->getInput('vendor_id') == '') {
return self::NAME;
}
if ($this->html == null) {
$this->_fetchContent();
}
if ($this->html == null) {
$this->fetchContent();
}
$name = 'CVE Vulnerabilities for ' . $this->vendor;
if ($this->product != '') {
$name .= '/' . $this->product;
}
$name = 'CVE Vulnerabilities for ' . $this->vendor;
if ($this->product != '') {
$name .= '/' . $this->product;
}
return $name;
}
return $name;
}
// Pull the data from the HTML response and fill the items..
public function collectData() {
if ($this->html == null) {
$this->_fetchContent();
}
// Pull the data from the HTML response and fill the items..
public function collectData()
{
if ($this->html == null) {
$this->fetchContent();
}
foreach ($this->html->find('#vulnslisttable .srrowns') as $i => $tr) {
// There are some optional vulnerability types, which will be
// added to the categories as well as the CWE number -- which is
// always given.
$categories = array($this->vendor);
$enclosures = array();
foreach ($this->html->find('#vulnslisttable .srrowns') as $i => $tr) {
// There are some optional vulnerability types, which will be
// added to the categories as well as the CWE number -- which is
// always given.
$categories = [$this->vendor];
$enclosures = [];
$cwe = $tr->find('td', 2)->find('a', 0);
if ($cwe != null) {
$cwe = $cwe->innertext;
$categories[] = 'CWE-' . $cwe;
$enclosures[] = 'https://cwe.mitre.org/data/definitions/' . $cwe . '.html';
}
$c = $tr->find('td', 4)->innertext;
if (trim($c) != '') {
$categories[] = $c;
}
if ($this->product != '') {
$categories[] = $this->product;
}
$cwe = $tr->find('td', 2)->find('a', 0);
if ($cwe != null) {
$cwe = $cwe->innertext;
$categories[] = 'CWE-' . $cwe;
$enclosures[] = 'https://cwe.mitre.org/data/definitions/' . $cwe . '.html';
}
$c = $tr->find('td', 4)->innertext;
if (trim($c) != '') {
$categories[] = $c;
}
if ($this->product != '') {
$categories[] = $this->product;
}
// The CVE number itself
$title = $tr->find('td', 1)->find('a', 0)->innertext;
// The CVE number itself
$title = $tr->find('td', 1)->find('a', 0)->innertext;
$this->items[] = array(
'uri' => $tr->find('td', 1)->find('a', 0)->href,
'title' => $title,
'timestamp' => $tr->find('td', 5)->innertext,
'content' => $tr->next_sibling()->innertext,
'categories' => $categories,
'enclosures' => $enclosures,
'uid' => $tr->find('td', 1)->find('a', 0)->innertext,
);
$this->items[] = [
'uri' => $tr->find('td', 1)->find('a', 0)->href,
'title' => $title,
'timestamp' => $tr->find('td', 5)->innertext,
'content' => $tr->next_sibling()->innertext,
'categories' => $categories,
'enclosures' => $enclosures,
'uid' => $tr->find('td', 1)->find('a', 0)->innertext,
];
// We only want to fetch the latest 10 CVEs
if (count($this->items) >= 10) {
break;
}
}
}
// We only want to fetch the latest 10 CVEs
if (count($this->items) >= 10) {
break;
}
}
}
}

View File

@@ -1,134 +1,138 @@
<?php
class CachetBridge extends BridgeAbstract {
const NAME = 'Cachet Bridge';
const URI = 'https://cachethq.io/';
const DESCRIPTION = 'Returns status updates from any Cachet installation';
const MAINTAINER = 'klimplant';
const PARAMETERS = array(
array(
'host' => array(
'name' => 'Cachet installation',
'type' => 'text',
'required' => true,
'title' => 'The URL of the Cachet installation',
'exampleValue' => 'https://demo.cachethq.io/',
), 'additional_info' => array(
'name' => 'Additional Timestamps',
'type' => 'checkbox',
'title' => 'Whether to include the given timestamps'
)
)
);
const CACHE_TIMEOUT = 300;
class CachetBridge extends BridgeAbstract
{
const NAME = 'Cachet Bridge';
const URI = 'https://cachethq.io/';
const DESCRIPTION = 'Returns status updates from any Cachet installation';
const MAINTAINER = 'klimplant';
const PARAMETERS = [
[
'host' => [
'name' => 'Cachet installation',
'type' => 'text',
'required' => true,
'title' => 'The URL of the Cachet installation',
'exampleValue' => 'https://demo.cachethq.io/',
], 'additional_info' => [
'name' => 'Additional Timestamps',
'type' => 'checkbox',
'title' => 'Whether to include the given timestamps'
]
]
];
const CACHE_TIMEOUT = 300;
private $componentCache = array();
private $componentCache = [];
public function getURI() {
return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host');
}
public function getURI()
{
return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host');
}
/**
* Validates the ping request to the cache API
*
* @param string $ping
* @return boolean
*/
private function validatePing($ping) {
$ping = json_decode($ping);
if ($ping === null) {
return false;
}
return $ping->data === 'Pong!';
}
/**
* Validates the ping request to the cache API
*
* @param string $ping
* @return boolean
*/
private function validatePing($ping)
{
$ping = json_decode($ping);
if ($ping === null) {
return false;
}
return $ping->data === 'Pong!';
}
/**
* Returns the component name of a cachat component
*
* @param integer $id
* @return string
*/
private function getComponentName($id) {
if ($id === 0) {
return '';
}
if (array_key_exists($id, $this->componentCache)) {
return $this->componentCache[$id];
}
/**
* Returns the component name of a cachat component
*
* @param integer $id
* @return string
*/
private function getComponentName($id)
{
if ($id === 0) {
return '';
}
if (array_key_exists($id, $this->componentCache)) {
return $this->componentCache[$id];
}
$component = getContents($this->getURI() . '/api/v1/components/' . $id);
$component = json_decode($component);
if ($component === null) {
return '';
}
return $component->data->name;
}
$component = getContents($this->getURI() . '/api/v1/components/' . $id);
$component = json_decode($component);
if ($component === null) {
return '';
}
return $component->data->name;
}
public function collectData() {
$ping = getContents(urljoin($this->getURI(), '/api/v1/ping'));
if (!$this->validatePing($ping)) {
returnClientError('Provided URI is invalid!');
}
public function collectData()
{
$ping = getContents(urljoin($this->getURI(), '/api/v1/ping'));
if (!$this->validatePing($ping)) {
returnClientError('Provided URI is invalid!');
}
$url = urljoin($this->getURI(), '/api/v1/incidents?sort=id&order=desc');
$incidents = getContents($url);
$incidents = json_decode($incidents);
if ($incidents === null) {
returnClientError('/api/v1/incidents returned no valid json');
}
$url = urljoin($this->getURI(), '/api/v1/incidents?sort=id&order=desc');
$incidents = getContents($url);
$incidents = json_decode($incidents);
if ($incidents === null) {
returnClientError('/api/v1/incidents returned no valid json');
}
usort($incidents->data, function ($a, $b) {
$timeA = strtotime($a->updated_at);
$timeB = strtotime($b->updated_at);
return $timeA > $timeB ? -1 : 1;
});
usort($incidents->data, function ($a, $b) {
$timeA = strtotime($a->updated_at);
$timeB = strtotime($b->updated_at);
return $timeA > $timeB ? -1 : 1;
});
foreach ($incidents->data as $incident) {
foreach ($incidents->data as $incident) {
if (isset($incident->permalink)) {
$permalink = $incident->permalink;
} else {
$permalink = urljoin($this->getURI(), '/incident/' . $incident->id);
}
if (isset($incident->permalink)) {
$permalink = $incident->permalink;
} else {
$permalink = urljoin($this->getURI(), '/incident/' . $incident->id);
}
$title = $incident->human_status . ': ' . $incident->name;
$message = '';
if ($this->getInput('additional_info')) {
if (isset($incident->occurred_at)) {
$message .= 'Occurred at: ' . $incident->occurred_at . "\r\n";
}
if (isset($incident->scheduled_at)) {
$message .= 'Scheduled at: ' . $incident->scheduled_at . "\r\n";
}
if (isset($incident->created_at)) {
$message .= 'Created at: ' . $incident->created_at . "\r\n";
}
if (isset($incident->updated_at)) {
$message .= 'Updated at: ' . $incident->updated_at . "\r\n\r\n";
}
}
$title = $incident->human_status . ': ' . $incident->name;
$message = '';
if ($this->getInput('additional_info')) {
if (isset($incident->occurred_at)) {
$message .= 'Occurred at: ' . $incident->occurred_at . "\r\n";
}
if (isset($incident->scheduled_at)) {
$message .= 'Scheduled at: ' . $incident->scheduled_at . "\r\n";
}
if (isset($incident->created_at)) {
$message .= 'Created at: ' . $incident->created_at . "\r\n";
}
if (isset($incident->updated_at)) {
$message .= 'Updated at: ' . $incident->updated_at . "\r\n\r\n";
}
}
$message .= $incident->message;
$content = nl2br($message);
$componentName = $this->getComponentName($incident->component_id);
$uidOrig = $permalink . $incident->created_at;
$uid = hash('sha512', $uidOrig);
$timestamp = strtotime($incident->created_at);
$categories = [];
$categories[] = $incident->human_status;
if ($componentName !== '') {
$categories[] = $componentName;
}
$message .= $incident->message;
$content = nl2br($message);
$componentName = $this->getComponentName($incident->component_id);
$uidOrig = $permalink . $incident->created_at;
$uid = hash('sha512', $uidOrig);
$timestamp = strtotime($incident->created_at);
$categories = array();
$categories[] = $incident->human_status;
if ($componentName !== '') {
$categories[] = $componentName;
}
$item = [];
$item['uri'] = $permalink;
$item['title'] = $title;
$item['timestamp'] = $timestamp;
$item['content'] = $content;
$item['uid'] = $uid;
$item['categories'] = $categories;
$item = array();
$item['uri'] = $permalink;
$item['title'] = $title;
$item['timestamp'] = $timestamp;
$item['content'] = $content;
$item['uid'] = $uid;
$item['categories'] = $categories;
$this->items[] = $item;
}
}
$this->items[] = $item;
}
}
}

View File

@@ -1,41 +1,44 @@
<?php
class CarThrottleBridge extends FeedExpander {
const NAME = 'Car Throttle ';
const URI = 'https://www.carthrottle.com';
const DESCRIPTION = 'Get the latest car-related news from Car Throttle.';
const MAINTAINER = 't0stiman';
public function collectData() {
$this->collectExpandableDatas('https://www.carthrottle.com/rss', 10);
}
class CarThrottleBridge extends FeedExpander
{
const NAME = 'Car Throttle ';
const URI = 'https://www.carthrottle.com';
const DESCRIPTION = 'Get the latest car-related news from Car Throttle.';
const MAINTAINER = 't0stiman';
protected function parseItem($feedItem) {
$item = parent::parseItem($feedItem);
public function collectData()
{
$this->collectExpandableDatas('https://www.carthrottle.com/rss', 10);
}
//fetch page
$articlePage = getSimpleHTMLDOMCached($feedItem->link)
or returnServerError('Could not retrieve ' . $feedItem->link);
protected function parseItem($feedItem)
{
$item = parent::parseItem($feedItem);
$subtitle = $articlePage->find('p.standfirst', 0);
$article = $articlePage->find('div.content_field', 0);
//fetch page
$articlePage = getSimpleHTMLDOMCached($feedItem->link)
or returnServerError('Could not retrieve ' . $feedItem->link);
$item['content'] = str_get_html($subtitle . $article);
$subtitle = $articlePage->find('p.standfirst', 0);
$article = $articlePage->find('div.content_field', 0);
//convert <iframe>s to <a>s. meant for embedded videos.
foreach($item['content']->find('iframe') as $found) {
$item['content'] = str_get_html($subtitle . $article);
$iframeUrl = $found->getAttribute('src');
//convert <iframe>s to <a>s. meant for embedded videos.
foreach ($item['content']->find('iframe') as $found) {
$iframeUrl = $found->getAttribute('src');
if ($iframeUrl) {
$found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>';
}
}
if ($iframeUrl) {
$found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>';
}
}
//remove scripts from the text
foreach ($item['content']->find('script') as $remove) {
$remove->outertext = '';
}
//remove scripts from the text
foreach ($item['content']->find('script') as $remove) {
$remove->outertext = '';
}
return $item;
}
return $item;
}
}

77
bridges/CaschyBridge.php Normal file
View File

@@ -0,0 +1,77 @@
<?php
class CaschyBridge extends FeedExpander
{
const MAINTAINER = 'Tone866';
const NAME = 'Caschys Blog Bridge';
const URI = 'https://stadt-bremerhaven.de/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns the full articles instead of only the intro';
const PARAMETERS = [[
'category' => [
'name' => 'Category',
'type' => 'list',
'values' => [
'Alle News'
=> 'https://stadt-bremerhaven.de/feed/'
]
],
'limit' => [
'name' => 'Limit',
'type' => 'number',
'required' => false,
'title' => 'Specify number of full articles to return',
'defaultValue' => 5
]
]];
const LIMIT = 5;
public function collectData()
{
$this->collectExpandableDatas(
$this->getInput('category'),
$this->getInput('limit') ?: static::LIMIT
);
}
protected function parseItem($feedItem)
{
$item = parent::parseItem($feedItem);
if (strpos($item['uri'], 'https://stadt-bremerhaven.de/') !== 0) {
return $item;
}
$article = getSimpleHTMLDOMCached($item['uri']);
if ($article) {
$article = defaultLinkTo($article, $item['uri']);
$item = $this->addArticleToItem($item, $article);
}
return $item;
}
private function addArticleToItem($item, $article)
{
// remove unwanted stuff
foreach (
$article->find('div.video-container, div.aawp, p.aawp-disclaimer, iframe.wp-embedded-content,
div.wp-embed, p.wp-caption-text, script') as $element
) {
$element->remove();
}
// reload html, as remove() is buggy
$article = str_get_html($article->outertext);
$categories = $article->find('div.post-category a');
foreach ($categories as $category) {
$item['categories'][] = $category->plaintext;
}
$content = $article->find('div.entry-inner', 0);
$item['content'] = $content;
return $item;
}
}

View File

@@ -1,118 +1,135 @@
<?php
class CastorusBridge extends BridgeAbstract {
const MAINTAINER = 'logmanoriginal';
const NAME = 'Castorus Bridge';
const URI = 'https://www.castorus.com';
const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'Returns the latest changes';
const PARAMETERS = array(
'Get latest changes' => array(),
'Get latest changes via ZIP code' => array(
'zip' => array(
'name' => 'ZIP code',
'type' => 'text',
'required' => true,
'exampleValue' => '7',
'title' => 'Insert ZIP code (complete or partial). e.g: 78125 OR 781 OR 7'
)
),
'Get latest changes via city name' => array(
'city' => array(
'name' => 'City name',
'type' => 'text',
'required' => true,
'exampleValue' => 'Paris',
'title' => 'Insert city name (complete or partial). e.g: Paris OR Par OR P'
)
)
);
class CastorusBridge extends BridgeAbstract
{
const MAINTAINER = 'logmanoriginal';
const NAME = 'Castorus Bridge';
const URI = 'https://www.castorus.com';
const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'Returns the latest changes';
// Extracts the title from an actitiy
private function extractActivityTitle($activity){
$title = $activity->find('a', 0);
const PARAMETERS = [
'Get latest changes' => [],
'Get latest changes via ZIP code' => [
'zip' => [
'name' => 'ZIP code',
'type' => 'text',
'required' => true,
'exampleValue' => '7',
'title' => 'Insert ZIP code (complete or partial). e.g: 78125 OR 781 OR 7'
]
],
'Get latest changes via city name' => [
'city' => [
'name' => 'City name',
'type' => 'text',
'required' => true,
'exampleValue' => 'Paris',
'title' => 'Insert city name (complete or partial). e.g: Paris OR Par OR P'
]
]
];
if(!$title)
returnServerError('Cannot find title!');
// Extracts the title from an actitiy
private function extractActivityTitle($activity)
{
$title = $activity->find('a', 0);
return htmlspecialchars(trim($title->plaintext));
}
if (!$title) {
returnServerError('Cannot find title!');
}
// Extracts the url from an actitiy
private function extractActivityUrl($activity){
$url = $activity->find('a', 0);
return trim($title->plaintext);
}
if(!$url)
returnServerError('Cannot find url!');
// Extracts the url from an actitiy
private function extractActivityUrl($activity)
{
$url = $activity->find('a', 0);
return self::URI . $url->href;
}
if (!$url) {
returnServerError('Cannot find url!');
}
// Extracts the time from an activity
private function extractActivityTime($activity){
// Unfortunately the time is part of the parent node,
// so we have to clear all child nodes first
$nodes = $activity->find('*');
return self::URI . $url->href;
}
if(!$nodes)
returnServerError('Cannot find nodes!');
// Extracts the time from an activity
private function extractActivityTime($activity)
{
// Unfortunately the time is part of the parent node,
// so we have to clear all child nodes first
$nodes = $activity->find('*');
foreach($nodes as $node) {
$node->outertext = '';
}
if (!$nodes) {
returnServerError('Cannot find nodes!');
}
return strtotime($activity->innertext);
}
foreach ($nodes as $node) {
$node->outertext = '';
}
// Extracts the price change
private function extractActivityPrice($activity){
$price = $activity->find('span', 1);
return strtotime($activity->innertext);
}
if(!$price)
returnServerError('Cannot find price!');
// Extracts the price change
private function extractActivityPrice($activity)
{
$price = $activity->find('span', 1);
return $price->innertext;
}
if (!$price) {
returnServerError('Cannot find price!');
}
public function collectData(){
$zip_filter = trim($this->getInput('zip'));
$city_filter = trim($this->getInput('city'));
return $price->innertext;
}
$html = getSimpleHTMLDOM(self::URI);
public function collectData()
{
$zip_filter = trim($this->getInput('zip'));
$city_filter = trim($this->getInput('city'));
if(!$html)
returnServerError('Could not load data from ' . self::URI . '!');
$html = getSimpleHTMLDOM(self::URI);
$activities = $html->find('div#activite > li');
if (!$html) {
returnServerError('Could not load data from ' . self::URI . '!');
}
if(!$activities)
returnServerError('Failed to find activities!');
$activities = $html->find('div#activite > li');
foreach($activities as $activity) {
$item = array();
if (!$activities) {
returnServerError('Failed to find activities!');
}
$item['title'] = $this->extractActivityTitle($activity);
$item['uri'] = $this->extractActivityUrl($activity);
$item['timestamp'] = $this->extractActivityTime($activity);
$item['content'] = '<a href="'
. $item['uri']
. '">'
. $item['title']
. '</a><br><p>'
. $this->extractActivityPrice($activity)
. '</p>';
foreach ($activities as $activity) {
$item = [];
if(isset($zip_filter)
&& !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter)) {
continue; // Skip this item
}
$item['title'] = $this->extractActivityTitle($activity);
$item['uri'] = $this->extractActivityUrl($activity);
$item['timestamp'] = $this->extractActivityTime($activity);
$item['content'] = '<a href="'
. $item['uri']
. '">'
. $item['title']
. '</a><br><p>'
. $this->extractActivityPrice($activity)
. '</p>';
if(isset($city_filter)
&& !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter)) {
continue; // Skip this item
}
if (
isset($zip_filter)
&& !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter)
) {
continue; // Skip this item
}
$this->items[] = $item;
}
}
if (
isset($city_filter)
&& !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter)
) {
continue; // Skip this item
}
$this->items[] = $item;
}
}
}

View File

@@ -1,60 +1,62 @@
<?php
class CdactionBridge extends BridgeAbstract {
const NAME = 'CD-ACTION bridge';
const URI = 'https://cdaction.pl';
const DESCRIPTION = 'Fetches the latest posts from given category.';
const MAINTAINER = 'tomaszkane';
const PARAMETERS = array( array(
'category' => array(
'name' => 'Kategoria',
'type' => 'list',
'values' => array(
'Najnowsze (wszystkie)' => 'najnowsze',
'Newsy' => 'newsy',
'Recenzje' => 'recenzje',
'Teksty' => array(
'Publicystyka' => 'publicystyka',
'Zapowiedzi' => 'zapowiedzi',
'Już graliśmy' => 'juz-gralismy',
'Poradniki' => 'poradniki',
),
'Kultura' => 'kultura',
'Wideo' => 'wideo',
'Czasopismo' => 'czasopismo',
'Technologie' => array(
'Artykuły' => 'artykuly',
'Testy' => 'testy',
),
'Na luzie' => array(
'Konkursy' => 'konkursy',
'Nadgodziny' => 'nadgodziny',
)
)
))
);
class CdactionBridge extends BridgeAbstract
{
const NAME = 'CD-ACTION bridge';
const URI = 'https://cdaction.pl';
const DESCRIPTION = 'Fetches the latest posts from given category.';
const MAINTAINER = 'tomaszkane';
const PARAMETERS = [ [
'category' => [
'name' => 'Kategoria',
'type' => 'list',
'values' => [
'Najnowsze (wszystkie)' => 'najnowsze',
'Newsy' => 'newsy',
'Recenzje' => 'recenzje',
'Teksty' => [
'Publicystyka' => 'publicystyka',
'Zapowiedzi' => 'zapowiedzi',
'Już graliśmy' => 'juz-gralismy',
'Poradniki' => 'poradniki',
],
'Kultura' => 'kultura',
'Wideo' => 'wideo',
'Czasopismo' => 'czasopismo',
'Technologie' => [
'Artykuły' => 'artykuly',
'Testy' => 'testy',
],
'Na luzie' => [
'Konkursy' => 'konkursy',
'Nadgodziny' => 'nadgodziny',
]
]
]]
];
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI() . '/' . $this->getInput('category'));
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI() . '/' . $this->getInput('category'));
$newsJson = $html->find('script#__NEXT_DATA__', 0)->innertext;
if (!$newsJson = json_decode($newsJson)) {
return;
}
$newsJson = $html->find('script#__NEXT_DATA__', 0)->innertext;
if (!$newsJson = json_decode($newsJson)) {
return;
}
$queriesIndex = $this->getInput('category') === 'najnowsze' ? 0 : 1;
foreach ($newsJson->props->pageProps->dehydratedState->queries[$queriesIndex]->state->data->results as $news) {
$item = array();
$item['uri'] = $this->getURI() . '/' . $news->category->slug . '/' . $news->slug;
$item['title'] = $news->title;
$item['timestamp'] = $news->publishedAt;
$item['author'] = $news->editor->fullName;
$item['content'] = $news->lead;
$item['enclosures'][] = $news->bannerUrl;
$item['categories'] = array_column($news->tags, 'name');
$item['uid'] = $news->id;
$queriesIndex = $this->getInput('category') === 'najnowsze' ? 0 : 1;
foreach ($newsJson->props->pageProps->dehydratedState->queries[$queriesIndex]->state->data->results as $news) {
$item = [];
$item['uri'] = $this->getURI() . '/' . $news->category->slug . '/' . $news->slug;
$item['title'] = $news->title;
$item['timestamp'] = $news->publishedAt;
$item['author'] = $news->editor->fullName;
$item['content'] = $news->lead;
$item['enclosures'][] = $news->bannerUrl;
$item['categories'] = array_column($news->tags, 'name');
$item['uid'] = $news->id;
$this->items[] = $item;
}
}
$this->items[] = $item;
}
}
}

View File

@@ -1,83 +1,88 @@
<?php
class CeskaTelevizeBridge extends BridgeAbstract {
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 NAME = 'Česká televize Bridge';
const URI = 'https://www.ceskatelevize.cz';
const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'Return newest videos';
const MAINTAINER = 'kolarcz';
const PARAMETERS = [
[
'url' => [
'name' => 'url to the show',
'required' => true,
'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/'
]
]
];
const PARAMETERS = array(
array(
'url' => array(
'name' => 'url to the show',
'required' => true,
'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/'
)
)
);
private function fixChars($text)
{
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
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');
}
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', $match[3] ?? date('Y'), $match[2], $match[1]);
return strtotime($date);
}
$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');
public function collectData() {
$url = $this->getInput('url');
$validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(bonus\/)?$/';
if (!preg_match($validUrl, $url, $match)) {
returnServerError('Invalid url');
}
$validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(bonus\/)?$/';
if (!preg_match($validUrl, $url, $match)) {
returnServerError('Invalid url');
}
$category = $match[4] ?? 'nove';
$fixedUrl = "{$match[1]}dily/{$category}/";
$category = isset($match[4]) ? $match[4] : 'nove';
$fixedUrl = "{$match[1]}dily/{$category}/";
$html = getSimpleHTMLDOM($fixedUrl);
$html = getSimpleHTMLDOM($fixedUrl);
$this->feedUri = $fixedUrl;
$this->feedName = str_replace('Přehled dílů — ', '', $this->fixChars($html->find('title', 0)->plaintext));
if ($category !== 'nove') {
$this->feedName .= " ({$category})";
}
$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('#episodeListSection a[data-testid=next-link]') as $element) {
$itemTitle = $element->find('h3', 0);
$itemContent = $element->find('div[class^=content-]', 0);
$itemDate = $element->find('div[class^=playTime-] span', 0);
$itemThumbnail = $element->find('img', 0);
$itemUri = self::URI . $element->getAttribute('href');
foreach ($html->find('#episodeListSection a[data-testid=next-link]') as $element) {
$itemTitle = $element->find('h3', 0);
$itemContent = $element->find('div[class^=content-]', 0);
$itemDate = $element->find('div[class^=playTime-] span', 0);
$itemThumbnail = $element->find('img', 0);
$itemUri = self::URI . $element->getAttribute('href');
$item = [
'title' => $this->fixChars($itemTitle->plaintext),
'uri' => $itemUri,
'content' => '<img src="' . $itemThumbnail->getAttribute('src') . '" /><br />'
. $this->fixChars($itemContent->plaintext),
'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext)
];
$item = array(
'title' => $this->fixChars($itemTitle->plaintext),
'uri' => $itemUri,
'content' => '<img src="' . $itemThumbnail->getAttribute('src') . '" /><br />'
. $this->fixChars($itemContent->plaintext),
'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext)
);
$this->items[] = $item;
}
}
$this->items[] = $item;
}
}
public function getURI()
{
return $this->feedUri ?? parent::getURI();
}
public function getURI() {
return isset($this->feedUri) ? $this->feedUri : parent::getURI();
}
public function getName() {
return isset($this->feedName) ? $this->feedName : parent::getName();
}
public function getName()
{
return $this->feedName ?? parent::getName();
}
}

View File

@@ -1,346 +1,314 @@
<?php
class CodebergBridge extends BridgeAbstract {
const NAME = 'Codeberg Bridge';
const URI = 'https://codeberg.org/';
const DESCRIPTION = 'Returns commits, issues, pull requests or releases for a repository.';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(
'Commits' => array(
'branch' => array(
'name' => 'branch',
'type' => 'text',
'exampleValue' => 'main',
'required' => false,
'title' => 'Optional, main branch is used by default.',
),
),
'Issues' => array(),
'Issue Comments' => array(
'issueId' => array(
'name' => 'Issue ID',
'type' => 'text',
'required' => true,
'exampleValue' => '513',
)
),
'Pull Requests' => array(),
'Releases' => array(),
'global' => array(
'username' => array(
'name' => 'Username',
'type' => 'text',
'exampleValue' => 'Codeberg',
'title' => 'Username of account that the repository belongs to.',
'required' => true,
),
'repo' => array(
'name' => 'Repository',
'type' => 'text',
'exampleValue' => 'Community',
'required' => true,
)
)
);
const CACHE_TIMEOUT = 1800;
class CodebergBridge extends BridgeAbstract
{
const NAME = 'Codeberg Bridge';
const URI = 'https://codeberg.org/';
const DESCRIPTION = 'Returns commits, issues, pull requests or releases for a repository.';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = [
'Commits' => [
'branch' => [
'name' => 'branch',
'type' => 'text',
'exampleValue' => 'main',
'required' => false,
'title' => 'Optional, main branch is used by default.',
],
],
'Issues' => [],
'Issue Comments' => [
'issueId' => [
'name' => 'Issue ID',
'type' => 'text',
'required' => true,
'exampleValue' => '513',
]
],
'Pull Requests' => [],
'Releases' => [],
'Tags' => [],
'global' => [
'username' => [
'name' => 'Username',
'type' => 'text',
'exampleValue' => 'Codeberg',
'title' => 'Username of account that the repository belongs to.',
'required' => true,
],
'repo' => [
'name' => 'Repository',
'type' => 'text',
'exampleValue' => 'Community',
'required' => true,
]
]
];
const TEST_DETECT_PARAMETERS = array(
'https://codeberg.org/Codeberg/Community/issues/507' => array(
'context' => 'Issue Comments', 'username' => 'Codeberg', 'repo' => 'Community', 'issueId' => '507'
),
'https://codeberg.org/Codeberg/Community/issues' => array(
'context' => 'Issues', 'username' => 'Codeberg', 'repo' => 'Community'
),
'https://codeberg.org/Codeberg/Community/pulls' => array(
'context' => 'Pull Requests', 'username' => 'Codeberg', 'repo' => 'Community'
),
'https://codeberg.org/Codeberg/Community/releases' => array(
'context' => 'Releases', 'username' => 'Codeberg', 'repo' => 'Community'
),
'https://codeberg.org/Codeberg/Community/commits/branch/master' => array(
'context' => 'Commits', 'username' => 'Codeberg', 'repo' => 'Community', 'branch' => 'master'
),
'https://codeberg.org/Codeberg/Community/commits' => array(
'context' => 'Commits', 'username' => 'Codeberg', 'repo' => 'Community'
)
);
const CACHE_TIMEOUT = 1800;
private $defaultBranch = 'main';
private $issueTitle = '';
const TEST_DETECT_PARAMETERS = [
'https://codeberg.org/Codeberg/Community/issues/507' => [
'context' => 'Issue Comments', 'username' => 'Codeberg', 'repo' => 'Community', 'issueId' => '507'
],
'https://codeberg.org/Codeberg/Community/issues' => [
'context' => 'Issues', 'username' => 'Codeberg', 'repo' => 'Community'
],
'https://codeberg.org/Codeberg/Community/pulls' => [
'context' => 'Pull Requests', 'username' => 'Codeberg', 'repo' => 'Community'
],
'https://codeberg.org/Codeberg/Community/releases' => [
'context' => 'Releases', 'username' => 'Codeberg', 'repo' => 'Community'
],
'https://codeberg.org/Codeberg/Community/commits/branch/master' => [
'context' => 'Commits', 'username' => 'Codeberg', 'repo' => 'Community', 'branch' => 'master'
],
'https://codeberg.org/Codeberg/Community/commits' => [
'context' => 'Commits', 'username' => 'Codeberg', 'repo' => 'Community'
]
];
private $urlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)(?:\/commits\/branch\/([\w]+))?/';
private $issuesUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/issues/';
private $pullsUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/pulls/';
private $releasesUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/releases/';
private $issueCommentsUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/issues\/([0-9]+)/';
private $defaultBranch = 'main';
private $issueTitle = '';
public function detectParameters($url) {
$params = array();
private $urlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)(?:\/commits\/branch\/([\w]+))?/';
private $issuesUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/issues/';
private $pullsUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/pulls/';
private $releasesUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/releases/';
private $issueCommentsUrlRegex = '/codeberg\.org\/([\w]+)\/([\w]+)\/issues\/([0-9]+)/';
// Issue Comments
if(preg_match($this->issueCommentsUrlRegex, $url, $matches)) {
$params['context'] = 'Issue Comments';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
$params['issueId'] = $matches[3];
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
return $params;
}
$html = defaultLinkTo($html, $this->getURI());
// Issues
if(preg_match($this->issuesUrlRegex, $url, $matches)) {
$params['context'] = 'Issues';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
switch ($this->queriedContext) {
case 'Commits':
$this->extractCommits($html);
break;
case 'Issues':
$this->extractIssues($html);
break;
case 'Issue Comments':
$this->extractIssueComments($html);
break;
case 'Pull Requests':
$this->extractPulls($html);
break;
case 'Releases':
$this->extractReleases($html);
break;
case 'Tags':
$this->extractTags($html);
break;
default:
throw new \Exception('Invalid context: ' . $this->queriedContext);
}
}
return $params;
}
public function getName()
{
switch ($this->queriedContext) {
case 'Commits':
if ($this->getBranch() === $this->defaultBranch) {
return $this->getRepo() . ' Commits';
}
// Pull Requests
if(preg_match($this->pullsUrlRegex, $url, $matches)) {
$params['context'] = 'Pull Requests';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
return $this->getRepo() . ' Commits (' . $this->getBranch() . ' branch) - ' . self::NAME;
case 'Issues':
return $this->getRepo() . ' Issues - ' . self::NAME;
case 'Issue Comments':
return $this->issueTitle . ' - Issue Comments - ' . self::NAME;
case 'Pull Requests':
return $this->getRepo() . ' Pull Requests - ' . self::NAME;
case 'Releases':
return $this->getRepo() . ' Releases - ' . self::NAME;
case 'Tags':
return $this->getRepo() . ' Tags - ' . self::NAME;
default:
return parent::getName();
}
}
return $params;
}
public function getURI()
{
switch ($this->queriedContext) {
case 'Commits':
return self::URI . $this->getRepo() . '/commits/branch/' . $this->getBranch();
case 'Issues':
return self::URI . $this->getRepo() . '/issues/';
case 'Issue Comments':
return self::URI . $this->getRepo() . '/issues/' . $this->getInput('issueId');
case 'Pull Requests':
return self::URI . $this->getRepo() . '/pulls';
case 'Releases':
return self::URI . $this->getRepo() . '/releases';
case 'Tags':
return self::URI . $this->getRepo() . '/tags';
default:
return parent::getURI();
}
}
// Releases
if(preg_match($this->releasesUrlRegex, $url, $matches)) {
$params['context'] = 'Releases';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
private function getBranch()
{
if ($this->getInput('branch')) {
return $this->getInput('branch');
}
return $params;
}
return $this->defaultBranch;
}
// Commits
if(preg_match($this->urlRegex, $url, $matches)) {
$params['context'] = 'Commits';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
private function getRepo()
{
return $this->getInput('username') . '/' . $this->getInput('repo');
}
if (isset($matches[3])) {
$params['branch'] = $matches[3];
}
/**
* Extract commits
*/
private function extractCommits($html)
{
$table = $html->find('table#commits-table', 0);
$tbody = $table->find('tbody.commit-list', 0);
return $params;
}
foreach ($tbody->find('tr') as $tr) {
$item = [];
return null;
}
$message = $tr->find('td.message', 0);
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI());
$item['title'] = $message->find('span.message-wrapper', 0)->plaintext;
$item['uri'] = $tr->find('td.sha', 0)->find('a', 0)->href;
$item['author'] = $tr->find('td.author', 0)->plaintext;
$item['timestamp'] = $tr->find('td', 3)->find('span', 0)->title;
$html = defaultLinkTo($html, $this->getURI());
if ($message->find('pre.commit-body', 0)) {
$message->find('pre.commit-body', 0)->style = '';
switch($this->queriedContext) {
case 'Commits':
$this->extractCommits($html);
break;
case 'Issues':
$this->extractIssues($html);
break;
case 'Issue Comments':
$this->extractIssueComments($html);
break;
case 'Pull Requests':
$this->extractPulls($html);
break;
case 'Releases':
$this->extractReleases($html);
break;
default:
returnClientError('Invalid context: ' . $this->queriedContext);
}
}
$item['content'] = $message->find('pre.commit-body', 0);
} else {
$item['content'] = '<blockquote>' . $item['title'] . '</blockquote>';
}
public function getName() {
switch($this->queriedContext) {
case 'Commits':
if ($this->getBranch() === $this->defaultBranch) {
return $this->getRepo() . ' Commits';
}
$this->items[] = $item;
}
}
return $this->getRepo() . ' Commits (' . $this->getBranch() . ' branch) - ' . self::NAME;
case 'Issues':
return $this->getRepo() . ' Issues - ' . self::NAME;
case 'Issue Comments':
return $this->issueTitle . ' - Issue Comments - ' . self::NAME;
case 'Pull Requests':
return $this->getRepo() . ' Pull Requests - ' . self::NAME;
case 'Releases':
return $this->getRepo() . ' Releases - ' . self::NAME;
default:
return parent::getName();
}
}
/**
* Extract issues
*/
private function extractIssues($html)
{
$div = $html->find('div.issue.list', 0);
public function getURI() {
switch($this->queriedContext) {
case 'Commits':
return self::URI . $this->getRepo() . '/commits/branch/' . $this->getBranch();
case 'Issues':
return self::URI . $this->getRepo() . '/issues/';
case 'Issue Comments':
return self::URI . $this->getRepo() . '/issues/' . $this->getInput('issueId');
case 'Pull Requests':
return self::URI . $this->getRepo() . '/pulls';
case 'Releases':
return self::URI . $this->getRepo() . '/releases';
default:
return parent::getURI();
}
}
foreach ($div->find('li.item') as $li) {
$item = [];
private function getBranch() {
if ($this->getInput('branch')) {
return $this->getInput('branch');
}
$number = trim($li->find('a.index,ml-0.mr-2', 0)->plaintext);
return $this->defaultBranch;
}
$item['title'] = $li->find('a.title', 0)->plaintext . ' (' . $number . ')';
$item['uri'] = $li->find('a.title', 0)->href;
$item['timestamp'] = $li->find('span.time-since', 0)->title;
$item['author'] = $li->find('div.desc', 0)->find('a', 1)->plaintext;
private function getRepo() {
return $this->getInput('username') . '/' . $this->getInput('repo');
}
// Fetch issue page
$issuePage = getSimpleHTMLDOMCached($item['uri'], 3600);
$issuePage = defaultLinkTo($issuePage, self::URI);
/**
* Extract commits
*/
private function extractCommits($html) {
$table = $html->find('table#commits-table', 0);
$tbody = $table->find('tbody.commit-list', 0);
$item['content'] = $issuePage->find('div.timeline-item.comment.first', 0)->find('div.render-content.markup', 0);
foreach ($tbody->find('tr') as $tr) {
$item = array();
foreach ($li->find('a.ui.label') as $label) {
$item['categories'][] = $label->plaintext;
}
$message = $tr->find('td.message', 0);
$this->items[] = $item;
}
}
$item['title'] = $message->find('span.message-wrapper', 0)->plaintext;
$item['uri'] = $tr->find('td.sha', 0)->find('a', 0)->href;
$item['author'] = $tr->find('td.author', 0)->plaintext;
$item['timestamp'] = $tr->find('td', 3)->find('span', 0)->title;
/**
* Extract issue comments
*/
private function extractIssueComments($html)
{
$this->issueTitle = $html->find('span#issue-title', 0)->plaintext
. ' (' . $html->find('span.index', 0)->plaintext . ')';
if ($message->find('pre.commit-body', 0)) {
$message->find('pre.commit-body', 0)->style = '';
foreach ($html->find('div.timeline-item.comment') as $div) {
$item = [];
$item['content'] = $message->find('pre.commit-body', 0);
} else {
$item['content'] = '<blockquote>' . $item['title'] . '</blockquote>';
}
if ($div->class === 'timeline-item comment merge box') {
continue;
}
$this->items[] = $item;
}
}
$item['title'] = $this->ellipsisTitle($div->find('div.render-content.markup', 0)->plaintext);
$item['uri'] = $div->find('span.text.grey', 0)->find('a', 1)->href;
$item['content'] = $div->find('div.render-content.markup', 0);
/**
* Extract issues
*/
private function extractIssues($html) {
$div = $html->find('div.issue.list', 0);
if ($div->find('div.dropzone-attachments', 0)) {
$item['content'] .= $div->find('div.dropzone-attachments', 0);
}
foreach ($div->find('li.item') as $li) {
$item = array();
$item['author'] = $div->find('a.author', 0)->innertext;
$item['timestamp'] = $div->find('span.time-since', 0)->title;
$number = trim($li->find('a.index,ml-0.mr-2', 0)->plaintext);
$this->items[] = $item;
}
}
$item['title'] = $li->find('a.title', 0)->plaintext . ' (' . $number . ')';
$item['uri'] = $li->find('a.title', 0)->href;
$item['timestamp'] = $li->find('span.time-since', 0)->title;
$item['author'] = $li->find('div.desc', 0)->find('a', 1)->plaintext;
/**
* Extract pulls
*/
private function extractPulls($html)
{
$div = $html->find('div.issue.list', 0);
// Fetch issue page
$issuePage = getSimpleHTMLDOMCached($item['uri'], 3600);
$issuePage = defaultLinkTo($issuePage, self::URI);
foreach ($div->find('li.item') as $li) {
$item = [];
$item['content'] = $issuePage->find('div.timeline-item.comment.first', 0)->find('div.render-content.markup', 0);
$number = trim($li->find('a.index,ml-0.mr-2', 0)->plaintext);
foreach ($li->find('a.ui.label') as $label) {
$item['categories'][] = $label->plaintext;
}
$item['title'] = $li->find('a.title', 0)->plaintext . ' (' . $number . ')';
$item['uri'] = $li->find('a.title', 0)->href;
$item['timestamp'] = $li->find('span.time-since', 0)->title;
$item['author'] = $li->find('div.desc', 0)->find('a', 1)->plaintext;
$this->items[] = $item;
}
}
// Fetch pull request page
$pullRequestPage = getSimpleHTMLDOMCached($item['uri'], 3600);
$pullRequestPage = defaultLinkTo($pullRequestPage, self::URI);
/**
* Extract issue comments
*/
private function extractIssueComments($html) {
$this->issueTitle = $html->find('span#issue-title', 0)->plaintext
. ' (' . $html->find('span.index', 0)->plaintext . ')';
$item['content'] = $pullRequestPage->find('ui.timeline', 0)->find('div.render-content.markup', 0);
foreach ($html->find('div.timeline-item.comment') as $div) {
$item = array();
foreach ($li->find('a.ui.label') as $label) {
$item['categories'][] = $label->plaintext;
}
if ($div->class === 'timeline-item comment merge box') {
continue;
}
$this->items[] = $item;
}
}
$item['title'] = $this->ellipsisTitle($div->find('div.render-content.markup', 0)->plaintext);
$item['uri'] = $div->find('span.text.grey', 0)->find('a', 1)->href;
$item['content'] = $div->find('div.render-content.markup', 0);
/**
* Extract releases
*/
private function extractReleases($html)
{
$ul = $html->find('ul#release-list', 0);
if ($div->find('div.dropzone-attachments', 0)) {
$item['content'] .= $div->find('div.dropzone-attachments', 0);
}
$lis = $ul->find('li.ui.grid');
if ($lis === []) {
throw new \Exception('Found zero releases');
}
foreach ($lis as $li) {
$item = [];
$item['title'] = $li->find('h4', 0)->plaintext;
$item['uri'] = $li->find('h4', 0)->find('a', 0)->href;
$item['author'] = $div->find('a.author', 0)->innertext;
$item['timestamp'] = $div->find('span.time-since', 0)->title;
$tag = $this->stripSvg($li->find('span.tag', 0));
$commit = $this->stripSvg($li->find('span.commit', 0));
$downloads = $this->extractDownloads($li->find('details.download', 0));
$this->items[] = $item;
}
}
/**
* Extract pulls
*/
private function extractPulls($html) {
$div = $html->find('div.issue.list', 0);
foreach ($div->find('li.item') as $li) {
$item = array();
$number = trim($li->find('a.index,ml-0.mr-2', 0)->plaintext);
$item['title'] = $li->find('a.title', 0)->plaintext . ' (' . $number . ')';
$item['uri'] = $li->find('a.title', 0)->href;
$item['timestamp'] = $li->find('span.time-since', 0)->title;
$item['author'] = $li->find('div.desc', 0)->find('a', 1)->plaintext;
// Fetch pull request page
$pullRequestPage = getSimpleHTMLDOMCached($item['uri'], 3600);
$pullRequestPage = defaultLinkTo($pullRequestPage, self::URI);
$item['content'] = $pullRequestPage->find('ui.timeline', 0)->find('div.render-content.markup', 0);
foreach ($li->find('a.ui.label') as $label) {
$item['categories'][] = $label->plaintext;
}
$this->items[] = $item;
}
}
/**
* Extract releases
*/
private function extractReleases($html) {
$ul = $html->find('ul#release-list', 0);
foreach ($ul->find('li.ui.grid') as $li) {
$item = array();
$item['title'] = $li->find('h4', 0)->plaintext;
$item['uri'] = $li->find('h4', 0)->find('a', 0)->href;
$tag = $this->stripSvg($li->find('span.tag', 0));
$commit = $this->stripSvg($li->find('span.commit', 0));
$downloads = $this->extractDownloads($li->find('details.download', 0));
$item['content'] = $li->find('div.markup.desc', 0);
$item['content'] .= <<<HTML
$item['content'] = $li->find('div.markup.desc', 0);
$item['content'] .= <<<HTML
<strong>Tag</strong>
<p>{$tag}</p>
<strong>Commit</strong>
@@ -348,56 +316,131 @@ class CodebergBridge extends BridgeAbstract {
{$downloads}
HTML;
$item['timestamp'] = $li->find('span.time', 0)->find('span', 0)->title;
$item['author'] = $li->find('span.author', 0)->find('a', 0)->plaintext;
$item['timestamp'] = $li->find('span.time', 0)->find('span', 0)->title;
$item['author'] = $li->find('span.author', 0)->find('a', 0)->plaintext;
$this->items[] = $item;
}
}
$this->items[] = $item;
}
}
/**
* Extract downloads for a releases
*/
private function extractDownloads($html, $skipFirst = false) {
$downloads = '';
private function extractTags($html)
{
$tags = $html->find('td.tag');
if ($tags === []) {
throw new \Exception('Found zero tags');
}
foreach ($tags as $tag) {
$this->items[] = [
'title' => $tag->find('a', 0)->plaintext,
'uri' => $tag->find('a', 0)->href,
'content' => $tag->innertext,
];
}
}
foreach ($html->find('a') as $index => $a) {
if ($skipFirst === true && $index === 0) {
continue;
}
/**
* Extract downloads for a releases
*/
private function extractDownloads($html, $skipFirst = false)
{
$downloads = '';
$downloads .= <<<HTML
foreach ($html->find('a') as $index => $a) {
if ($skipFirst === true && $index === 0) {
continue;
}
$downloads .= <<<HTML
<a href="{$a->herf}">{$a->plaintext}</a><br>
HTML;
}
}
return <<<EOD
return <<<EOD
<strong>Downloads</strong>
<p>{$downloads}</p>
EOD;
}
}
/**
* Ellipsis title to first 100 characters
*/
private function ellipsisTitle($text) {
$length = 100;
/**
* Ellipsis title to first 100 characters
*/
private function ellipsisTitle($text)
{
$length = 100;
if (strlen($text) > $length) {
$text = explode('<br>', wordwrap($text, $length, '<br>'));
return $text[0] . '...';
}
return $text;
}
if (strlen($text) > $length) {
$text = explode('<br>', wordwrap($text, $length, '<br>'));
return $text[0] . '...';
}
return $text;
}
/**
* Strip SVG tag
*/
private function stripSvg($html) {
if ($html->find('svg', 0)) {
$html->find('svg', 0)->outertext = '';
}
/**
* Strip SVG tag
*/
private function stripSvg($html)
{
if ($html->find('svg', 0)) {
$html->find('svg', 0)->outertext = '';
}
return $html;
}
return $html;
}
public function detectParameters($url)
{
$params = [];
// Issue Comments
if (preg_match($this->issueCommentsUrlRegex, $url, $matches)) {
$params['context'] = 'Issue Comments';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
$params['issueId'] = $matches[3];
return $params;
}
// Issues
if (preg_match($this->issuesUrlRegex, $url, $matches)) {
$params['context'] = 'Issues';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
return $params;
}
// Pull Requests
if (preg_match($this->pullsUrlRegex, $url, $matches)) {
$params['context'] = 'Pull Requests';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
return $params;
}
// Releases
if (preg_match($this->releasesUrlRegex, $url, $matches)) {
$params['context'] = 'Releases';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
return $params;
}
// Commits
if (preg_match($this->urlRegex, $url, $matches)) {
$params['context'] = 'Commits';
$params['username'] = $matches[1];
$params['repo'] = $matches[2];
if (isset($matches[3])) {
$params['branch'] = $matches[3];
}
return $params;
}
return null;
}
}

View File

@@ -1,83 +1,85 @@
<?php
class CollegeDeFranceBridge extends BridgeAbstract {
const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'CollegeDeFrance';
const URI = 'https://www.college-de-france.fr/';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance';
class CollegeDeFranceBridge extends BridgeAbstract
{
const MAINTAINER = 'pit-fgfjiudghdf';
const NAME = 'CollegeDeFrance';
const URI = 'https://www.college-de-france.fr/';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the latest audio and video from CollegeDeFrance';
public function collectData(){
$months = array(
'01' => 'janv.',
'02' => 'févr.',
'03' => 'mars',
'04' => 'avr.',
'05' => 'mai',
'06' => 'juin',
'07' => 'juil.',
'08' => 'août',
'09' => 'sept.',
'10' => 'oct.',
'11' => 'nov.',
'12' => 'déc.'
);
public function collectData()
{
$months = [
'01' => 'janv.',
'02' => 'févr.',
'03' => 'mars',
'04' => 'avr.',
'05' => 'mai',
'06' => 'juin',
'07' => 'juil.',
'08' => 'août',
'09' => 'sept.',
'10' => 'oct.',
'11' => 'nov.',
'12' => 'déc.'
];
// The "API" used by the site returns a list of partial HTML in this form
/* <li>
* <a href="/site/thomas-romer/guestlecturer-2016-04-15-14h30.htm" data-target="after">
* <span class="date"><span class="list-icon list-icon-video"></span>
* <span class="list-icon list-icon-audio"></span>15 avr. 2016</span>
* <span class="lecturer">Christopher Hays</span>
* <span class='title'>Imagery of Divine Suckling in the Hebrew Bible and the Ancient Near East</span>
* </a>
* </li>
*/
$html = getSimpleHTMLDOM(self::URI
. 'components/search-audiovideo.jsp?fulltext=&siteid=1156951719600&lang=FR&type=all');
// The "API" used by the site returns a list of partial HTML in this form
/* <li>
* <a href="/site/thomas-romer/guestlecturer-2016-04-15-14h30.htm" data-target="after">
* <span class="date"><span class="list-icon list-icon-video"></span>
* <span class="list-icon list-icon-audio"></span>15 avr. 2016</span>
* <span class="lecturer">Christopher Hays</span>
* <span class='title'>Imagery of Divine Suckling in the Hebrew Bible and the Ancient Near East</span>
* </a>
* </li>
*/
$html = getSimpleHTMLDOM(self::URI
. 'components/search-audiovideo.jsp?fulltext=&siteid=1156951719600&lang=FR&type=all');
foreach($html->find('a[data-target]') as $element) {
$item = array();
$item['title'] = $element->find('.title', 0)->plaintext;
foreach ($html->find('a[data-target]') as $element) {
$item = [];
$item['title'] = $element->find('.title', 0)->plaintext;
// Most relative URLs contains an hour in addition to the date, so let's use it
// <a href="/site/yann-lecun/course-2016-04-08-11h00.htm" data-target="after">
//
// Sometimes there's an __1, perhaps it signifies an update
// "/site/patrick-boucheron/seminar-2016-05-03-18h00__1.htm"
//
// But unfortunately some don't have any hours info
// <a href="/site/institut-physique/
// The-Mysteries-of-Decoherence-Sebastien-Gleyzes-[Video-3-35].htm" data-target="after">
$timezone = new DateTimeZone('Europe/Paris');
// Most relative URLs contains an hour in addition to the date, so let's use it
// <a href="/site/yann-lecun/course-2016-04-08-11h00.htm" data-target="after">
//
// Sometimes there's an __1, perhaps it signifies an update
// "/site/patrick-boucheron/seminar-2016-05-03-18h00__1.htm"
//
// But unfortunately some don't have any hours info
// <a href="/site/institut-physique/
// The-Mysteries-of-Decoherence-Sebastien-Gleyzes-[Video-3-35].htm" data-target="after">
$timezone = new DateTimeZone('Europe/Paris');
// strpos($element->href, '201') will break in 2020 but it'll
// probably break prior to then due to site changes anyway
$d = DateTime::createFromFormat(
'!Y-m-d-H\hi',
substr($element->href, strpos($element->href, '201'), 16),
$timezone
);
// strpos($element->href, '201') will break in 2020 but it'll
// probably break prior to then due to site changes anyway
$d = DateTime::createFromFormat(
'!Y-m-d-H\hi',
substr($element->href, strpos($element->href, '201'), 16),
$timezone
);
if(!$d) {
$d = DateTime::createFromFormat(
'!d m Y',
trim(str_replace(
array_values($months),
array_keys($months),
$element->find('.date', 0)->plaintext
)),
$timezone
);
}
if (!$d) {
$d = DateTime::createFromFormat(
'!d m Y',
trim(str_replace(
array_values($months),
array_keys($months),
$element->find('.date', 0)->plaintext
)),
$timezone
);
}
$item['timestamp'] = $d->format('U');
$item['content'] = $element->find('.lecturer', 0)->innertext
. ' - '
. $element->find('.title', 0)->innertext;
$item['timestamp'] = $d->format('U');
$item['content'] = $element->find('.lecturer', 0)->innertext
. ' - '
. $element->find('.title', 0)->innertext;
$item['uri'] = self::URI . $element->href;
$this->items[] = $item;
}
}
$item['uri'] = self::URI . $element->href;
$this->items[] = $item;
}
}
}

View File

@@ -1,25 +1,28 @@
<?php
class ComboiosDePortugalBridge extends BridgeAbstract {
const NAME = 'CP | Avisos';
const BASE_URI = 'https://www.cp.pt';
const URI = self::BASE_URI . '/passageiros/pt';
const DESCRIPTION = 'Comboios de Portugal | Avisos';
const MAINTAINER = 'somini';
public function collectData() {
# Do not verify SSL certificate (the server doesn't send the intermediate)
# https://github.com/RSS-Bridge/rss-bridge/issues/2397
$html = getSimpleHTMLDOM($this->getURI() . '/consultar-horarios/avisos', array(), array(
CURLOPT_SSL_VERIFYPEER => 0,
));
class ComboiosDePortugalBridge extends BridgeAbstract
{
const NAME = 'CP | Avisos';
const BASE_URI = 'https://www.cp.pt';
const URI = self::BASE_URI . '/passageiros/pt';
const DESCRIPTION = 'Comboios de Portugal | Avisos';
const MAINTAINER = 'somini';
foreach($html->find('.warnings-table a') as $element) {
$item = array();
public function collectData()
{
# Do not verify SSL certificate (the server doesn't send the intermediate)
# https://github.com/RSS-Bridge/rss-bridge/issues/2397
$html = getSimpleHTMLDOM($this->getURI() . '/consultar-horarios/avisos', [], [
CURLOPT_SSL_VERIFYPEER => 0,
]);
$item['title'] = $element->innertext;
$item['uri'] = self::BASE_URI . implode('/', array_map('urlencode', explode('/', $element->href)));
foreach ($html->find('.warnings-table a') as $element) {
$item = [];
$this->items[] = $item;
}
}
$item['title'] = $element->innertext;
$item['uri'] = self::BASE_URI . implode('/', array_map('urlencode', explode('/', $element->href)));
$this->items[] = $item;
}
}
}

View File

@@ -1,64 +1,71 @@
<?php
class ComicsKingdomBridge extends BridgeAbstract {
const MAINTAINER = 'stjohnjohnson';
const NAME = 'Comics Kingdom Unofficial RSS';
const URI = 'https://comicskingdom.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Comics Kingdom Unofficial RSS';
const PARAMETERS = array( array(
'comicname' => array(
'name' => 'comicname',
'type' => 'text',
'exampleValue' => 'mutts',
'title' => 'The name of the comic in the URL after https://comicskingdom.com/',
'required' => true
)
));
class ComicsKingdomBridge extends BridgeAbstract
{
const MAINTAINER = 'stjohnjohnson';
const NAME = 'Comics Kingdom Unofficial RSS';
const URI = 'https://comicskingdom.com/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Comics Kingdom Unofficial RSS';
const PARAMETERS = [ [
'comicname' => [
'name' => 'comicname',
'type' => 'text',
'exampleValue' => 'mutts',
'title' => 'The name of the comic in the URL after https://comicskingdom.com/',
'required' => true
]
]];
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI(), array(), array(), true, false);
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI(), [], [], true, false);
// Get author from first page
$author = $html->find('div.author p', 0);;
// Get author from first page
$author = $html->find('div.author p', 0);
;
// Get current date/link
$link = $html->find('meta[property=og:url]', -1)->content;
for($i = 0; $i < 3; $i++) {
$item = array();
// Get current date/link
$link = $html->find('meta[property=og:url]', -1)->content;
for ($i = 0; $i < 3; $i++) {
$item = [];
$page = getSimpleHTMLDOM($link);
$page = getSimpleHTMLDOM($link);
$imagelink = $page->find('meta[property=og:image]', 0)->content;
$imagelink = $page->find('meta[property=og:image]', 0)->content;
$date = explode('/', $link);
$date = explode('/', $link);
$item['id'] = $imagelink;
$item['uri'] = $link;
$item['author'] = $author;
$item['title'] = 'Comics Kingdom ' . $this->getInput('comicname');
$item['timestamp'] = DateTime::createFromFormat('Y-m-d', $date[count($date) - 1])->getTimestamp();
$item['content'] = '<img src="' . $imagelink . '" />';
$item['id'] = $imagelink;
$item['uri'] = $link;
$item['author'] = $author;
$item['title'] = 'Comics Kingdom ' . $this->getInput('comicname');
$item['timestamp'] = DateTime::createFromFormat('Y-m-d', $date[count($date) - 1])->getTimestamp();
$item['content'] = '<img src="' . $imagelink . '" />';
$this->items[] = $item;
$link = $page->find('div.comic-viewer-inline a', 0)->href;
if (empty($link)) break; // allow bridge to continue if there's less than 3 comics
}
}
$this->items[] = $item;
$link = $page->find('div.comic-viewer-inline a', 0)->href;
if (empty($link)) {
break; // allow bridge to continue if there's less than 3 comics
}
}
}
public function getURI(){
if(!is_null($this->getInput('comicname'))) {
return self::URI . urlencode($this->getInput('comicname'));
}
public function getURI()
{
if (!is_null($this->getInput('comicname'))) {
return self::URI . urlencode($this->getInput('comicname'));
}
return parent::getURI();
}
return parent::getURI();
}
public function getName(){
if(!is_null($this->getInput('comicname'))) {
return $this->getInput('comicname') . ' - Comics Kingdom';
}
public function getName()
{
if (!is_null($this->getInput('comicname'))) {
return $this->getInput('comicname') . ' - Comics Kingdom';
}
return parent::getName();
}
return parent::getName();
}
}

View File

@@ -1,26 +1,31 @@
<?php
class CommonDreamsBridge extends FeedExpander {
const MAINTAINER = 'nyutag';
const NAME = 'CommonDreams Bridge';
const URI = 'https://www.commondreams.org/';
const DESCRIPTION = 'Returns the newest articles.';
class CommonDreamsBridge extends FeedExpander
{
const MAINTAINER = 'nyutag';
const NAME = 'CommonDreams Bridge';
const URI = 'https://www.commondreams.org/';
const DESCRIPTION = 'Returns the newest articles.';
public function collectData(){
$this->collectExpandableDatas('http://www.commondreams.org/rss.xml', 10);
}
public function collectData()
{
$this->collectExpandableDatas('http://www.commondreams.org/rss.xml', 10);
}
protected function parseItem($newsItem){
$item = parent::parseItem($newsItem);
$item['content'] = $this->extractContent($item['uri']);
return $item;
}
protected function parseItem($newsItem)
{
$item = parent::parseItem($newsItem);
$item['content'] = $this->extractContent($item['uri']);
return $item;
}
private function extractContent($url){
$html3 = getSimpleHTMLDOMCached($url);
$text = $html3->find('div[class=field--type-text-with-summary]', 0)->innertext;
$html3->clear();
unset ($html3);
return $text;
}
private function extractContent($url)
{
$dom = getSimpleHTMLDOMCached($url);
$summary = $dom->find('div.node__body', 0);
$text = $summary->innertext;
$dom->clear();
unset($dom);
return $text;
}
}

View File

@@ -1,34 +1,36 @@
<?php
class CopieDoubleBridge extends BridgeAbstract {
const MAINTAINER = 'superbaillot.net';
const NAME = 'CopieDouble';
const URI = 'http://www.copie-double.com/';
const CACHE_TIMEOUT = 14400; // 4h
const DESCRIPTION = 'CopieDouble';
class CopieDoubleBridge extends BridgeAbstract
{
const MAINTAINER = 'superbaillot.net';
const NAME = 'CopieDouble';
const URI = 'http://www.copie-double.com/';
const CACHE_TIMEOUT = 14400; // 4h
const DESCRIPTION = 'CopieDouble';
public function collectData(){
$html = getSimpleHTMLDOM(self::URI);
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI);
$table = $html->find('table table', 2);
$table = $html->find('table table', 2);
foreach($table->find('tr') as $element) {
$td = $element->find('td', 0);
foreach ($table->find('tr') as $element) {
$td = $element->find('td', 0);
if($td->class === 'couleur_1') {
$item = array();
$title = $td->innertext;
$pos = strpos($title, '<a');
$title = substr($title, 0, $pos);
$item['title'] = $title;
} elseif(strpos($element->innertext, '/images/suivant.gif') === false) {
$a = $element->find('a', 0);
$item['uri'] = self::URI . $a->href;
$content = str_replace('src="/', 'src="/' . self::URI, $element->find('td', 0)->innertext);
$content = str_replace('href="/', 'href="' . self::URI, $content);
$item['content'] = $content;
$this->items[] = $item;
}
}
}
if ($td->class === 'couleur_1') {
$item = [];
$title = $td->innertext;
$pos = strpos($title, '<a');
$title = substr($title, 0, $pos);
$item['title'] = $title;
} elseif (strpos($element->innertext, '/images/suivant.gif') === false) {
$a = $element->find('a', 0);
$item['uri'] = self::URI . $a->href;
$content = str_replace('src="/', 'src="/' . self::URI, $element->find('td', 0)->innertext);
$content = str_replace('href="/', 'href="' . self::URI, $content);
$item['content'] = $content;
$this->items[] = $item;
}
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
class CorreioDaFeiraBridge extends BridgeAbstract
{
const NAME = 'Correio da Feira';
const URI = 'https://www.correiodafeira.pt/';
const DESCRIPTION = 'Returns news from the Portuguese local newspaper Correio da Feira';
const MAINTAINER = 'rmscoelho';
const CACHE_TIMEOUT = 86400;
const PARAMETERS = [
[
'feed' => [
'name' => 'News Feed',
'type' => 'list',
'title' => 'Feeds from the Portuguese sports newspaper A BOLA.PT',
'values' => [
'Cultura' => 'cultura',
'Desporto' => 'desporto',
'Economia' => 'economia',
'Entrevista' => 'entrevista',
'Freguesias' => 'freguesias',
'Justiça' => 'justica',
'Opinião' => 'opiniao',
'Política' => 'politica',
'Reportagem' => 'reportagem',
'Sociedade' => 'sociedade',
'Tecnologia' => 'tecnologia',
]
]
]
];
public function getIcon()
{
return 'https://www.correiodafeira.pt/wp-content/uploads/base_reporter-200x200.jpg';
}
public function getName()
{
return !is_null($this->getKey('feed')) ? self::NAME . ' | ' . $this->getKey('feed') : self::NAME;
}
public function getURI()
{
return self::URI . $this->getInput('feed');
}
public function collectData()
{
$url = sprintf('https://www.correiodafeira.pt/categoria/%s', $this->getInput('feed'));
$dom = getSimpleHTMLDOM($url);
$dom = $dom->find('main', 0);
if (!$dom) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
}
$dom = defaultLinkTo($dom, $this->getURI());
foreach ($dom->find('div.post') as $article) {
$a = $article->find('div.blog-box', 0);
//Get date and time of publishing
$time = $a->find('.post-date > :nth-child(2)', 0)->plaintext;
$datetime = explode('/', $time);
$year = $datetime[2];
$month = $datetime[1];
$day = $datetime[0];
$timestamp = mktime(0, 0, 0, $month, $day, $year);
$this->items[] = [
'title' => $a->find('h2.entry-title > a', 0)->plaintext,
'uri' => $a->find('h2.entry-title > a', 0)->href,
'author' => $a->find('li.post-author > a', 0)->plaintext,
'content' => $a->find('.entry-content > p', 0)->plaintext,
'timestamp' => $timestamp,
];
}
}
}

View File

@@ -1,26 +1,29 @@
<?php
class CourrierInternationalBridge extends FeedExpander {
const MAINTAINER = 'teromene';
const NAME = 'Courrier International Bridge';
const URI = 'https://www.courrierinternational.com/';
const CACHE_TIMEOUT = 300; // 5 min
const DESCRIPTION = 'Returns the newest articles';
class CourrierInternationalBridge extends FeedExpander
{
const MAINTAINER = 'teromene';
const NAME = 'Courrier International Bridge';
const URI = 'https://www.courrierinternational.com/';
const CACHE_TIMEOUT = 300; // 5 min
const DESCRIPTION = 'Returns the newest articles';
public function collectData(){
$this->collectExpandableDatas(static::URI . 'feed/all/rss.xml', 20);
}
public function collectData()
{
$this->collectExpandableDatas(static::URI . 'feed/all/rss.xml', 20);
}
protected function parseItem($feedItem){
$item = parent::parseItem($feedItem);
protected function parseItem($feedItem)
{
$item = parent::parseItem($feedItem);
$articlePage = getSimpleHTMLDOMCached($feedItem->link);
$content = $articlePage->find('.article-text, depeche-text', 0);
if (!$content) {
return $item;
}
$item['content'] = sanitize($content);
$articlePage = getSimpleHTMLDOMCached($feedItem->link);
$content = $articlePage->find('.article-text, depeche-text', 0);
if (!$content) {
return $item;
}
$item['content'] = sanitize($content);
return $item;
}
return $item;
}
}

View File

@@ -1,107 +1,112 @@
<?php
class CraigslistBridge extends BridgeAbstract {
const MAINTAINER = 'Yaman Qalieh';
const NAME = 'Craigslist Bridge';
const URI = 'https://craigslist.org/';
const DESCRIPTION = 'Returns craigslist search results';
const PARAMETERS = array( array(
'region' => array(
'name' => 'Region',
'title' => 'The subdomain before craigslist.org in the URL',
'exampleValue' => 'sfbay',
'required' => true
),
'search' => array(
'name' => 'Search Query',
'title' => 'Everything in the URL after /search/',
'exampleValue' => 'sya?query=laptop',
'required' => true
),
'limit' => array(
'name' => 'Number of Posts',
'type' => 'number',
'title' => 'The maximum number of posts is 120. Use 0 for unlimited posts.',
'defaultValue' => '25'
)
));
class CraigslistBridge extends BridgeAbstract
{
const NAME = 'Craigslist Bridge';
const URI = 'https://craigslist.org/';
const DESCRIPTION = 'Returns craigslist search results';
const TEST_DETECT_PARAMETERS = array(
'https://sfbay.craigslist.org/search/sya?query=laptop' => array(
'region' => 'sfbay', 'search' => 'sya?query=laptop'
),
'https://newyork.craigslist.org/search/sss?query=32gb+flash+drive&bundleDuplicates=1&max_price=20' => array(
'region' => 'newyork', 'search' => 'sss?query=32gb+flash+drive&bundleDuplicates=1&max_price=20'
),
);
const PARAMETERS = [ [
'region' => [
'name' => 'Region',
'title' => 'The subdomain before craigslist.org in the URL',
'exampleValue' => 'sfbay',
'required' => true
],
'search' => [
'name' => 'Search Query',
'title' => 'Everything in the URL after /search/',
'exampleValue' => 'sya?query=laptop',
'required' => true
],
'limit' => [
'name' => 'Number of Posts',
'type' => 'number',
'title' => 'The maximum number of posts is 120. Use 0 for unlimited posts.',
'defaultValue' => '25'
]
]];
const URL_REGEX = '/^https:\/\/(?<region>\w+).craigslist.org\/search\/(?<search>.+)/';
const TEST_DETECT_PARAMETERS = [
'https://sfbay.craigslist.org/search/sya?query=laptop' => [
'region' => 'sfbay', 'search' => 'sya?query=laptop'
],
'https://newyork.craigslist.org/search/sss?query=32gb+flash+drive&bundleDuplicates=1&max_price=20' => [
'region' => 'newyork', 'search' => 'sss?query=32gb+flash+drive&bundleDuplicates=1&max_price=20'
],
];
public function detectParameters($url) {
if(preg_match(self::URL_REGEX, $url, $matches)) {
$params = array();
$params['region'] = $matches['region'];
$params['search'] = $matches['search'];
return $params;
}
}
const URL_REGEX = '/^https:\/\/(?<region>\w+).craigslist.org\/search\/(?<search>.+)/';
public function getURI() {
if (!is_null($this->getInput('region'))) {
$domain = 'https://' . $this->getInput('region') . '.craigslist.org/search/';
return urljoin($domain, $this->getInput('search'));
}
return parent::getURI();
}
public function detectParameters($url)
{
if (preg_match(self::URL_REGEX, $url, $matches)) {
$params = [];
$params['region'] = $matches['region'];
$params['search'] = $matches['search'];
return $params;
}
}
public function collectData() {
$uri = $this->getURI();
$html = getSimpleHTMLDOM($uri);
public function getURI()
{
if (!is_null($this->getInput('region'))) {
$domain = 'https://' . $this->getInput('region') . '.craigslist.org/search/';
return urljoin($domain, $this->getInput('search'));
}
return parent::getURI();
}
// Check if no results page is shown (nearby results)
if ($html->find('.displaycountShow', 0)->plaintext == '0') {
return;
}
public function collectData()
{
$uri = $this->getURI();
$html = getSimpleHTMLDOM($uri);
// Search for "more from nearby areas" banner in order to skip those results
$results = $html->find('.result-row, h4.nearby');
// Check if no results page is shown (nearby results)
if (($html->find('.displaycountShow', 0)->plaintext ?? '') == '0') {
return;
}
// Limit the number of posts
if ($this->getInput('limit') > 0) {
$results = array_slice($results, 0, $this->getInput('limit'));
}
// Search for "more from nearby areas" banner in order to skip those results
$results = $html->find('.result-row, h4.nearby');
foreach($results as $post) {
// Limit the number of posts
if ($this->getInput('limit') > 0) {
$results = array_slice($results, 0, $this->getInput('limit'));
}
// Skip "nearby results" banner and results
// This only appears when searchNearby is not specified
if ($post->tag == 'h4') {
break;
}
foreach ($results as $post) {
// Skip "nearby results" banner and results
// This only appears when searchNearby is not specified
if ($post->tag == 'h4') {
break;
}
$item = array();
$item = [];
$heading = $post->find('.result-heading a', 0);
$item['uri'] = $heading->href;
$item['title'] = $heading->plaintext;
$item['timestamp'] = $post->find('.result-date', 0)->datetime;
$item['uid'] = $heading->id;
$item['content'] = $post->find('.result-price', 0)->plaintext . ' '
// Find the location (local and nearby results if searchNearby=1)
. $post->find('.result-hood, span.nearby', 0)->plaintext;
$heading = $post->find('.result-heading a', 0);
$item['uri'] = $heading->href;
$item['title'] = $heading->plaintext;
$item['timestamp'] = $post->find('.result-date', 0)->datetime;
$item['uid'] = $heading->id;
$images = $post->find('.result-image[data-ids]', 0);
if (!is_null($images)) {
$item['content'] .= '<br>';
foreach(explode(',', $images->getAttribute('data-ids')) as $image) {
// Remove leading 3: from each image id
$id = substr($image, 2);
$image_uri = 'https://images.craigslist.org/' . $id . '_300x300.jpg';
$item['content'] .= '<img src="' . $image_uri . '">';
$item['enclosures'][] = $image_uri;
}
}
$this->items[] = $item;
}
}
$price = $post->find('.result-price', 0)->plaintext ?? '';
// Find the location (local and nearby results if searchNearby=1)
$nearby = $post->find('.result-hood, span.nearby', 0)->plaintext ?? '';
$item['content'] = sprintf('%s %s', $price, $nearby);
$images = $post->find('.result-image[data-ids]', 0);
if (!is_null($images)) {
$item['content'] .= '<br>';
foreach (explode(',', $images->getAttribute('data-ids')) as $image) {
// Remove leading 3: from each image id
$id = substr($image, 2);
$image_uri = 'https://images.craigslist.org/' . $id . '_300x300.jpg';
$item['content'] .= '<img src="' . $image_uri . '">';
$item['enclosures'][] = $image_uri;
}
}
$this->items[] = $item;
}
}
}

View File

@@ -1,227 +1,233 @@
<?php
class CrewbayBridge extends BridgeAbstract {
const MAINTAINER = 'couraudt';
const NAME = 'Crewbay Bridge';
const URI = 'https://www.crewbay.com';
const DESCRIPTION = 'Returns the newest sailing offers.';
const PARAMETERS = array(
array(
'keyword' => array(
'name' => 'Filter by keyword',
'title' => 'Enter the keyword to filter here'
),
'type' => array(
'name' => 'Type of search',
'title' => 'Choose between finding a boat or a crew',
'type' => 'list',
'values' => array(
'Find a boat' => 'boats',
'Find a crew' => 'crew'
)
),
'status' => array(
'name' => 'Status on the boat',
'title' => 'Choose between recreational or professional classified ads',
'type' => 'list',
'values' => array(
'Recreational' => 'recreational',
'Professional' => 'professional'
)
),
'recreational_position' => array(
'name' => 'Recreational position wanted',
'title' => 'Filter by recreational position you wanted aboard',
'required' => false,
'type' => 'list',
'values' => array(
'' => '',
'Amateur Crew' => 'Amateur Crew',
'Friendship' => 'Friendship',
'Competent Crew' => 'Competent Crew',
'Racing' => 'Racing',
'Voluntary work' => 'Voluntary work',
'Mile building' => 'Mile building'
)
),
'professional_position' => array(
'name' => 'Professional position wanted',
'title' => 'Filter by professional position you wanted aboard',
'required' => false,
'type' => 'list',
'values' => array(
'' => '',
'1st Engineer' => '1st Engineer',
'1st Mate' => '1st Mate',
'Beautician' => 'Beautician',
'Bosun' => 'Bosun',
'Captain' => 'Captain',
'Chef' => 'Chef',
'Steward(ess)' => 'Steward(ess)',
'Deckhand' => 'Deckhand',
'Delivery Crew' => 'Delivery Crew',
'Dive Instructor' => 'Dive Instructor',
'Masseur' => 'Masseur',
'Medical Staff' => 'Medical Staff',
'Nanny' => 'Nanny',
'Navigator' => 'Navigator',
'Racing Crew' => 'Racing Crew',
'Teacher' => 'Teacher',
'Electrical Engineer' => 'Electrical Engineer',
'Fitter' => 'Fitter',
'2nd Engineer' => '2nd Engineer',
'3rd Engineer' => '3rd Engineer',
'Lead Deckhand' => 'Lead Deckhand',
'Security Officer' => 'Security Officer',
'O.O.W' => 'O.O.W',
'1st Officer' => '1st Officer',
'2nd Officer' => '2nd Officer',
'3rd Officer' => '3rd Officer',
'Captain/Engineer' => 'Captain/Engineer',
'Hairdresser' => 'Hairdresser',
'Fitness Trainer' => 'Fitness Trainer',
'Laundry' => 'Laundry',
'Solo Steward/ess' => 'Solo Steward/ess',
'Stew/Deck' => 'Stew/Deck',
'2nd Steward/ess' => '2nd Steward/ess',
'3rd Steward/ess' => '3rd Steward/ess',
'Chief Steward/ess' => 'Chief Steward/ess',
'Head Housekeeper' => 'Head Housekeeper',
'Purser' => 'Purser',
'Cook' => 'Cook',
'Cook/Stew' => 'Cook/Stew',
'2nd Chef' => '2nd Chef',
'Head Chef' => 'Head Chef',
'Administrator' => 'Administrator',
'P.A' => 'P.A',
'Villa staff' => 'Villa staff',
'Housekeeping/Stew' => 'Housekeeping/Stew',
'Stew/Beautician' => 'Stew/Beautician',
'Stew/Masseuse' => 'Stew/Masseuse',
'Manager' => 'Manager',
'Sailing instructor' => 'Sailing instructor'
)
)
)
);
public function collectData() {
$url = $this->getURI();
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
class CrewbayBridge extends BridgeAbstract
{
const MAINTAINER = 'couraudt';
const NAME = 'Crewbay Bridge';
const URI = 'https://www.crewbay.com';
const DESCRIPTION = 'Returns the newest sailing offers.';
const PARAMETERS = [
[
'keyword' => [
'name' => 'Filter by keyword',
'title' => 'Enter the keyword to filter here'
],
'type' => [
'name' => 'Type of search',
'title' => 'Choose between finding a boat or a crew',
'type' => 'list',
'values' => [
'Find a boat' => 'boats',
'Find a crew' => 'crew'
]
],
'status' => [
'name' => 'Status on the boat',
'title' => 'Choose between recreational or professional classified ads',
'type' => 'list',
'values' => [
'Recreational' => 'recreational',
'Professional' => 'professional'
]
],
'recreational_position' => [
'name' => 'Recreational position wanted',
'title' => 'Filter by recreational position you wanted aboard',
'required' => false,
'type' => 'list',
'values' => [
'' => '',
'Amateur Crew' => 'Amateur Crew',
'Friendship' => 'Friendship',
'Competent Crew' => 'Competent Crew',
'Racing' => 'Racing',
'Voluntary work' => 'Voluntary work',
'Mile building' => 'Mile building'
]
],
'professional_position' => [
'name' => 'Professional position wanted',
'title' => 'Filter by professional position you wanted aboard',
'required' => false,
'type' => 'list',
'values' => [
'' => '',
'1st Engineer' => '1st Engineer',
'1st Mate' => '1st Mate',
'Beautician' => 'Beautician',
'Bosun' => 'Bosun',
'Captain' => 'Captain',
'Chef' => 'Chef',
'Steward(ess)' => 'Steward(ess)',
'Deckhand' => 'Deckhand',
'Delivery Crew' => 'Delivery Crew',
'Dive Instructor' => 'Dive Instructor',
'Masseur' => 'Masseur',
'Medical Staff' => 'Medical Staff',
'Nanny' => 'Nanny',
'Navigator' => 'Navigator',
'Racing Crew' => 'Racing Crew',
'Teacher' => 'Teacher',
'Electrical Engineer' => 'Electrical Engineer',
'Fitter' => 'Fitter',
'2nd Engineer' => '2nd Engineer',
'3rd Engineer' => '3rd Engineer',
'Lead Deckhand' => 'Lead Deckhand',
'Security Officer' => 'Security Officer',
'O.O.W' => 'O.O.W',
'1st Officer' => '1st Officer',
'2nd Officer' => '2nd Officer',
'3rd Officer' => '3rd Officer',
'Captain/Engineer' => 'Captain/Engineer',
'Hairdresser' => 'Hairdresser',
'Fitness Trainer' => 'Fitness Trainer',
'Laundry' => 'Laundry',
'Solo Steward/ess' => 'Solo Steward/ess',
'Stew/Deck' => 'Stew/Deck',
'2nd Steward/ess' => '2nd Steward/ess',
'3rd Steward/ess' => '3rd Steward/ess',
'Chief Steward/ess' => 'Chief Steward/ess',
'Head Housekeeper' => 'Head Housekeeper',
'Purser' => 'Purser',
'Cook' => 'Cook',
'Cook/Stew' => 'Cook/Stew',
'2nd Chef' => '2nd Chef',
'Head Chef' => 'Head Chef',
'Administrator' => 'Administrator',
'P.A' => 'P.A',
'Villa staff' => 'Villa staff',
'Housekeeping/Stew' => 'Housekeeping/Stew',
'Stew/Beautician' => 'Stew/Beautician',
'Stew/Masseuse' => 'Stew/Masseuse',
'Manager' => 'Manager',
'Sailing instructor' => 'Sailing instructor'
]
]
]
];
$annonces = $html->find('#SearchResults div.result');
$limit = 0;
public function collectData()
{
$url = $this->getURI();
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
foreach ($annonces as $annonce) {
$detail = $annonce->find('.btn--profile', 0);
$htmlDetail = getSimpleHTMLDOMCached($detail->href);
$annonces = $html->find('#SearchResults div.result');
$limit = 0;
if (!empty($this->getInput('recreational_position')) || !empty($this->getInput('professional_position'))) {
if ($this->getInput('type') == 'boats') {
if ($this->getInput('status') == 'professional') {
$positions = array($annonce->find('.title .position', 0)->plaintext);
} else {
$positions = array(str_replace('Wanted:', '', $annonce->find('.content li', 0)->plaintext));
}
} else {
$list = $htmlDetail->find('.viewer-details .viewer-list');
$positions = explode("\r\n", end($list)->find('span.value', 0)->plaintext);
}
foreach ($annonces as $annonce) {
$detail = $annonce->find('.btn--profile', 0);
$htmlDetail = getSimpleHTMLDOMCached($detail->href);
$found = false;
$keyword = $this->getInput('status') == 'professional' ? 'professional_position' : 'recreational_position';
foreach ($positions as $position) {
if (strpos(trim($position), $this->getInput($keyword)) !== false) {
$found = true;
break;
}
}
if (!empty($this->getInput('recreational_position')) || !empty($this->getInput('professional_position'))) {
if ($this->getInput('type') == 'boats') {
if ($this->getInput('status') == 'professional') {
$positions = [$annonce->find('.title .position', 0)->plaintext];
} else {
$positions = [str_replace('Wanted:', '', $annonce->find('.content li', 0)->plaintext)];
}
} else {
$list = $htmlDetail->find('.viewer-details .viewer-list');
$positions = explode("\r\n", end($list)->find('span.value', 0)->plaintext);
}
if (!$found) {
continue;
}
}
$found = false;
$keyword = $this->getInput('status') == 'professional' ? 'professional_position' : 'recreational_position';
foreach ($positions as $position) {
if (strpos(trim($position), $this->getInput($keyword)) !== false) {
$found = true;
break;
}
}
$item = array();
if (!$found) {
continue;
}
}
if ($this->getInput('type') == 'boats') {
$titleSelector = '.title h2';
} else {
$titleSelector = '.layout__item h2';
}
$userName = $annonce->find('.result--description a', 0)->plaintext;
$annonceTitle = trim($annonce->find($titleSelector, 0)->plaintext);
if (empty($annonceTitle)) {
$item['title'] = $userName;
} else {
$item['title'] = $userName . ' - ' . $annonceTitle;
}
$item = [];
$item['uri'] = $detail->href;
$images = $annonce->find('.avatar img');
$item['enclosures'] = array(end($images)->getAttribute('src'));
if ($this->getInput('type') == 'boats') {
$titleSelector = '.title h2';
} else {
$titleSelector = '.layout__item h2';
}
$userName = $annonce->find('.result--description a', 0)->plaintext;
$annonceTitle = trim($annonce->find($titleSelector, 0)->plaintext);
if (empty($annonceTitle)) {
$item['title'] = $userName;
} else {
$item['title'] = $userName . ' - ' . $annonceTitle;
}
$content = $htmlDetail->find('.viewer-intro--info', 0)->innertext;
$item['uri'] = $detail->href;
$images = $annonce->find('.avatar img');
$item['enclosures'] = [end($images)->getAttribute('src')];
$sections = $htmlDetail->find('.viewer-container .viewer-section');
foreach ($sections as $section) {
if ($section->find('.viewer-section-title', 0)) {
$class = str_replace('viewer-', '', explode(' ', $section->getAttribute('class'))[0]);
if (!in_array($class, array('apply', 'photos', 'reviews', 'contact', 'experience', 'qa'))) {
// Basic sections
$content .= $section->find('.viewer-section-title h3', 0)->outertext;
$content .= $section->find('.viewer-section-content', 0)->innertext;
}
} else {
// Info section
$content .= $section->find('.viewer-section-content h3', 0)->outertext;
$content .= $section->find('.viewer-section-content p', 0)->outertext;
}
}
$content = $htmlDetail->find('.viewer-intro--info', 0)->innertext;
if (!empty($this->getInput('keyword'))) {
$keyword = strtolower($this->getInput('keyword'));
if (strpos(strtolower($item['title']), $keyword) === false) {
if (strpos(strtolower($content), $keyword) === false) {
continue;
}
}
}
$sections = $htmlDetail->find('.viewer-container .viewer-section');
foreach ($sections as $section) {
if ($section->find('.viewer-section-title', 0)) {
$class = str_replace('viewer-', '', explode(' ', $section->getAttribute('class'))[0]);
if (!in_array($class, ['apply', 'photos', 'reviews', 'contact', 'experience', 'qa'])) {
// Basic sections
$content .= $section->find('.viewer-section-title h3', 0)->outertext;
$content .= $section->find('.viewer-section-content', 0)->innertext;
}
} else {
// Info section
$content .= $section->find('.viewer-section-content h3', 0)->outertext;
$content .= $section->find('.viewer-section-content p', 0)->outertext;
}
}
$item['content'] = $content;
if (!empty($this->getInput('keyword'))) {
$keyword = strtolower($this->getInput('keyword'));
if (strpos(strtolower($item['title']), $keyword) === false) {
if (strpos(strtolower($content), $keyword) === false) {
continue;
}
}
}
$tags = $htmlDetail->find('li.viewer-tags--tag');
foreach ($tags as $tag) {
if (!isset($item['categories'])) {
$item['categories'] = array();
}
$text = trim($tag->plaintext);
if (!in_array($text, $item['categories'])) {
$item['categories'][] = $text;
}
}
$item['content'] = $content;
$this->items[] = $item;
$limit += 1;
$tags = $htmlDetail->find('li.viewer-tags--tag');
foreach ($tags as $tag) {
if (!isset($item['categories'])) {
$item['categories'] = [];
}
$text = trim($tag->plaintext);
if (!in_array($text, $item['categories'])) {
$item['categories'][] = $text;
}
}
if ($limit == 10) break;
}
}
$this->items[] = $item;
$limit += 1;
public function getURI() {
$uri = parent::getURI();
if ($limit == 10) {
break;
}
}
}
if ($this->getInput('type') == 'boats') {
$uri .= '/boats';
} else {
$uri .= '/crew';
}
public function getURI()
{
$uri = parent::getURI();
if ($this->getInput('status') == 'professional') {
$uri .= '/professional';
} else {
$uri .= '/recreational';
}
if ($this->getInput('type') == 'boats') {
$uri .= '/boats';
} else {
$uri .= '/crew';
}
return $uri;
}
if ($this->getInput('status') == 'professional') {
$uri .= '/professional';
} else {
$uri .= '/recreational';
}
return $uri;
}
}

View File

@@ -1,44 +1,47 @@
<?php
class CryptomeBridge extends BridgeAbstract {
const MAINTAINER = 'BoboTiG';
const NAME = 'Cryptome';
const URI = 'https://cryptome.org/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns the N most recent documents.';
const PARAMETERS = array( array(
'n' => array(
'name' => 'number of elements',
'type' => 'number',
'required' => true,
'exampleValue' => 10
)
));
class CryptomeBridge extends BridgeAbstract
{
const MAINTAINER = 'BoboTiG';
const NAME = 'Cryptome';
const URI = 'https://cryptome.org/';
const CACHE_TIMEOUT = 21600; // 6h
const DESCRIPTION = 'Returns the N most recent documents.';
const PARAMETERS = [ [
'n' => [
'name' => 'number of elements',
'type' => 'number',
'required' => true,
'exampleValue' => 10
]
]];
public function getIcon() {
return self::URI . '/favicon.ico';
}
public function getIcon()
{
return self::URI . '/favicon.ico';
}
public function collectData(){
$html = getSimpleHTMLDOM(self::URI);
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI);
$number = $this->getInput('n');
if(!empty($number)) {
$num = min($number, 20);
}
$i = 0;
foreach($html->find('pre', 1)->find('b') as $element) {
foreach($element->find('a') as $element1) {
$item = array();
$item['uri'] = $element1->href;
$item['title'] = $element->plaintext;
$this->items[] = $item;
$number = $this->getInput('n');
if (!empty($number)) {
$num = min($number, 20);
}
$i = 0;
foreach ($html->find('pre', 1)->find('b') as $element) {
foreach ($element->find('a') as $element1) {
$item = [];
$item['uri'] = $element1->href;
$item['title'] = $element->plaintext;
$this->items[] = $item;
if ($i > $num) {
break 2;
}
$i++;
}
}
}
if ($i > $num) {
break 2;
}
$i++;
}
}
}
}

102
bridges/CubariBridge.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
class CubariBridge extends BridgeAbstract
{
const NAME = 'Cubari';
const URI = 'https://cubari.moe';
const DESCRIPTION = 'Parses given cubari-formatted JSON file for updates.';
const MAINTAINER = 'KamaleiZestri';
const PARAMETERS = [[
'gist' => [
'name' => 'Gist/Raw Url',
'type' => 'text',
'required' => true,
'exampleValue' => 'https://raw.githubusercontent.com/kurisumx/baka/main/ikedan'
]
]];
private $mangaTitle = '';
public function getName()
{
if (!empty($this->mangaTitle)) {
return $this->mangaTitle . ' - ' . self::NAME;
} else {
return self::NAME;
}
}
public function getURI()
{
if ($this->getInput('gist') != '') {
return self::URI . '/read/gist/' . $this->getEncodedGist();
} else {
return self::URI;
}
}
/**
* The Cubari bridge.
*
* Cubari urls are base64 encodes of a given github raw or gist link described as below:
* https://cubari.moe/read/gist/${bаse64.url_encode(raw/<rest of the url...>)}/
* https://cubari.moe/read/gist/${bаse64.url_encode(gist/<rest of the url...>)}/
* https://cubari.moe/read/gist/${gitio shortcode}
*
* This bridge uses just the raw/gist and generates matching cubari urls.
*/
public function collectData()
{
$jsonSite = getContents($this->getInput('gist'));
$jsonFile = json_decode($jsonSite, true);
$this->mangaTitle = $jsonFile['title'];
$chapters = $jsonFile['chapters'];
foreach ($chapters as $chapnum => $chapter) {
$item = $this->getItemFromChapter($chapnum, $chapter);
$this->items[] = $item;
}
array_multisort(array_column($this->items, 'timestamp'), SORT_DESC, $this->items);
}
protected function getEncodedGist()
{
$url = $this->getInput('gist');
preg_match('/\/([a-z]*)\.githubusercontent.com(.*)/', $url, $matches);
// raw or gist is first match.
$unencoded = $matches[1] . $matches[2];
return base64_encode($unencoded);
}
private function getSanitizedHash($string)
{
return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
}
protected function getItemFromChapter($chapnum, $chapter)
{
$item = [];
$item['uri'] = $this->getURI() . '/' . $chapnum;
$item['title'] = 'Chapter ' . $chapnum . ' - ' . $chapter['title'] . ' - ' . $this->mangaTitle;
foreach ($chapter['groups'] as $key => $value) {
$item['author'] = $key;
}
$item['timestamp'] = $chapter['last_updated'];
$item['content'] = '<p>Manga: <a href=' . $this->getURI() . '>' . $this->mangaTitle . '</a> </p>
<p>Chapter Number: ' . $chapnum . '</p>
<p>Chapter Title: <a href=' . $item['uri'] . '>' . $chapter['title'] . '</a></p>
<p>Group: ' . $item['author'] . '</p>';
$item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
return $item;
}
}

View File

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

View File

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

View File

@@ -1,66 +1,73 @@
<?php
class DanbooruBridge extends BridgeAbstract {
const MAINTAINER = 'mitsukarenai, logmanoriginal';
const NAME = 'Danbooru';
const URI = 'http://donmai.us/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns images from given page';
class DanbooruBridge extends BridgeAbstract
{
const MAINTAINER = 'mitsukarenai, logmanoriginal';
const NAME = 'Danbooru';
const URI = 'http://donmai.us/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns images from given page';
const PARAMETERS = array(
'global' => array(
'p' => array(
'name' => 'page',
'defaultValue' => 1,
'type' => 'number'
),
't' => array(
'name' => 'tags'
)
),
0 => array()
);
const PARAMETERS = [
'global' => [
'p' => [
'name' => 'page',
'defaultValue' => 1,
'type' => 'number'
],
't' => [
'type' => 'text',
'name' => 'tags',
'exampleValue' => 'cosplay',
]
],
0 => []
];
const PATHTODATA = 'article';
const IDATTRIBUTE = 'data-id';
const TAGATTRIBUTE = 'alt';
const PATHTODATA = 'article';
const IDATTRIBUTE = 'data-id';
const TAGATTRIBUTE = 'alt';
protected function getFullURI(){
return $this->getURI()
. 'posts?&page=' . $this->getInput('p')
. '&tags=' . urlencode($this->getInput('t'));
}
protected function getFullURI()
{
return $this->getURI()
. 'posts?&page=' . $this->getInput('p')
. '&tags=' . urlencode($this->getInput('t'));
}
protected function getTags($element){
return $element->find('img', 0)->getAttribute(static::TAGATTRIBUTE);
}
protected function getTags($element)
{
return $element->find('img', 0)->getAttribute(static::TAGATTRIBUTE);
}
protected function getItemFromElement($element){
// Fix links
defaultLinkTo($element, $this->getURI());
protected function getItemFromElement($element)
{
// Fix links
defaultLinkTo($element, $this->getURI());
$item = array();
$item['uri'] = html_entity_decode($element->find('a', 0)->href);
$item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src;
$item['categories'] = array_filter(explode(' ', $this->getTags($element)));
$item['title'] = $this->getName() . ' | ' . $item['postid'];
$item['content'] = '<a href="'
. $item['uri']
. '"><img src="'
. $thumbnailUri
. '" /></a><br>Tags: '
. $this->getTags($element);
$item = [];
$item['uri'] = html_entity_decode($element->find('a', 0)->href);
$item['postid'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE));
$item['timestamp'] = time();
$thumbnailUri = $element->find('img', 0)->src;
$item['categories'] = array_filter(explode(' ', $this->getTags($element)));
$item['title'] = $this->getName() . ' | ' . $item['postid'];
$item['content'] = '<a href="'
. $item['uri']
. '"><img src="'
. $thumbnailUri
. '" /></a><br>Tags: '
. $this->getTags($element);
return $item;
}
return $item;
}
public function collectData(){
$html = getSimpleHTMLDOMCached($this->getFullURI());
public function collectData()
{
$html = getSimpleHTMLDOMCached($this->getFullURI());
foreach($html->find(static::PATHTODATA) as $element) {
$this->items[] = $this->getItemFromElement($element);
}
}
foreach ($html->find(static::PATHTODATA) as $element) {
$this->items[] = $this->getItemFromElement($element);
}
}
}

View File

@@ -1,27 +1,28 @@
<?php
class DansTonChatBridge extends BridgeAbstract {
const MAINTAINER = 'Astalaseven';
const NAME = 'DansTonChat Bridge';
const URI = 'https://danstonchat.com/';
const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'Returns latest quotes from DansTonChat.';
class DansTonChatBridge extends BridgeAbstract
{
const MAINTAINER = 'Astalaseven';
const NAME = 'DansTonChat Bridge';
const URI = 'https://danstonchat.com/';
const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'Returns latest quotes from DansTonChat.';
public function collectData(){
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI . 'latest.html');
$html = getSimpleHTMLDOM(self::URI . 'latest.html');
foreach($html->find('div.item') as $element) {
$item = array();
$item['uri'] = $element->find('a', 0)->href;
$titleContent = $element->find('h3 a', 0);
if($titleContent) {
$item['title'] = 'DansTonChat ' . html_entity_decode($titleContent->plaintext, ENT_QUOTES);
} else {
$item['title'] = 'DansTonChat';
}
$item['content'] = $element->find('div.item-content a', 0)->innertext;
$this->items[] = $item;
}
}
foreach ($html->find('div.item') as $element) {
$item = [];
$item['uri'] = $element->find('a', 0)->href;
$titleContent = $element->find('h3 a', 0);
if ($titleContent) {
$item['title'] = 'DansTonChat ' . html_entity_decode($titleContent->plaintext, ENT_QUOTES);
} else {
$item['title'] = 'DansTonChat';
}
$item['content'] = $element->find('div.item-content a', 0)->innertext;
$this->items[] = $item;
}
}
}

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